mirror of
https://github.com/enso-org/enso.git
synced 2024-12-22 21:01:37 +03:00
Add lint for jsdocs in GUI2 (#11234)
Part of #10526 (without TS structures: interfaces, type aliases etc.) Added lints checking for jsdocs presence (and their format) and generated stub with TODO for missing ones. Now only 344 docs are missing 🙃 # Important Notes Many changes are just automated, but they are in the middle commit. So recommended way of reviewing is review `Add jsdoc to eslint config` for checking config changes, and then `Some of the missing documentation and fixes`.
This commit is contained in:
parent
78993a0d1a
commit
7c5124094e
@ -24,6 +24,13 @@ export async function goToGraph(page: Page, closeDocPanel: boolean = true) {
|
||||
await expectNodePositionsInitialized(page, -16)
|
||||
}
|
||||
|
||||
/**
|
||||
* Run assertions for nodes and edges positions being properly initialized.
|
||||
*
|
||||
* Usually, after opening project or entering a node, we need some ticks for placing both nodes
|
||||
* and edges properly on the screen. If test relies on their positions, it must ensure this
|
||||
* initialization is done.
|
||||
*/
|
||||
export async function expectNodePositionsInitialized(page: Page, yPos: number) {
|
||||
// Wait until edges are initialized and displayed correctly.
|
||||
await expect(page.getByTestId('broken-edge')).toBeHidden()
|
||||
@ -38,6 +45,7 @@ export async function expectNodePositionsInitialized(page: Page, yPos: number) {
|
||||
)
|
||||
}
|
||||
|
||||
/** Exit the currently opened graph (of collapsed function). */
|
||||
export async function exitFunction(page: Page, x = 300, y = 300) {
|
||||
await locate.graphEditor(page).dblclick({ position: { x, y } })
|
||||
}
|
||||
@ -46,7 +54,7 @@ export async function exitFunction(page: Page, x = 300, y = 300) {
|
||||
// === Drag Node ===
|
||||
// =================
|
||||
|
||||
/// Move node defined by the given binding by the given x and y.
|
||||
/** Move node defined by the given binding by the given x and y. */
|
||||
export async function dragNodeByBinding(page: Page, nodeBinding: string, x: number, y: number) {
|
||||
const node = graphNodeByBinding(page, nodeBinding)
|
||||
const grabHandle = node.locator('.grab-handle')
|
||||
@ -56,13 +64,13 @@ export async function dragNodeByBinding(page: Page, nodeBinding: string, x: numb
|
||||
})
|
||||
}
|
||||
|
||||
/// Move mouse away to avoid random hover events and wait for any circular menus to disappear.
|
||||
/** Move mouse away to avoid random hover events and wait for any circular menus to disappear. */
|
||||
export async function ensureNoCircularMenusVisibleDueToHovering(page: Page) {
|
||||
await page.mouse.move(-1000, 0)
|
||||
await expect(locate.circularMenu(page)).toBeHidden()
|
||||
}
|
||||
|
||||
/// Ensure no nodes are selected.
|
||||
/** Ensure no nodes are selected. */
|
||||
export async function deselectNodes(page: Page) {
|
||||
await page.mouse.click(0, 0)
|
||||
await expect(locate.selectedNodes(page)).toHaveCount(0)
|
||||
|
@ -1,7 +1,8 @@
|
||||
import type { ElementHandle } from 'playwright'
|
||||
|
||||
/** Returns text content of the element, including CSS ::before and ::after content in the element's tree.
|
||||
* Currently whitespace produced around pseudo-elements is unspecified; block/inline logic is not implemented.
|
||||
/**
|
||||
* Returns text content of the element, including CSS ::before and ::after content in the element's tree.
|
||||
* Currently whitespace produced around pseudo-elements is unspecified; block/inline logic is not implemented.
|
||||
*/
|
||||
export function computedContent(element: ElementHandle<HTMLElement | SVGElement>): Promise<string> {
|
||||
return element.evaluate<string>((element) => {
|
||||
|
@ -1,8 +1,10 @@
|
||||
import { expect as baseExpect, type Locator } from 'playwright/test'
|
||||
|
||||
export const expect = baseExpect.extend({
|
||||
/** Ensures that at least one of the elements that the Locator points to,
|
||||
* is an attached and visible DOM node. */
|
||||
/**
|
||||
* Ensures that at least one of the elements that the Locator points to,
|
||||
* is an attached and visible DOM node.
|
||||
*/
|
||||
async toExist(locator: Locator) {
|
||||
// Counter-intuitive, but correct:
|
||||
// https://playwright.dev/docs/api/class-locatorassertions#locator-assertions-to-be-visible
|
||||
|
@ -44,20 +44,24 @@ function or(a: (page: Locator | Page) => Locator, b: (page: Locator | Page) => L
|
||||
return (page: Locator | Page) => a(page).or(b(page))
|
||||
}
|
||||
|
||||
/** Show/hide visualization button */
|
||||
export function toggleVisualizationButton(page: Locator | Page) {
|
||||
return page.getByLabel('Visualization', { exact: true })
|
||||
}
|
||||
|
||||
/** Visualization Selector button */
|
||||
export function toggleVisualizationSelectorButton(page: Locator | Page) {
|
||||
return page.getByLabel('Visualization Selector')
|
||||
}
|
||||
|
||||
// === Fullscreen ===
|
||||
|
||||
/** Enter fullscreen */
|
||||
export function enterFullscreenButton(page: Locator | Page) {
|
||||
return page.getByLabel('Fullscreen')
|
||||
}
|
||||
|
||||
/** Exit fullscreen */
|
||||
export function exitFullscreenButton(page: Locator | Page) {
|
||||
return page.getByLabel('Exit Fullscreen')
|
||||
}
|
||||
@ -67,23 +71,31 @@ export const toggleFullscreenButton = or(enterFullscreenButton, exitFullscreenBu
|
||||
// === Nodes ===
|
||||
|
||||
declare const nodeLocatorBrand: unique symbol
|
||||
|
||||
/** A locator which resolves to graph nodes only */
|
||||
export type Node = Locator & { [nodeLocatorBrand]: never }
|
||||
|
||||
/** All nodes in graph */
|
||||
export function graphNode(page: Page | Locator): Node {
|
||||
return page.locator('.GraphNode') as Node
|
||||
}
|
||||
/** Node with given binding (name) */
|
||||
export function graphNodeByBinding(page: Locator | Page, binding: string): Node {
|
||||
return graphNode(page).filter({ has: page.locator('.binding', { hasText: binding }) }) as Node
|
||||
}
|
||||
/** Icon inside the node */
|
||||
export function graphNodeIcon(node: Node) {
|
||||
return node.locator('.nodeCategoryIcon')
|
||||
}
|
||||
/** All selected nodes */
|
||||
export function selectedNodes(page: Page | Locator): Node {
|
||||
return page.locator('.GraphNode.selected') as Node
|
||||
}
|
||||
/** All input nodes */
|
||||
export function inputNode(page: Page | Locator): Node {
|
||||
return page.locator('.GraphNode.inputNode') as Node
|
||||
}
|
||||
/** All output nodes */
|
||||
export function outputNode(page: Page | Locator): Node {
|
||||
return page.locator('.GraphNode.outputNode') as Node
|
||||
}
|
||||
@ -107,6 +119,11 @@ export const nodeOutputPort = componentLocator('.outputPortHoverArea')
|
||||
export const smallPlusButton = componentLocator('.SmallPlusButton')
|
||||
export const lexicalContent = componentLocator('.LexicalContent')
|
||||
|
||||
/**
|
||||
* A not-selected variant of Component Browser Entry.
|
||||
*
|
||||
* It may be covered by selected one due to way we display them.
|
||||
*/
|
||||
export function componentBrowserEntry(
|
||||
page: Locator | Page,
|
||||
filter?: (f: Filter) => { selector: string },
|
||||
@ -116,6 +133,7 @@ export function componentBrowserEntry(
|
||||
)
|
||||
}
|
||||
|
||||
/** A selected variant of Component Browser Entry */
|
||||
export function componentBrowserSelectedEntry(
|
||||
page: Locator | Page,
|
||||
filter?: (f: Filter) => { selector: string },
|
||||
@ -125,10 +143,12 @@ export function componentBrowserSelectedEntry(
|
||||
)
|
||||
}
|
||||
|
||||
/** A not-selected variant of Component Browser entry with given label */
|
||||
export function componentBrowserEntryByLabel(page: Locator | Page, label: string) {
|
||||
return componentBrowserEntry(page).filter({ has: page.getByText(label) })
|
||||
}
|
||||
|
||||
/** Right-docked panel */
|
||||
export function rightDock(page: Page) {
|
||||
return page.getByTestId('rightDock')
|
||||
}
|
||||
@ -138,6 +158,7 @@ export function rightDockRoot(page: Page) {
|
||||
return page.getByTestId('rightDockRoot')
|
||||
}
|
||||
|
||||
/** Bottom-docked panel */
|
||||
export function bottomDock(page: Page) {
|
||||
return page.getByTestId('bottomDock')
|
||||
}
|
||||
@ -167,12 +188,14 @@ export const warningsVisualization = visualizationLocator('.WarningsVisualizatio
|
||||
|
||||
// === Edge locators ===
|
||||
|
||||
/** All edges going from a node with given binding. */
|
||||
export async function edgesFromNodeWithBinding(page: Page, binding: string) {
|
||||
const node = graphNodeByBinding(page, binding).first()
|
||||
const nodeId = await node.getAttribute('data-node-id')
|
||||
return page.locator(`[data-source-node-id="${nodeId}"]`)
|
||||
}
|
||||
|
||||
/** All edges going to a node with given binding. */
|
||||
export async function edgesToNodeWithBinding(page: Page, binding: string) {
|
||||
const node = graphNodeByBinding(page, binding).first()
|
||||
const nodeId = await node.getAttribute('data-node-id')
|
||||
@ -181,8 +204,9 @@ export async function edgesToNodeWithBinding(page: Page, binding: string) {
|
||||
|
||||
// === Output ports ===
|
||||
|
||||
/** Returns a location that can be clicked to activate an output port.
|
||||
* Using a `Locator` would be better, but `position` option of `click` doesn't work.
|
||||
/**
|
||||
* Returns a location that can be clicked to activate an output port.
|
||||
* Using a `Locator` would be better, but `position` option of `click` doesn't work.
|
||||
*/
|
||||
export async function outputPortCoordinates(node: Locator) {
|
||||
const outputPortArea = await node.locator('.outputPortHoverArea').boundingBox()
|
||||
|
@ -5,8 +5,10 @@ import { CONTROL_KEY } from './keyboard'
|
||||
import * as locate from './locate'
|
||||
import { edgesFromNodeWithBinding, edgesToNodeWithBinding } from './locate'
|
||||
|
||||
/** Every edge consists of multiple parts.
|
||||
* See e2e/edgeRendering.spec.ts for explanation. */
|
||||
/**
|
||||
* Every edge consists of multiple parts.
|
||||
* See e2e/edgeRendering.spec.ts for explanation.
|
||||
*/
|
||||
const EDGE_PARTS = 2
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
|
@ -9,6 +9,11 @@ import {
|
||||
} from '../mock/projectManager'
|
||||
import pmSpec from './pm-openrpc.json' assert { type: 'json' }
|
||||
|
||||
/**
|
||||
* Setup for all E2E tests.
|
||||
*
|
||||
* It runs mocked project manager server.
|
||||
*/
|
||||
export default function setup() {
|
||||
const pm = new Server({
|
||||
transportConfigs: [
|
||||
|
3
app/gui2/env.d.ts
vendored
3
app/gui2/env.d.ts
vendored
@ -12,7 +12,8 @@ interface Window {
|
||||
fileBrowserApi: FileBrowserApi
|
||||
}
|
||||
|
||||
/** `window.fileBrowserApi` is a context bridge to the main process, when we're running in an
|
||||
/**
|
||||
* `window.fileBrowserApi` is a context bridge to the main process, when we're running in an
|
||||
* Electron context.
|
||||
*
|
||||
* # Safety
|
||||
|
@ -1,5 +1,6 @@
|
||||
import { FlatCompat } from '@eslint/eslintrc'
|
||||
import eslintJs from '@eslint/js'
|
||||
import jsdoc from 'eslint-plugin-jsdoc'
|
||||
import * as path from 'node:path'
|
||||
import * as url from 'node:url'
|
||||
|
||||
@ -23,6 +24,7 @@ const conf = [
|
||||
...compat.extends('@vue/eslint-config-typescript', '@vue/eslint-config-prettier'),
|
||||
{
|
||||
// files: ['{**,src}/*.{vue,js,jsx,cjs,mjs,ts,tsx,cts,mts}'],
|
||||
plugins: { jsdoc },
|
||||
languageOptions: {
|
||||
parserOptions: {
|
||||
tsconfigRootDir: DIR_NAME,
|
||||
@ -51,6 +53,34 @@ const conf = [
|
||||
'vue/multi-word-component-names': 0,
|
||||
},
|
||||
},
|
||||
// JsDoc lints for typescript - the recommended set with some modifications.
|
||||
{
|
||||
ignores: ['**/*.js'],
|
||||
...jsdoc.configs['flat/recommended-typescript'],
|
||||
rules: {
|
||||
...jsdoc.configs['flat/recommended-typescript'].rules,
|
||||
'jsdoc/check-param-names': [
|
||||
'warn',
|
||||
{ checkDestructured: false, disableMissingParamChecks: true },
|
||||
],
|
||||
'jsdoc/require-jsdoc': [
|
||||
'warn',
|
||||
{
|
||||
publicOnly: true,
|
||||
require: {
|
||||
FunctionDeclaration: true,
|
||||
MethodDefinition: true,
|
||||
ClassDeclaration: true,
|
||||
ArrowFunctionExpression: false,
|
||||
FunctionExpression: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
'jsdoc/require-param': 'off',
|
||||
'jsdoc/require-returns': 'off',
|
||||
'jsdoc/require-yields': 'off',
|
||||
},
|
||||
},
|
||||
// We must make sure our E2E tests await all steps, otherwise they're flaky.
|
||||
{
|
||||
files: ['e2e/**/*.spec.ts'],
|
||||
|
@ -66,6 +66,7 @@ const PAYLOAD_CONSTRUCTOR = {
|
||||
[InboundPayload.CHECKSUM_BYTES_CMD]: ChecksumBytesCommand,
|
||||
} satisfies Record<InboundPayload, new () => Table>
|
||||
|
||||
/** TODO: Add docs */
|
||||
export function mockDataWSHandler(
|
||||
readFile: (segments: string[]) => Promise<ArrayBuffer | null | undefined>,
|
||||
cb?: (send: (data: string | Blob | ArrayBufferLike | ArrayBufferView) => void) => void,
|
||||
|
@ -33,7 +33,7 @@ import { mockDataWSHandler as originalMockDataWSHandler } from './dataServer'
|
||||
const mockProjectId = random.uuidv4() as Uuid
|
||||
const standardBase = 'Standard.Base' as QualifiedName
|
||||
|
||||
export function placeholderGroups(): LibraryComponentGroup[] {
|
||||
function placeholderGroups(): LibraryComponentGroup[] {
|
||||
return [
|
||||
{ color: '#4D9A29', name: 'Input', library: standardBase, exports: [] },
|
||||
{ color: '#B37923', name: 'Web', library: standardBase, exports: [] },
|
||||
@ -45,7 +45,7 @@ export function placeholderGroups(): LibraryComponentGroup[] {
|
||||
]
|
||||
}
|
||||
|
||||
let mainFile = `\
|
||||
const mainFile = `\
|
||||
## Module documentation
|
||||
from Standard.Base import all
|
||||
|
||||
@ -77,14 +77,6 @@ main =
|
||||
selected = data.select_columns
|
||||
`
|
||||
|
||||
export function getMainFile() {
|
||||
return mainFile
|
||||
}
|
||||
|
||||
export function setMainFile(newMainFile: string) {
|
||||
return (mainFile = newMainFile)
|
||||
}
|
||||
|
||||
const fileTree = {
|
||||
src: {
|
||||
get 'Main.enso'() {
|
||||
|
@ -1,19 +0,0 @@
|
||||
import { type VueWrapper } from '@vue/test-utils'
|
||||
import { nextTick } from 'vue'
|
||||
|
||||
// It is currently not feasible to use generics here, as the type of the component's emits
|
||||
// is not exposed.
|
||||
export function handleEmit(wrapper: VueWrapper<any>, event: string, fn: (...args: any[]) => void) {
|
||||
let previousLength = 0
|
||||
return {
|
||||
async run() {
|
||||
const emitted = wrapper.emitted(event)
|
||||
if (!emitted) return
|
||||
for (let i = previousLength; i < emitted.length; i += 1) {
|
||||
fn(...emitted[i]!)
|
||||
}
|
||||
previousLength = emitted.length
|
||||
await nextTick()
|
||||
},
|
||||
}
|
||||
}
|
@ -1,9 +1,11 @@
|
||||
/** @file Playwright browser testing configuration. */
|
||||
/** Note that running Playwright in CI poses a number of issues:
|
||||
/**
|
||||
* Note that running Playwright in CI poses a number of issues:
|
||||
* - `backdrop-filter: blur` is disabled, due to issues with Chromium's `--disable-gpu` flag
|
||||
* (see below).
|
||||
* - System validation dialogs are not reliable between computers, as they may have different
|
||||
* default fonts. */
|
||||
* default fonts.
|
||||
*/
|
||||
import { defineConfig } from '@playwright/test'
|
||||
import net from 'net'
|
||||
|
||||
|
@ -1,5 +1,7 @@
|
||||
/** @file A HTTP server middleware which handles routes normally proxied through to
|
||||
* the Project Manager. */
|
||||
/**
|
||||
* @file A HTTP server middleware which handles routes normally proxied through to
|
||||
* the Project Manager.
|
||||
*/
|
||||
import * as fsSync from 'node:fs'
|
||||
import * as fs from 'node:fs/promises'
|
||||
import * as http from 'node:http'
|
||||
@ -34,11 +36,13 @@ interface ProjectMetadata {
|
||||
readonly namespace: string
|
||||
/** The project id. */
|
||||
readonly id: string
|
||||
/** The Enso Engine version to use for the project, represented by a semver version
|
||||
/**
|
||||
* The Enso Engine version to use for the project, represented by a semver version
|
||||
* string.
|
||||
*
|
||||
* If the edition associated with the project could not be resolved, the
|
||||
* engine version may be missing. */
|
||||
* engine version may be missing.
|
||||
*/
|
||||
readonly engineVersion?: string
|
||||
/** The project creation time. */
|
||||
readonly created: string
|
||||
@ -406,8 +410,10 @@ export default function projectManagerShimMiddleware(
|
||||
}
|
||||
}
|
||||
|
||||
/** Return a {@link ProjectMetadata} if the metadata is a valid metadata object,
|
||||
* else return `null`. */
|
||||
/**
|
||||
* Return a {@link ProjectMetadata} if the metadata is a valid metadata object,
|
||||
* else return `null`.
|
||||
*/
|
||||
function extractProjectMetadata(yamlObj: unknown, jsonObj: unknown): ProjectMetadata | null {
|
||||
if (
|
||||
typeof yamlObj !== 'object' ||
|
||||
|
@ -1,4 +1,5 @@
|
||||
/** @file This module contains functions for importing projects into the Project Manager.
|
||||
/**
|
||||
* @file This module contains functions for importing projects into the Project Manager.
|
||||
*
|
||||
* Eventually this module should be replaced with a new Project Manager API that supports
|
||||
* importing projects.
|
||||
@ -6,7 +7,8 @@
|
||||
* - if the project is already in the Project Manager's location, we just open it;
|
||||
* - if the project is in a different location, we copy it to the Project Manager's location
|
||||
* and open it.
|
||||
* - if the project is a bundle, we extract it to the Project Manager's location and open it. */
|
||||
* - if the project is a bundle, we extract it to the Project Manager's location and open it.
|
||||
*/
|
||||
import * as crypto from 'node:crypto'
|
||||
import * as fs from 'node:fs'
|
||||
import * as os from 'node:os'
|
||||
@ -64,8 +66,10 @@ export async function uploadBundle(
|
||||
|
||||
/** The Project Manager's metadata associated with a project. */
|
||||
interface ProjectMetadata {
|
||||
/** The ID of the project. It is only used in communication with project manager;
|
||||
* it has no semantic meaning. */
|
||||
/**
|
||||
* The ID of the project. It is only used in communication with project manager;
|
||||
* it has no semantic meaning.
|
||||
*/
|
||||
readonly id: string
|
||||
/** The project variant. This is currently always `UserProject`. */
|
||||
readonly kind: 'UserProject'
|
||||
@ -75,14 +79,16 @@ interface ProjectMetadata {
|
||||
readonly lastOpened: string
|
||||
}
|
||||
|
||||
/** A type guard function to check if an object conforms to the {@link ProjectMetadata} interface.
|
||||
/**
|
||||
* A type guard function to check if an object conforms to the {@link ProjectMetadata} interface.
|
||||
*
|
||||
* This function checks if the input object has the required properties and correct types
|
||||
* to match the {@link ProjectMetadata} interface. It can be used at runtime to validate that
|
||||
* a given object has the expected shape.
|
||||
* @param value - The object to check against the ProjectMetadata interface.
|
||||
* @returns A boolean value indicating whether the object matches
|
||||
* the {@link ProjectMetadata} interface. */
|
||||
* the {@link ProjectMetadata} interface.
|
||||
*/
|
||||
function isProjectMetadata(value: unknown): value is ProjectMetadata {
|
||||
return typeof value === 'object' && value != null && 'id' in value && typeof value.id === 'string'
|
||||
}
|
||||
@ -132,10 +138,12 @@ function writeMetadata(projectRoot: string, metadata: ProjectMetadata): void {
|
||||
fs.writeFileSync(metadataPath, JSON.stringify(metadata, null, 4))
|
||||
}
|
||||
|
||||
/** Update the project's metadata.
|
||||
/**
|
||||
* Update the project's metadata.
|
||||
* If the provided updater does not return anything, the metadata file is left intact.
|
||||
*
|
||||
* Returns the metadata returned from the updater function. */
|
||||
* Returns the metadata returned from the updater function.
|
||||
*/
|
||||
function updateMetadata(
|
||||
projectRoot: string,
|
||||
updater: (initialMetadata: ProjectMetadata) => ProjectMetadata,
|
||||
@ -150,8 +158,10 @@ function updateMetadata(
|
||||
// === Project Directory ===
|
||||
// =========================
|
||||
|
||||
/** Check if the given path represents the root of an Enso project.
|
||||
* This is decided by the presence of the Project Manager's metadata. */
|
||||
/**
|
||||
* Check if the given path represents the root of an Enso project.
|
||||
* This is decided by the presence of the Project Manager's metadata.
|
||||
*/
|
||||
function isProjectRoot(candidatePath: string): boolean {
|
||||
const projectJsonPath = pathModule.join(candidatePath, PROJECT_METADATA_RELATIVE_PATH)
|
||||
try {
|
||||
@ -162,13 +172,15 @@ function isProjectRoot(candidatePath: string): boolean {
|
||||
}
|
||||
}
|
||||
|
||||
/** Generate a name for a project using given base string. A suffix is added if there is a
|
||||
/**
|
||||
* Generate a name for a project using given base string. A suffix is added if there is a
|
||||
* collision.
|
||||
*
|
||||
* For example `Name` will become `Name_1` if there's already a directory named `Name`.
|
||||
* If given a name like `Name_1` it will become `Name_2` if there is already a directory named
|
||||
* `Name_1`. If a path containing multiple components is given, only the last component is used
|
||||
* for the name. */
|
||||
* for the name.
|
||||
*/
|
||||
function generateDirectoryName(name: string, directory = getProjectsDirectory()): string {
|
||||
// Use only the last path component.
|
||||
name = pathModule.parse(name).name
|
||||
@ -198,8 +210,10 @@ function generateDirectoryName(name: string, directory = getProjectsDirectory())
|
||||
return finalPath
|
||||
}
|
||||
|
||||
/** Take a path to a file, presumably located in a project's subtree.Returns the path
|
||||
* to the project's root directory or `null` if the file is not located in a project. */
|
||||
/**
|
||||
* Take a path to a file, presumably located in a project's subtree.Returns the path
|
||||
* to the project's root directory or `null` if the file is not located in a project.
|
||||
*/
|
||||
export function getProjectRoot(subtreePath: string): string | null {
|
||||
let currentPath = subtreePath
|
||||
while (!isProjectRoot(currentPath)) {
|
||||
|
@ -1,6 +1,8 @@
|
||||
/** @file ⚠️⚠️⚠️ THIS SCRIPT IS PROVIDED ONLY FOR CONVENIENCE. ⚠️⚠️⚠️
|
||||
/**
|
||||
* @file ⚠️⚠️⚠️ THIS SCRIPT IS PROVIDED ONLY FOR CONVENIENCE. ⚠️⚠️⚠️
|
||||
* The sources of truth are at `build/build/src/project/gui.rs` and
|
||||
* `build/build/src/ide/web/fonts.rs`. */
|
||||
* `build/build/src/ide/web/fonts.rs`.
|
||||
*/
|
||||
|
||||
import * as fsSync from 'node:fs'
|
||||
import * as fs from 'node:fs/promises'
|
||||
@ -23,8 +25,10 @@ const MPLUS1_FONT_URL =
|
||||
const DEJAVU_SANS_MONO_FONT_URL =
|
||||
'https://sourceforge.net/projects/dejavu/files/dejavu/2.37/dejavu-fonts-ttf-2.37.tar.bz2'
|
||||
|
||||
/** @param {string | https.RequestOptions | URL} options
|
||||
* @param {((res: import('node:http').IncomingMessage) => void) | undefined} [callback] */
|
||||
/**
|
||||
* @param {string | https.RequestOptions | URL} options
|
||||
* @param {((res: import('node:http').IncomingMessage) => void) | undefined} [callback]
|
||||
*/
|
||||
function get(options, callback) {
|
||||
const protocol =
|
||||
typeof options === 'string' ? new URL(options).protocol : options.protocol ?? 'https:'
|
||||
|
@ -15,6 +15,7 @@ await fs.writeFile(
|
||||
// Please run \`npm run generate-metadata\` to regenerate this file whenever \`icons.svg\` is changed.
|
||||
import iconNames from '@/util/iconList.json'
|
||||
|
||||
/** One of the possible icon names of SvgIcon component. */
|
||||
export type Icon =
|
||||
${iconNames?.map((name) => ` | '${name}'`).join('\n')}
|
||||
|
||||
@ -22,6 +23,7 @@ export { iconNames }
|
||||
|
||||
const iconNamesSet = new Set(iconNames)
|
||||
|
||||
/** Check if string is one of the valid icon names for SvgIcon. */
|
||||
export function isIconName(value: string): value is Icon {
|
||||
return iconNamesSet.has(value)
|
||||
}
|
||||
|
@ -1,5 +1,8 @@
|
||||
import '@/assets/base.css'
|
||||
|
||||
/**
|
||||
* Load App.vue asynchronously.
|
||||
*/
|
||||
export async function AsyncApp() {
|
||||
const app = await import('@/App.vue')
|
||||
return app
|
||||
|
@ -47,6 +47,7 @@ import type { Diagnostic as LSDiagnostic } from 'ydoc-shared/languageServerTypes
|
||||
import { tryGetSoleValue } from 'ydoc-shared/util/data/iterable'
|
||||
import type { SourceRangeEdit } from 'ydoc-shared/util/data/text'
|
||||
|
||||
/** TODO: Add docs */
|
||||
export function lsDiagnosticsToCMDiagnostics(
|
||||
source: string,
|
||||
diagnostics: LSDiagnostic[],
|
||||
@ -172,10 +173,12 @@ class EnsoLanguage extends Language {
|
||||
|
||||
const ensoLanguage = new EnsoLanguage()
|
||||
|
||||
/** TODO: Add docs */
|
||||
export function enso() {
|
||||
return new LanguageSupport(ensoLanguage)
|
||||
}
|
||||
|
||||
/** TODO: Add docs */
|
||||
export function hoverTooltip(
|
||||
create: (
|
||||
ast: AstNode,
|
||||
@ -199,6 +202,7 @@ export function hoverTooltip(
|
||||
})
|
||||
}
|
||||
|
||||
/** TODO: Add docs */
|
||||
export function textEditToChangeSpec({ range: [from, to], insert }: SourceRangeEdit): ChangeSpec {
|
||||
return { from, to, insert }
|
||||
}
|
||||
|
@ -21,10 +21,12 @@ function normalizeRangeInputs(inputs: Iterable<number>, radius: number) {
|
||||
return normalizedInputs
|
||||
}
|
||||
|
||||
/** TODO: Add docs */
|
||||
export function seminormalizeHue(value: number) {
|
||||
return value === 1 ? 1 : normalizeHue(value)
|
||||
}
|
||||
|
||||
/** TODO: Add docs */
|
||||
export function rangesForInputs(inputs: Iterable<number>, radius: number): FixedRange[] {
|
||||
if (radius === 0) return []
|
||||
const ranges = new Array<FixedRange & { rawHue: number }>()
|
||||
@ -80,6 +82,7 @@ export interface GradientPoint {
|
||||
angle: number
|
||||
angle2?: number
|
||||
}
|
||||
/** TODO: Add docs */
|
||||
export function cssAngularColorStop({ hue, angle, angle2 }: GradientPoint) {
|
||||
return [
|
||||
formatCssColor(ensoColor(hue)),
|
||||
@ -88,6 +91,7 @@ export function cssAngularColorStop({ hue, angle, angle2 }: GradientPoint) {
|
||||
].join(' ')
|
||||
}
|
||||
|
||||
/** TODO: Add docs */
|
||||
export function gradientPoints(
|
||||
inputRanges: Iterable<FixedRange>,
|
||||
minStops?: number | undefined,
|
||||
|
@ -8,6 +8,10 @@ import type { ExternalId } from 'ydoc-shared/yjsModel'
|
||||
const AI_GOAL_PLACEHOLDER = '__$$GOAL$$__'
|
||||
const AI_STOP_SEQUENCE = '`'
|
||||
|
||||
/**
|
||||
* A Composable for using AI prompts in Component Browser. Use `query` function to get AI result
|
||||
* for given query.
|
||||
*/
|
||||
export function useAI(
|
||||
graphDb: GraphDb = useGraphStore().db,
|
||||
project: {
|
||||
|
@ -23,12 +23,16 @@ interface ComponentLabel {
|
||||
matchedRanges?: Range[] | undefined
|
||||
}
|
||||
|
||||
/**
|
||||
* A model of component suggestion displayed in the Component Browser.
|
||||
*/
|
||||
export interface Component extends ComponentLabel {
|
||||
suggestionId: SuggestionId
|
||||
icon: Icon
|
||||
group?: number | undefined
|
||||
}
|
||||
|
||||
/** @returns the displayed label of given suggestion entry with information of highlighted ranges. */
|
||||
export function labelOfEntry(entry: SuggestionEntry, match: MatchResult): ComponentLabelInfo {
|
||||
if (entry.memberOf && entry.selfType == null) {
|
||||
const ownerLastSegmentStart = qnLastSegmentIndex(entry.memberOf) + 1
|
||||
@ -68,12 +72,18 @@ function formatLabel(labelInfo: ComponentLabelInfo): ComponentLabel {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Suggestion entry with matching information.
|
||||
*/
|
||||
export interface MatchedSuggestion {
|
||||
id: SuggestionId
|
||||
entry: SuggestionEntry
|
||||
match: MatchResult
|
||||
}
|
||||
|
||||
/**
|
||||
* A suggestion comparator. The "lower" suggestion should be first in Component Browser's list.
|
||||
*/
|
||||
export function compareSuggestions(a: MatchedSuggestion, b: MatchedSuggestion): number {
|
||||
const matchCompare = a.match.score - b.match.score
|
||||
if (matchCompare !== 0) return matchCompare
|
||||
@ -87,12 +97,15 @@ export function compareSuggestions(a: MatchedSuggestion, b: MatchedSuggestion):
|
||||
return a.id - b.id
|
||||
}
|
||||
|
||||
export interface ComponentInfo {
|
||||
interface ComponentInfo {
|
||||
id: number
|
||||
entry: SuggestionEntry
|
||||
match: MatchResult
|
||||
}
|
||||
|
||||
/**
|
||||
* Create {@link Component} from information about suggestion and matching.
|
||||
*/
|
||||
export function makeComponent({ id, entry, match }: ComponentInfo): Component {
|
||||
return {
|
||||
...formatLabel(labelOfEntry(entry, match)),
|
||||
@ -102,6 +115,9 @@ export function makeComponent({ id, entry, match }: ComponentInfo): Component {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create {@link Component} list from filtered suggestions.
|
||||
*/
|
||||
export function makeComponentList(db: SuggestionDb, filtering: Filtering): Component[] {
|
||||
function* matchSuggestions() {
|
||||
for (const [id, entry] of db.entries()) {
|
||||
|
@ -238,6 +238,7 @@ export class Filtering {
|
||||
selfArg?: SelfArg
|
||||
currentModule?: QualifiedName
|
||||
|
||||
/** TODO: Add docs */
|
||||
constructor(filter: Filter, currentModule: Opt<QualifiedName> = undefined) {
|
||||
const { pattern, selfArg } = filter
|
||||
if (pattern) {
|
||||
@ -253,6 +254,7 @@ export class Filtering {
|
||||
else return entry.selfType != null
|
||||
}
|
||||
|
||||
/** TODO: Add docs */
|
||||
isMainView() {
|
||||
return this.pattern == null && this.selfArg == null
|
||||
}
|
||||
@ -268,6 +270,7 @@ export class Filtering {
|
||||
return this.currentModule != null && entry.definedIn === this.currentModule
|
||||
}
|
||||
|
||||
/** TODO: Add docs */
|
||||
filter(entry: SuggestionEntry): MatchResult | null {
|
||||
if (entry.isPrivate || entry.kind != SuggestionKind.Method || entry.memberOf == null)
|
||||
return null
|
||||
|
@ -9,16 +9,17 @@ import { isIdentifier, type AstId, type Identifier } from '@/util/ast/abstract'
|
||||
import { Err, Ok, type Result } from '@/util/data/result'
|
||||
import { qnLastSegment, type QualifiedName } from '@/util/qualifiedName'
|
||||
import { useToast } from '@/util/toast'
|
||||
import { computed, proxyRefs, readonly, ref, watch, type ComputedRef } from 'vue'
|
||||
import { computed, proxyRefs, readonly, ref, type ComputedRef } from 'vue'
|
||||
|
||||
/** Information how the component browser is used, needed for proper input initializing. */
|
||||
export type Usage =
|
||||
| { type: 'newNode'; sourcePort?: AstId | undefined }
|
||||
| { type: 'editNode'; node: NodeId; cursorPos: number }
|
||||
|
||||
/** One of the modes of the component browser:
|
||||
* * "component browsing" when user wants to add new component
|
||||
* * "code editing" for editing existing, or just added nodes
|
||||
/**
|
||||
* One of the modes of the component browser:
|
||||
* "component browsing" when user wants to add new component
|
||||
* "code editing" for editing existing, or just added nodes
|
||||
* See https://github.com/enso-org/enso/issues/10598 for design details.
|
||||
*/
|
||||
export type ComponentBrowserMode =
|
||||
@ -177,7 +178,8 @@ export function useComponentBrowserInput(
|
||||
}
|
||||
}
|
||||
|
||||
/** List of imports required for applied suggestions.
|
||||
/**
|
||||
* List of imports required for applied suggestions.
|
||||
*
|
||||
* If suggestion was manually edited by the user after accepting, it is not included.
|
||||
*/
|
||||
|
@ -16,6 +16,9 @@ const orDefaultSize = (rect: Rect) => {
|
||||
return new Rect(rect.pos, new Vec2(width, height))
|
||||
}
|
||||
|
||||
/**
|
||||
* A composable with logic related to nodes placement.
|
||||
*/
|
||||
export function usePlacement(nodeRects: ToValue<Iterable<Rect>>, screenBounds: ToValue<Rect>) {
|
||||
const gap = themeGap()
|
||||
const environment = (selectedNodeRects: Iterable<Rect>) => ({
|
||||
@ -24,10 +27,13 @@ export function usePlacement(nodeRects: ToValue<Iterable<Rect>>, screenBounds: T
|
||||
nodeRects: Array.from(toValue(nodeRects), orDefaultSize),
|
||||
})
|
||||
return {
|
||||
/** Find a free position for a new node. For details, see {@link previousNodeDictatedPlacement}. */
|
||||
place: (selectedNodeRects: Iterable<Rect> = [], nodeSize: Vec2 = DEFAULT_NODE_SIZE): Vec2 =>
|
||||
previousNodeDictatedPlacement(nodeSize, environment(selectedNodeRects), gap),
|
||||
/** Compute position of new collapsed node. For details, see {@link collapsedNodePlacement}. */
|
||||
collapse: (selectedNodeRects: Iterable<Rect>, nodeSize: Vec2 = DEFAULT_NODE_SIZE): Vec2 =>
|
||||
collapsedNodePlacement(nodeSize, environment(selectedNodeRects), gap),
|
||||
/** Compute position of an input node. For details, see {@link inputNodePlacement}. */
|
||||
input: (nonInputNodeRects: Iterable<Rect>, nodeSize: Vec2 = DEFAULT_NODE_SIZE): Vec2 =>
|
||||
inputNodePlacement(nodeSize, { ...environment([]), nonInputNodeRects }, gap),
|
||||
}
|
||||
@ -50,7 +56,8 @@ function themeGap(): Vec2 {
|
||||
return new Vec2(theme.node.horizontal_gap, theme.node.vertical_gap)
|
||||
}
|
||||
|
||||
/** The new node should appear at the center of the screen if there is enough space for the new node.
|
||||
/**
|
||||
* The new node should appear at the center of the screen if there is enough space for the new node.
|
||||
* Otherwise, it should be moved down to the closest free space.
|
||||
*
|
||||
* Specifically, this code, in order:
|
||||
@ -60,7 +67,8 @@ function themeGap(): Vec2 {
|
||||
* - shifts the node down (if required) until there is sufficient vertical space -
|
||||
* the height of the node, in addition to the specified gap both above and below the node.
|
||||
*
|
||||
* [Documentation](https://github.com/enso-org/design/blob/main/epics/component-browser/design.md#placement-of-newly-opened-component-browser) */
|
||||
* [Documentation](https://github.com/enso-org/design/blob/main/epics/component-browser/design.md#placement-of-newly-opened-component-browser)
|
||||
*/
|
||||
export function nonDictatedPlacement(
|
||||
nodeSize: Vec2,
|
||||
{ screenBounds, nodeRects }: NonDictatedEnvironment,
|
||||
@ -70,7 +78,8 @@ export function nonDictatedPlacement(
|
||||
return seekVertical(new Rect(initialPosition, nodeSize), nodeRects, gap)
|
||||
}
|
||||
|
||||
/** The new node should be left aligned to the first selected node (order of selection matters).
|
||||
/**
|
||||
* The new node should be left aligned to the first selected node (order of selection matters).
|
||||
* The Panel should also be placed vertically directly below the lowest of all selected nodes.
|
||||
*
|
||||
* If there is not enough empty space, the Expression Input Panel should be moved right
|
||||
@ -90,7 +99,8 @@ export function nonDictatedPlacement(
|
||||
* Note that the algorithm for finding free space is almost the same as for non-dictated placement,
|
||||
* except it searches horizontally instead of vertically.
|
||||
*
|
||||
* [Documentation](https://github.com/enso-org/design/blob/main/epics/component-browser/design.md#placement-of-newly-opened-component-browser) */
|
||||
* [Documentation](https://github.com/enso-org/design/blob/main/epics/component-browser/design.md#placement-of-newly-opened-component-browser)
|
||||
*/
|
||||
export function previousNodeDictatedPlacement(
|
||||
nodeSize: Vec2,
|
||||
{ screenBounds, selectedNodeRects, nodeRects }: Environment,
|
||||
@ -110,13 +120,15 @@ export function previousNodeDictatedPlacement(
|
||||
return seekHorizontal(new Rect(initialPosition, nodeSize), nodeRects, gap)
|
||||
}
|
||||
|
||||
/** The new node should appear exactly below the mouse.
|
||||
/**
|
||||
* The new node should appear exactly below the mouse.
|
||||
*
|
||||
* Specifically, this code assumes the node is fully rounded on the left and right sides,
|
||||
* so it adds half the node height (assumed to be the node radius) from the mouse x and y
|
||||
* positions.
|
||||
*
|
||||
* [Documentation](https://github.com/enso-org/design/blob/main/epics/component-browser/design.md#placement-of-newly-opened-component-browser) */
|
||||
* [Documentation](https://github.com/enso-org/design/blob/main/epics/component-browser/design.md#placement-of-newly-opened-component-browser)
|
||||
*/
|
||||
export function mouseDictatedPlacement(
|
||||
mousePosition: Vec2,
|
||||
nodeSize: Vec2 = DEFAULT_NODE_SIZE,
|
||||
@ -145,7 +157,8 @@ export function inputNodePlacement(
|
||||
return seekHorizontal(new Rect(initialPosition, nodeSize), nodeRects, gap)
|
||||
}
|
||||
|
||||
/** The new node should appear at the average Y-position of selected nodes and with the X-position of the leftmost node.
|
||||
/**
|
||||
* The new node should appear at the average Y-position of selected nodes and with the X-position of the leftmost node.
|
||||
*
|
||||
* If the desired place is already occupied by non-selected node, it should be moved down to the closest free space.
|
||||
*
|
||||
@ -181,8 +194,10 @@ export function collapsedNodePlacement(
|
||||
return seekVertical(new Rect(initialPosition, nodeSize), nonSelectedNodeRects, gap)
|
||||
}
|
||||
|
||||
/** Given a preferred location for a node, adjust the top as low as necessary for it not to collide with any of the
|
||||
* provided `otherRects`. */
|
||||
/**
|
||||
* Given a preferred location for a node, adjust the top as low as necessary for it not to collide with any of the
|
||||
* provided `otherRects`.
|
||||
*/
|
||||
export function seekVertical(preferredRect: Rect, otherRects: Iterable<Rect>, gap = themeGap()) {
|
||||
const initialRect = orDefaultSize(preferredRect)
|
||||
const nodeRectsSorted = Array.from(otherRects, orDefaultSize).sort((a, b) => a.top - b.top)
|
||||
@ -198,8 +213,10 @@ export function seekVertical(preferredRect: Rect, otherRects: Iterable<Rect>, ga
|
||||
return new Vec2(initialRect.left, top)
|
||||
}
|
||||
|
||||
/** Given a preferred location for a node, adjust the left edge as much as necessary for it not to collide with any of
|
||||
* the provided `otherRects`. */
|
||||
/**
|
||||
* Given a preferred location for a node, adjust the left edge as much as necessary for it not to collide with any of
|
||||
* the provided `otherRects`.
|
||||
*/
|
||||
export function seekHorizontal(initialRect: Rect, otherRects: Iterable<Rect>, gap = themeGap()) {
|
||||
return seekVertical(
|
||||
orDefaultSize(initialRect).reflectXY(),
|
||||
|
@ -7,6 +7,13 @@ export type ScrollTarget =
|
||||
| { type: 'selected' }
|
||||
| { type: 'offset'; offset: number }
|
||||
|
||||
/**
|
||||
* Scrolling for the Component Browser List.
|
||||
*
|
||||
* The scrolling may be a bit different depending on if we want to scroll to selection (and then
|
||||
* stick to it), or top/specific offset. The scroll value should be updated by setting
|
||||
* `targetScroll` value, or by calling `scrollWithTransition` if we want animated scroll.
|
||||
*/
|
||||
export function useScrolling(selectedPos: ToValue<number>) {
|
||||
const targetScroll = ref<ScrollTarget>({ type: 'top' })
|
||||
const targetScrollPosition = computed(() => {
|
||||
|
@ -10,40 +10,47 @@ export class HistoryStack {
|
||||
private index: Ref<number>
|
||||
public current: ComputedRef<SuggestionId | undefined>
|
||||
|
||||
/** TODO: Add docs */
|
||||
constructor() {
|
||||
this.stack = reactive([])
|
||||
this.index = ref(0)
|
||||
this.current = computed(() => this.stack[this.index.value] ?? undefined)
|
||||
}
|
||||
|
||||
/** TODO: Add docs */
|
||||
public reset(current: SuggestionId) {
|
||||
this.stack.length = 0
|
||||
this.stack.push(current)
|
||||
this.index.value = 0
|
||||
}
|
||||
|
||||
/** TODO: Add docs */
|
||||
public record(id: SuggestionId) {
|
||||
this.stack.splice(this.index.value + 1)
|
||||
this.stack.push(id)
|
||||
this.index.value = this.stack.length - 1
|
||||
}
|
||||
|
||||
/** TODO: Add docs */
|
||||
public forward() {
|
||||
if (this.canGoForward()) {
|
||||
this.index.value += 1
|
||||
}
|
||||
}
|
||||
|
||||
/** TODO: Add docs */
|
||||
public backward() {
|
||||
if (this.canGoBackward()) {
|
||||
this.index.value -= 1
|
||||
}
|
||||
}
|
||||
|
||||
/** TODO: Add docs */
|
||||
public canGoBackward(): boolean {
|
||||
return this.index.value > 0
|
||||
}
|
||||
|
||||
/** TODO: Add docs */
|
||||
public canGoForward(): boolean {
|
||||
return this.index.value < this.stack.length - 1
|
||||
}
|
||||
|
@ -59,6 +59,9 @@ export interface Example {
|
||||
body: Doc.HtmlString
|
||||
}
|
||||
|
||||
/**
|
||||
* Placeholder constructor.
|
||||
*/
|
||||
export function placeholder(text: string): Placeholder {
|
||||
return { kind: 'Placeholder', text }
|
||||
}
|
||||
@ -96,6 +99,9 @@ function filterSections(sections: Iterable<Doc.Section>): Sections {
|
||||
|
||||
// === Lookup ===
|
||||
|
||||
/**
|
||||
* The main function for getting documentation page for given entry.
|
||||
*/
|
||||
export function lookupDocumentation(db: SuggestionDb, id: SuggestionId): Docs {
|
||||
const entry = db.get(id)
|
||||
if (!entry)
|
||||
|
@ -104,8 +104,10 @@ const sourceRect = computed<Rect | undefined>(() => {
|
||||
}
|
||||
})
|
||||
|
||||
/** Edges which do not have `sourceRect` and `targetPos` initialized are marked by a special
|
||||
* `broken-edge` data-testid, for debugging and e2e test purposes. */
|
||||
/**
|
||||
* Edges which do not have `sourceRect` and `targetPos` initialized are marked by a special
|
||||
* `broken-edge` data-testid, for debugging and e2e test purposes.
|
||||
*/
|
||||
const edgeIsBroken = computed(
|
||||
() =>
|
||||
sourceRect.value == null ||
|
||||
@ -145,11 +147,15 @@ const edgeColor = computed(() =>
|
||||
|
||||
/** The inputs to the edge state computation. */
|
||||
interface Inputs {
|
||||
/** The width and height of the node that originates the edge, if any.
|
||||
* The edge may begin anywhere around the bottom half of the node. */
|
||||
/**
|
||||
* The width and height of the node that originates the edge, if any.
|
||||
* The edge may begin anywhere around the bottom half of the node.
|
||||
*/
|
||||
sourceSize: Vec2
|
||||
/** The coordinates of the node input port that is the edge's destination, relative to the source
|
||||
* position. The edge enters the port from above. */
|
||||
/**
|
||||
* The coordinates of the node input port that is the edge's destination, relative to the source
|
||||
* position. The edge enters the port from above.
|
||||
*/
|
||||
targetOffset: Vec2
|
||||
}
|
||||
|
||||
@ -164,7 +170,8 @@ function circleIntersection(x: number, r1: number, r2: number): number {
|
||||
return Math.sqrt(r1 * r1 + r2 * r2 - xNorm * xNorm)
|
||||
}
|
||||
|
||||
/** Edge layout calculation.
|
||||
/**
|
||||
* Edge layout calculation.
|
||||
*
|
||||
* # Corners
|
||||
*
|
||||
@ -200,7 +207,8 @@ function circleIntersection(x: number, r1: number, r2: number): number {
|
||||
* horizontal/vertical, has a one-to-one relationship with a sequence of corners.
|
||||
*/
|
||||
|
||||
/** Calculate the start and end positions of each 1-corner section composing an edge to the
|
||||
/**
|
||||
* Calculate the start and end positions of each 1-corner section composing an edge to the
|
||||
* given offset. Return the points and the maximum radius that should be used to draw the corners
|
||||
* connecting them.
|
||||
*/
|
||||
|
@ -16,9 +16,11 @@ import { Vec2 } from '@/util/data/vec2'
|
||||
import { computed, nextTick, onUnmounted, ref, toRef, watch, watchEffect } from 'vue'
|
||||
import { visIdentifierEquals, type VisualizationIdentifier } from 'ydoc-shared/yjsModel'
|
||||
|
||||
/** The minimum width must be at least the total width of:
|
||||
/**
|
||||
* The minimum width must be at least the total width of:
|
||||
* - both of toolbars that are always visible (32px + 60px), and
|
||||
* - the 4px flex gap between the toolbars. */
|
||||
* - the 4px flex gap between the toolbars.
|
||||
*/
|
||||
const MIN_WIDTH_PX = 200
|
||||
const MIN_CONTENT_HEIGHT_PX = 32
|
||||
const DEFAULT_CONTENT_HEIGHT_PX = 150
|
||||
|
@ -29,7 +29,7 @@ import type { Opt } from 'ydoc-shared/util/data/opt'
|
||||
import type { Result } from 'ydoc-shared/util/data/result'
|
||||
import type { VisualizationIdentifier } from 'ydoc-shared/yjsModel'
|
||||
|
||||
// Used for testing.
|
||||
/** Used for testing. */
|
||||
export type RawDataSource = { type: 'raw'; data: any }
|
||||
|
||||
export interface UseVisualizationDataOptions {
|
||||
@ -38,6 +38,14 @@ export interface UseVisualizationDataOptions {
|
||||
dataSource: ToValue<VisualizationDataSource | RawDataSource | undefined>
|
||||
}
|
||||
|
||||
/**
|
||||
* Visualization data composable for Visualization component.
|
||||
*
|
||||
* This composable manages picking the proper visualization component, attaching engine's
|
||||
* visualization to get input data, and updating the preprocessor if requested.
|
||||
*
|
||||
* TODO[ao]: Docs about returned refs and functions.
|
||||
*/
|
||||
export function useVisualizationData({
|
||||
selectedVis,
|
||||
dataSource,
|
||||
|
@ -26,7 +26,7 @@ interface CopiedNode {
|
||||
metadata?: NodeMetadataFields
|
||||
}
|
||||
|
||||
/** @internal Exported for testing. */
|
||||
/** @internal */
|
||||
export async function nodesFromClipboardContent(
|
||||
clipboardItems: ClipboardItems,
|
||||
): Promise<CopiedNode[]> {
|
||||
@ -54,6 +54,7 @@ function getClipboard(): ExtendedClipboard {
|
||||
return (window.navigator as any).mockClipboard ?? window.navigator.clipboard
|
||||
}
|
||||
|
||||
/** A composable for handling copying and pasting nodes in the GraphEditor. */
|
||||
export function useGraphEditorClipboard(
|
||||
graphStore: GraphStore,
|
||||
selected: ToValue<Set<NodeId>>,
|
||||
@ -149,12 +150,13 @@ const spreadsheetDecoder: ClipboardDecoder<CopiedNode[]> = {
|
||||
|
||||
const toTable = computed(() => Pattern.parse('__.to Table'))
|
||||
|
||||
/** Create Enso Expression generating table from this tsvData. */
|
||||
export function tsvTableToEnsoExpression(tsvData: string) {
|
||||
const textLiteral = Ast.TextLiteral.new(tsvData)
|
||||
return toTable.value.instantiate(textLiteral.module, [textLiteral]).code()
|
||||
}
|
||||
|
||||
/** @internal Exported for testing. */
|
||||
/** @internal */
|
||||
export function isSpreadsheetTsv(htmlContent: string) {
|
||||
// This is a very general criterion that can have some false-positives (e.g. pasting rich text that includes a table).
|
||||
// However, due to non-standardized browser HTML sanitization it is difficult to precisely recognize spreadsheet
|
||||
@ -171,6 +173,7 @@ export function isSpreadsheetTsv(htmlContent: string) {
|
||||
export type MimeType = 'text/plain' | 'text/html' | typeof ENSO_MIME_TYPE
|
||||
export type MimeData = Partial<Record<MimeType, string>>
|
||||
|
||||
/** Write data to clipboard */
|
||||
export function writeClipboard(data: MimeData) {
|
||||
const dataBlobs = Object.fromEntries(
|
||||
Object.entries(data).map(([type, typeData]) => [type, new Blob([typeData], { type })]),
|
||||
@ -191,11 +194,13 @@ function nodeStructuredData(node: Node): CopiedNode {
|
||||
}
|
||||
}
|
||||
|
||||
/** TODO: Add docs */
|
||||
export function clipboardNodeData(nodes: CopiedNode[]): MimeData {
|
||||
const clipboardData: ClipboardData = { nodes }
|
||||
return { [ENSO_MIME_TYPE]: JSON.stringify(clipboardData) }
|
||||
}
|
||||
|
||||
/** TODO: Add docs */
|
||||
export function nodesToClipboardData(nodes: Node[]): MimeData {
|
||||
return {
|
||||
...clipboardNodeData(nodes.map(nodeStructuredData)),
|
||||
|
@ -31,7 +31,8 @@ interface ExtractedInfo {
|
||||
|
||||
/** The information about the output value of the extracted function. */
|
||||
interface Output {
|
||||
/** The id of the node the expression of which should be replaced by the function call.
|
||||
/**
|
||||
* The id of the node the expression of which should be replaced by the function call.
|
||||
* This node is also included into `ids` of the {@link ExtractedInfo} and must be moved into the extracted function.
|
||||
*/
|
||||
node: NodeId
|
||||
@ -51,7 +52,8 @@ interface RefactoredInfo {
|
||||
|
||||
// === prepareCollapsedInfo ===
|
||||
|
||||
/** Prepare the information necessary for collapsing nodes.
|
||||
/**
|
||||
* Prepare the information necessary for collapsing nodes.
|
||||
* @throws errors in case of failures, but it should not happen in normal execution.
|
||||
*/
|
||||
export function prepareCollapsedInfo(
|
||||
@ -153,7 +155,8 @@ interface CollapsingResult {
|
||||
/** The ID of the node refactored to the collapsed function call. */
|
||||
refactoredNodeId: NodeId
|
||||
refactoredExpressionAstId: Ast.AstId
|
||||
/** IDs of nodes inside the collapsed function, except the output node.
|
||||
/**
|
||||
* IDs of nodes inside the collapsed function, except the output node.
|
||||
* The order of these IDs is reversed comparing to the order of nodes in the source code.
|
||||
*/
|
||||
collapsedNodeIds: NodeId[]
|
||||
|
@ -17,12 +17,22 @@ interface PartialVec2 {
|
||||
y: number | null
|
||||
}
|
||||
|
||||
/**
|
||||
* Snap Grid for dragged nodes.
|
||||
*
|
||||
* Created from existing nodes' rects, it allows "snapping" dragged nodes to another nodes on
|
||||
* the scene, so the user could easily and nicely ailgn their nodes.
|
||||
*
|
||||
* The nodes will be snapped to align with every edge of any other node, and also at place above
|
||||
* and below node leaving default vertical gap (same as when adding new node).
|
||||
*/
|
||||
export class SnapGrid {
|
||||
leftAxes: ComputedRef<number[]>
|
||||
rightAxes: ComputedRef<number[]>
|
||||
topAxes: ComputedRef<number[]>
|
||||
bottomAxes: ComputedRef<number[]>
|
||||
|
||||
/** Create grid from existing nodes' rects */
|
||||
constructor(rects: ComputedRef<Rect[]>) {
|
||||
markRaw(this)
|
||||
this.leftAxes = computed(() =>
|
||||
@ -43,6 +53,12 @@ export class SnapGrid {
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Return "snapped" position of set of dragged nodes.
|
||||
* @param rects rects of dragged nodes
|
||||
* @param threshold a maximum distance from node's edge to the snap axe to have this node
|
||||
* snapped.
|
||||
*/
|
||||
snappedMany(rects: Rect[], threshold: number): Vec2 {
|
||||
const minSnap = rects.reduce<PartialVec2>(
|
||||
(minSnap, rect) => {
|
||||
@ -54,6 +70,13 @@ export class SnapGrid {
|
||||
return new Vec2(minSnap.x ?? 0.0, minSnap.y ?? 0.0)
|
||||
}
|
||||
|
||||
/**
|
||||
* Return "snapped" position of node with given rects.
|
||||
* @param rect rect of dragged node
|
||||
* @param threshold a maximum distance from node's edge to the snap axe to have this node
|
||||
* snapped.
|
||||
* @returns partial vector: the coordinate is missing if the node would not snap that direction.
|
||||
*/
|
||||
snap(rect: Rect, threshold: number): PartialVec2 {
|
||||
const leftSnap = SnapGrid.boundSnap(rect.left, this.leftAxes.value, threshold)
|
||||
const rightSnap = SnapGrid.boundSnap(rect.right, this.rightAxes.value, threshold)
|
||||
@ -111,6 +134,7 @@ export function useDragging() {
|
||||
grid: SnapGrid
|
||||
stopPositionUpdate: WatchStopHandle
|
||||
|
||||
/** Start dragging: initialize all properties and animations above */
|
||||
constructor(movedId: NodeId) {
|
||||
markRaw(this)
|
||||
function* draggedNodes(): Generator<[NodeId, DraggedNode]> {
|
||||
@ -131,6 +155,7 @@ export function useDragging() {
|
||||
this.stopPositionUpdate = watchEffect(() => this.updateNodesPosition())
|
||||
}
|
||||
|
||||
/** Update drag offset and snap animation target */
|
||||
updateOffset(newOffset: Vec2): void {
|
||||
const oldSnappedOffset = snappedOffset.value
|
||||
const rects: Rect[] = []
|
||||
@ -154,10 +179,13 @@ export function useDragging() {
|
||||
}
|
||||
}
|
||||
|
||||
/** Finish dragging and set nodes' positions */
|
||||
finishDragging(): void {
|
||||
this.stopPositionUpdate()
|
||||
this.updateNodesPosition()
|
||||
}
|
||||
|
||||
/** Cancel drag and reset nodes' positions */
|
||||
cancelDragging(): void {
|
||||
console.log('cancelDragging')
|
||||
this.stopPositionUpdate()
|
||||
@ -169,7 +197,7 @@ export function useDragging() {
|
||||
this.updateNodesPosition()
|
||||
}
|
||||
|
||||
createSnapGrid() {
|
||||
private createSnapGrid() {
|
||||
const nonDraggedRects = computed(() => {
|
||||
const nonDraggedNodes = iteratorFilter(
|
||||
graphStore.db.nodeIds(),
|
||||
@ -180,7 +208,7 @@ export function useDragging() {
|
||||
return new SnapGrid(nonDraggedRects)
|
||||
}
|
||||
|
||||
updateNodesPosition() {
|
||||
private updateNodesPosition() {
|
||||
graphStore.batchEdits(() => {
|
||||
for (const [id, dragged] of this.draggedNodes) {
|
||||
const node = graphStore.db.nodeIdToNode.get(id)
|
||||
|
@ -1,8 +1,10 @@
|
||||
import type { Pattern } from '@/util/ast/match'
|
||||
|
||||
interface AllNodeCreationOptions {
|
||||
/** If false, the Component Browser will be opened to edit the node.
|
||||
* If true, the node will be created without further interaction. */
|
||||
/**
|
||||
* If false, the Component Browser will be opened to edit the node.
|
||||
* If true, the node will be created without further interaction.
|
||||
*/
|
||||
commit: boolean
|
||||
/** The content of the node. If unspecified, it will be determined based on the source node. */
|
||||
content?: Pattern | undefined
|
||||
|
@ -2,6 +2,10 @@ import { useEvent } from '@/composables/events'
|
||||
import { type ProjectStore } from '@/stores/project'
|
||||
import { useToast } from '@/util/toast'
|
||||
|
||||
/**
|
||||
* A composable which sets up several toasts for project management, and creates one for message
|
||||
* about user's action error.
|
||||
*/
|
||||
export function useGraphEditorToasts(projectStore: ProjectStore) {
|
||||
const toastStartup = useToast.info({ autoClose: false })
|
||||
const toastConnectionLost = useToast.error({ autoClose: false })
|
||||
|
@ -15,6 +15,7 @@ import { Err, Ok, withContext, type Result } from 'ydoc-shared/util/data/result'
|
||||
|
||||
const DATA_DIR_NAME = 'data'
|
||||
|
||||
/** @returns the expression for node reading an uploaded file. */
|
||||
export function uploadedExpression(result: UploadResult) {
|
||||
switch (result.source) {
|
||||
case 'Project': {
|
||||
@ -28,11 +29,20 @@ export function uploadedExpression(result: UploadResult) {
|
||||
|
||||
// === Uploader ===
|
||||
|
||||
/** Upload result, containing information about upload destination. */
|
||||
export interface UploadResult {
|
||||
source: 'FileSystemRoot' | 'Project'
|
||||
name: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Uploader handles the uploading process of a single file to project directory.
|
||||
*
|
||||
* This will upload file chunks using binary protocol, updating information of progress in
|
||||
* {@link Awareness} object. On error, the file will be deleted.
|
||||
*
|
||||
* Checking the checksum is not implemented yet because of https://github.com/enso-org/enso/issues/6691
|
||||
*/
|
||||
export class Uploader {
|
||||
private checksum: Hash<Keccak>
|
||||
private uploadedBytes: bigint
|
||||
@ -54,6 +64,7 @@ export class Uploader {
|
||||
this.stackItem = markRaw(toRaw(stackItem))
|
||||
}
|
||||
|
||||
/** Constructor */
|
||||
static Create(
|
||||
rpc: LanguageServer,
|
||||
binary: DataServer,
|
||||
@ -78,6 +89,7 @@ export class Uploader {
|
||||
)
|
||||
}
|
||||
|
||||
/** Start the upload process */
|
||||
async upload(): Promise<Result<UploadResult>> {
|
||||
// This non-standard property is defined in Electron.
|
||||
if (
|
||||
@ -195,9 +207,7 @@ export class Uploader {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Split filename into stem and (optional) extension.
|
||||
*/
|
||||
/** Split filename into stem and (optional) extension. */
|
||||
function splitFilename(fileName: string): { stem: string; extension?: string } {
|
||||
const dotIndex = fileName.lastIndexOf('.')
|
||||
if (dotIndex !== -1 && dotIndex !== 0) {
|
||||
|
@ -52,7 +52,8 @@ export const FunctionName: unique symbol = Symbol.for('WidgetInput:FunctionName'
|
||||
declare module '@/providers/widgetRegistry' {
|
||||
export interface WidgetInput {
|
||||
[FunctionName]?: {
|
||||
/** Id of expression which is accepted by Language Server's
|
||||
/**
|
||||
* Id of expression which is accepted by Language Server's
|
||||
* [`refactoring/renameSymbol` method](https://github.com/enso-org/enso/blob/develop/docs/language-server/protocol-language-server.md#refactoringrenamesymbol)
|
||||
*/
|
||||
editableName: ExpressionId
|
||||
|
@ -2,14 +2,16 @@
|
||||
import type { IHeaderParams } from 'ag-grid-community'
|
||||
import { ref, watch } from 'vue'
|
||||
|
||||
/** Parameters recognized by this header component.
|
||||
/**
|
||||
* Parameters recognized by this header component.
|
||||
*
|
||||
* They are set through `headerComponentParams` option in AGGrid column definition.
|
||||
*/
|
||||
export interface HeaderParams {
|
||||
/** Setter called when column name is changed by the user. */
|
||||
nameSetter?: (newName: string) => void
|
||||
/** Column is virtual if it is not represented in the AST. Such column might be used
|
||||
/**
|
||||
* Column is virtual if it is not represented in the AST. Such column might be used
|
||||
* to create new one.
|
||||
*/
|
||||
virtualColumn?: boolean
|
||||
|
@ -23,14 +23,18 @@ export type RowData = {
|
||||
cells: Record<Ast.AstId, Ast.AstId>
|
||||
}
|
||||
|
||||
/** A more specialized version of AGGrid's `MenuItemDef` to simplify testing (the tests need to provide
|
||||
* only values actually used by the composable) */
|
||||
/**
|
||||
* A more specialized version of AGGrid's `MenuItemDef` to simplify testing (the tests need to provide
|
||||
* only values actually used by the composable)
|
||||
*/
|
||||
export interface MenuItem extends MenuItemDef<RowData> {
|
||||
action: (params: { node: { data: RowData | undefined } | null }) => void
|
||||
}
|
||||
|
||||
/** A more specialized version of AGGrid's `ColDef` to simplify testing (the tests need to provide
|
||||
* only values actually used by the composable) */
|
||||
/**
|
||||
* A more specialized version of AGGrid's `ColDef` to simplify testing (the tests need to provide
|
||||
* only values actually used by the composable)
|
||||
*/
|
||||
export interface ColumnDef extends ColDef<RowData> {
|
||||
valueGetter: ({ data }: { data: RowData | undefined }) => any
|
||||
valueSetter?: ({ data, newValue }: { data: RowData; newValue: any }) => boolean
|
||||
@ -39,6 +43,7 @@ export interface ColumnDef extends ColDef<RowData> {
|
||||
}
|
||||
|
||||
namespace cellValueConversion {
|
||||
/** TODO: Add docs */
|
||||
export function astToAgGrid(ast: Ast.Ast) {
|
||||
if (ast instanceof Ast.TextLiteral) return Ok(ast.rawTextContent)
|
||||
else if (ast instanceof Ast.Ident && ast.code() === NOTHING_NAME) return Ok(null)
|
||||
@ -50,6 +55,7 @@ namespace cellValueConversion {
|
||||
}
|
||||
}
|
||||
|
||||
/** TODO: Add docs */
|
||||
export function agGridToAst(
|
||||
value: unknown,
|
||||
module: Ast.MutableModule,
|
||||
@ -104,6 +110,11 @@ function retrieveColumnsDefinitions(columnsAst: Ast.Vector) {
|
||||
return transposeResult(Array.from(columnsAst.values(), readColumn))
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if given ast is a `Table.new` call which may be handled by the TableEditorWidget.
|
||||
*
|
||||
* This widget may handle table definitions filled with literals or `Nothing` values.
|
||||
*/
|
||||
export function tableNewCallMayBeHandled(call: Ast.Ast) {
|
||||
const columnsAst = retrieveColumnsAst(call)
|
||||
if (!columnsAst.ok) return false
|
||||
@ -121,7 +132,6 @@ export function tableNewCallMayBeHandled(call: Ast.Ast) {
|
||||
/**
|
||||
* A composable responsible for interpreting `Table.new` expressions, creating AGGrid column
|
||||
* definitions allowing also editing AST through AGGrid editing.
|
||||
*
|
||||
* @param input the widget's input
|
||||
* @param graph the graph store
|
||||
* @param onUpdate callback called when AGGrid was edited by user, resulting in AST change.
|
||||
|
@ -39,18 +39,22 @@ export type SerializedImageNode = Spread<
|
||||
SerializedLexicalNode
|
||||
>
|
||||
|
||||
/** TODO: Add docs */
|
||||
export class ImageNode extends DecoratorNode<Component> {
|
||||
__src: string
|
||||
__altText: string
|
||||
|
||||
/** TODO: Add docs */
|
||||
static override getType(): string {
|
||||
return 'image'
|
||||
}
|
||||
|
||||
/** TODO: Add docs */
|
||||
static override clone(node: ImageNode): ImageNode {
|
||||
return new ImageNode(node.__src, node.__altText, node.__key)
|
||||
}
|
||||
|
||||
/** TODO: Add docs */
|
||||
static override importJSON(serializedNode: SerializedImageNode): ImageNode {
|
||||
const { altText, src } = serializedNode
|
||||
return $createImageNode({
|
||||
@ -59,6 +63,7 @@ export class ImageNode extends DecoratorNode<Component> {
|
||||
})
|
||||
}
|
||||
|
||||
/** TODO: Add docs */
|
||||
static override importDOM(): DOMConversionMap | null {
|
||||
return {
|
||||
img: (_node: Node) => ({
|
||||
@ -68,12 +73,14 @@ export class ImageNode extends DecoratorNode<Component> {
|
||||
}
|
||||
}
|
||||
|
||||
/** TODO: Add docs */
|
||||
constructor(src: string, altText: string, key?: NodeKey) {
|
||||
super(key)
|
||||
this.__src = src
|
||||
this.__altText = altText
|
||||
}
|
||||
|
||||
/** TODO: Add docs */
|
||||
override exportDOM(): DOMExportOutput {
|
||||
const element = document.createElement('img')
|
||||
element.setAttribute('src', this.__src)
|
||||
@ -81,6 +88,7 @@ export class ImageNode extends DecoratorNode<Component> {
|
||||
return { element }
|
||||
}
|
||||
|
||||
/** TODO: Add docs */
|
||||
override exportJSON(): SerializedImageNode {
|
||||
return {
|
||||
altText: this.getAltText(),
|
||||
@ -90,19 +98,23 @@ export class ImageNode extends DecoratorNode<Component> {
|
||||
}
|
||||
}
|
||||
|
||||
/** TODO: Add docs */
|
||||
getSrc(): string {
|
||||
return this.__src
|
||||
}
|
||||
|
||||
/** TODO: Add docs */
|
||||
getAltText(): string {
|
||||
return this.__altText
|
||||
}
|
||||
|
||||
/** TODO: Add docs */
|
||||
setAltText(altText: string): void {
|
||||
const writable = this.getWritable()
|
||||
writable.__altText = altText
|
||||
}
|
||||
|
||||
/** TODO: Add docs */
|
||||
update(payload: UpdateImagePayload): void {
|
||||
const writable = this.getWritable()
|
||||
const { altText } = payload
|
||||
@ -113,6 +125,7 @@ export class ImageNode extends DecoratorNode<Component> {
|
||||
|
||||
// View
|
||||
|
||||
/** TODO: Add docs */
|
||||
override createDOM(config: EditorConfig): HTMLElement {
|
||||
const span = document.createElement('span')
|
||||
const className = config.theme.image
|
||||
@ -122,6 +135,7 @@ export class ImageNode extends DecoratorNode<Component> {
|
||||
return span
|
||||
}
|
||||
|
||||
/** TODO: Add docs */
|
||||
override updateDOM(_prevNode: ImageNode, dom: HTMLElement, config: EditorConfig): false {
|
||||
const className = config.theme.image
|
||||
if (className !== undefined) {
|
||||
@ -130,6 +144,7 @@ export class ImageNode extends DecoratorNode<Component> {
|
||||
return false
|
||||
}
|
||||
|
||||
/** TODO: Add docs */
|
||||
override decorate(): Component {
|
||||
return h(LexicalImage, {
|
||||
src: this.__src,
|
||||
@ -138,10 +153,12 @@ export class ImageNode extends DecoratorNode<Component> {
|
||||
}
|
||||
}
|
||||
|
||||
/** TODO: Add docs */
|
||||
export function $createImageNode({ altText, src, key }: ImagePayload): ImageNode {
|
||||
return $applyNodeReplacement(new ImageNode(src, altText, key))
|
||||
}
|
||||
|
||||
/** TODO: Add docs */
|
||||
export function $isImageNode(node: LexicalNode | null | undefined): node is ImageNode {
|
||||
return node instanceof ImageNode
|
||||
}
|
||||
|
@ -34,6 +34,7 @@ import {
|
||||
} from 'lexical'
|
||||
import { ref } from 'vue'
|
||||
|
||||
/** TODO: Add docs */
|
||||
export function useFormatting(editor: LexicalEditor) {
|
||||
const selectionReaders = new Array<(selection: RangeSelection) => void>()
|
||||
function onReadSelection(reader: (selection: RangeSelection) => void) {
|
||||
@ -162,6 +163,7 @@ function isBlockType(value: string): value is BlockType {
|
||||
return value in blockTypeToBlockName
|
||||
}
|
||||
|
||||
/** TODO: Add docs */
|
||||
export function normalizeHeadingLevel(heading: HeadingTagType): HeadingTagType & BlockType {
|
||||
return isBlockType(heading) ? heading : smallestEnabledHeading
|
||||
}
|
||||
@ -237,6 +239,7 @@ function useBlockType(
|
||||
}
|
||||
}
|
||||
|
||||
/** TODO: Add docs */
|
||||
export function lexicalRichTextTheme(themeCss: Record<string, string>): EditorThemeClasses {
|
||||
const theme = lexicalTheme(themeCss)
|
||||
if (theme.heading) {
|
||||
|
@ -26,6 +26,7 @@ export interface ResourceInfo<T> {
|
||||
export type ResourceLocator<T> = (url: Url) => Promise<Result<ResourceInfo<T>> | undefined>
|
||||
export type ResourceFetcher<T> = (locator: T) => Promise<Result<Blob>>
|
||||
|
||||
/** TODO: Add docs */
|
||||
export function fetcherUrlTransformer<ResourceLocation>(
|
||||
locateResource: ResourceLocator<ResourceLocation>,
|
||||
fetchResource: ResourceFetcher<ResourceLocation>,
|
||||
|
@ -19,6 +19,7 @@ export interface LexicalMarkdownPlugin extends LexicalPlugin {
|
||||
transformers?: Transformer[]
|
||||
}
|
||||
|
||||
/** TODO: Add docs */
|
||||
export function markdownPlugin(
|
||||
model: Ref<string>,
|
||||
extensions: LexicalMarkdownPlugin[],
|
||||
|
@ -36,6 +36,7 @@ type MatchedLink = {
|
||||
|
||||
export type LinkMatcher = (text: string) => MatchedLink | null
|
||||
|
||||
/** TODO: Add docs */
|
||||
export function createLinkMatcherWithRegExp(
|
||||
regExp: RegExp,
|
||||
urlTransformer: (text: string) => string = (text) => text,
|
||||
@ -403,6 +404,7 @@ function getTextNodesToMatch(textNode: TextNode): TextNode[] {
|
||||
return textNodesToMatch
|
||||
}
|
||||
|
||||
/** TODO: Add docs */
|
||||
export function useAutoLink(
|
||||
editor: LexicalEditor,
|
||||
matchers: Array<LinkMatcher>,
|
||||
|
@ -72,6 +72,7 @@ const LINK: Transformer = {
|
||||
type: 'text-match',
|
||||
}
|
||||
|
||||
/** TODO: Add docs */
|
||||
export function $getSelectedLinkNode() {
|
||||
const selection = $getSelection()
|
||||
if (selection?.isCollapsed) {
|
||||
@ -136,6 +137,7 @@ export const autoLinkPlugin: LexicalPlugin = {
|
||||
},
|
||||
}
|
||||
|
||||
/** TODO: Add docs */
|
||||
export function useLinkNode(editor: LexicalEditor) {
|
||||
const urlUnderCursor = shallowRef<string>()
|
||||
editor.registerCommand(
|
||||
|
@ -17,6 +17,7 @@ export interface LexicalPlugin {
|
||||
register: (editor: LexicalEditor) => void
|
||||
}
|
||||
|
||||
/** TODO: Add docs */
|
||||
export function lexicalTheme(theme: Record<string, string>): EditorThemeClasses {
|
||||
interface EditorThemeShape extends Record<string, EditorThemeShape | string> {}
|
||||
const editorClasses: EditorThemeShape = {}
|
||||
@ -41,6 +42,7 @@ export function lexicalTheme(theme: Record<string, string>): EditorThemeClasses
|
||||
return editorClasses
|
||||
}
|
||||
|
||||
/** TODO: Add docs */
|
||||
export function useLexical(
|
||||
contentElement: Ref<MaybeElement>,
|
||||
namespace: string,
|
||||
|
@ -5,7 +5,8 @@ import { computed, shallowRef, toValue } from 'vue'
|
||||
|
||||
const SYNC_TAG = 'ENSO_SYNC'
|
||||
|
||||
/** Enables two-way synchronization between the editor and a string model `content`.
|
||||
/**
|
||||
* Enables two-way synchronization between the editor and a string model `content`.
|
||||
*
|
||||
* By default, the editor's text contents are synchronized with the string. A content getter and setter may be provided
|
||||
* to synchronize a different view of the state, e.g. to transform to an encoding that keeps rich text information.
|
||||
@ -20,6 +21,7 @@ export function useLexicalStringSync(
|
||||
})
|
||||
}
|
||||
|
||||
/** TODO: Add docs */
|
||||
export function useLexicalSync<T>(
|
||||
editor: LexicalEditor,
|
||||
$read: () => T,
|
||||
@ -49,10 +51,12 @@ export function useLexicalSync<T>(
|
||||
}
|
||||
}
|
||||
|
||||
/** TODO: Add docs */
|
||||
export function $getRootText() {
|
||||
return $getRoot().getTextContent()
|
||||
}
|
||||
|
||||
/** TODO: Add docs */
|
||||
export function $setRootText(text: string) {
|
||||
const root = $getRoot()
|
||||
root.clear()
|
||||
|
@ -114,7 +114,8 @@ function lockColumnSize(e: ColumnResizedEvent) {
|
||||
}
|
||||
}
|
||||
|
||||
/** Copy the provided TSV-formatted table data to the clipboard.
|
||||
/**
|
||||
* Copy the provided TSV-formatted table data to the clipboard.
|
||||
*
|
||||
* The data will be copied as `text/plain` TSV data for spreadsheet applications, and an Enso-specific MIME section for
|
||||
* pasting as a new table node.
|
||||
|
@ -96,8 +96,10 @@ const props = defineProps<{ data: Data }>()
|
||||
|
||||
/** GeoMap Visualization. */
|
||||
|
||||
/** Mapbox API access token.
|
||||
* All the limits of API are listed here: https://docs.mapbox.com/api/#rate-limits */
|
||||
/**
|
||||
* Mapbox API access token.
|
||||
* All the limits of API are listed here: https://docs.mapbox.com/api/#rate-limits
|
||||
*/
|
||||
const TOKEN = import.meta.env.VITE_ENSO_MAPBOX_API_TOKEN
|
||||
if (TOKEN == null) {
|
||||
console.warn(
|
||||
@ -371,10 +373,7 @@ function extractVisualizationDataFromDataFrame(parsedData: DataFrame) {
|
||||
/**
|
||||
* Extracts the data form the given `parsedData`. Checks the type of input data and prepares our
|
||||
* internal data (`GeoPoints') for consumption in deck.gl.
|
||||
*
|
||||
* @param parsedData - All the parsed data to create points from.
|
||||
* @param preparedDataPoints - List holding data points to push the GeoPoints into.
|
||||
* @param ACCENT_COLOR - accent color of IDE if element doesn't specify one.
|
||||
* @param parsedData - All the parsed data to create points from.
|
||||
*/
|
||||
function extractDataPoints(parsedData: Data) {
|
||||
if ('df_latitude' in parsedData && 'df_longitude' in parsedData) {
|
||||
|
@ -368,8 +368,10 @@ function zoomed(event: d3.D3ZoomEvent<Element, unknown>) {
|
||||
}
|
||||
}
|
||||
|
||||
/** Return the zoom value computed from the initial right-mouse-button event to the current
|
||||
* right-mouse event. */
|
||||
/**
|
||||
* Return the zoom value computed from the initial right-mouse-button event to the current
|
||||
* right-mouse event.
|
||||
*/
|
||||
function rmbZoomValue(event: MouseEvent | WheelEvent | undefined) {
|
||||
const dX = (event?.clientX ?? 0) - startClientX
|
||||
const dY = (event?.clientY ?? 0) - startClientY
|
||||
@ -521,10 +523,12 @@ function endBrushing() {
|
||||
d3Brush.value.call(brush.value.move, null)
|
||||
}
|
||||
|
||||
/** Zoom into the selected area of the plot.
|
||||
/**
|
||||
* Zoom into the selected area of the plot.
|
||||
*
|
||||
* Based on https://www.d3-graph-gallery.com/graph/interactivity_brush.html
|
||||
* Section "Brushing for zooming". */
|
||||
* Section "Brushing for zooming".
|
||||
*/
|
||||
function zoomToSelected(override?: boolean) {
|
||||
const shouldZoomToSelected = override ?? isBrushing.value
|
||||
if (!shouldZoomToSelected) {
|
||||
|
@ -79,8 +79,10 @@ function convertColorToRgba(color: RGBA) {
|
||||
return 'rgba(' + r + ',' + g + ',' + b + ',' + a + ')'
|
||||
}
|
||||
|
||||
/** Replace the alpha component of a color (represented as a 4-element array),
|
||||
* returning a new color. */
|
||||
/**
|
||||
* Replace the alpha component of a color (represented as a 4-element array),
|
||||
* returning a new color.
|
||||
*/
|
||||
function replaceAlpha(color: RGBA, newAlpha: number) {
|
||||
return {
|
||||
red: color.red,
|
||||
|
@ -781,10 +781,12 @@ useEvent(document, 'auxclick', endBrushing)
|
||||
useEvent(document, 'contextmenu', endBrushing)
|
||||
useEvent(document, 'scroll', endBrushing)
|
||||
|
||||
/** Zoom into the selected area of the plot.
|
||||
/**
|
||||
* Zoom into the selected area of the plot.
|
||||
*
|
||||
* Based on https://www.d3-graph-gallery.com/graph/interactivity_brush.html
|
||||
* Section "Brushing for zooming". */
|
||||
* Section "Brushing for zooming".
|
||||
*/
|
||||
function zoomToSelected(override?: boolean) {
|
||||
shouldAnimate.value = true
|
||||
focus.value = undefined
|
||||
|
@ -183,6 +183,7 @@ function createFormatMenu({ textFormatterSelected }: FormatMenuOptions): Toolbar
|
||||
}
|
||||
}
|
||||
|
||||
/** TODO: Add docs */
|
||||
export function useTableVizToolbar(options: Options): ComputedRef<ToolbarItem[]> {
|
||||
const createNodesButton = useSortFilterNodesButton(options)
|
||||
const formatMenu = createFormatMenu(options)
|
||||
|
@ -32,14 +32,17 @@ export interface SelectionMenu {
|
||||
|
||||
export type ToolbarItem = ActionButton | ToggleButton | SelectionMenu
|
||||
|
||||
/** {@link ActionButton} discriminant */
|
||||
export function isActionButton(item: Readonly<ToolbarItem>): item is ActionButton {
|
||||
return 'onClick' in item
|
||||
}
|
||||
|
||||
/** {@link ToggleButton} discriminant */
|
||||
export function isToggleButton(item: Readonly<ToolbarItem>): item is ToggleButton {
|
||||
return 'toggle' in item
|
||||
}
|
||||
|
||||
/** {@link SelectionMenu} discriminant */
|
||||
export function isSelectionMenu(item: Readonly<ToolbarItem>): item is SelectionMenu {
|
||||
return 'selected' in item
|
||||
}
|
||||
|
@ -14,19 +14,26 @@ const props = defineProps<{
|
||||
modelValue: T[]
|
||||
newItem: () => T | undefined
|
||||
getKey?: (item: T) => string | number | undefined
|
||||
/** If present, a {@link DataTransferItem} is added with a MIME type of `text/plain`.
|
||||
/**
|
||||
* If present, a {@link DataTransferItem} is added with a MIME type of `text/plain`.
|
||||
* This is useful if the drag payload has a representation that can be pasted in terminals,
|
||||
* search bars, and/or address bars. */
|
||||
* search bars, and/or address bars.
|
||||
*/
|
||||
toPlainText?: (item: T) => string
|
||||
/** The MIME type for the payload output added by `toDragPayload`.
|
||||
/**
|
||||
* The MIME type for the payload output added by `toDragPayload`.
|
||||
* Unused if `toDragPayload` is not also present.
|
||||
* When in doubt, this should be `application/json`.
|
||||
* Defaults to `application/octet-stream`, meaning the payload is arbitrary binary data. */
|
||||
* Defaults to `application/octet-stream`, meaning the payload is arbitrary binary data.
|
||||
*/
|
||||
dragMimeType?: string
|
||||
/** Convert the list item to a drag payload stored under `dragMimeType`. When in doubt, this
|
||||
* should be `JSON.stringify` of data describing the object. */
|
||||
/**
|
||||
* Convert the list item to a drag payload stored under `dragMimeType`. When in doubt, this
|
||||
* should be `JSON.stringify` of data describing the object.
|
||||
*/
|
||||
toDragPayload: (item: T) => string
|
||||
/** Convert payload created by `toDragPayload` back to the list item. This function can be called
|
||||
/**
|
||||
* Convert payload created by `toDragPayload` back to the list item. This function can be called
|
||||
* on the payload received from a different application instance (e.g. another browser), so it
|
||||
* should not rely on any local state.
|
||||
*/
|
||||
|
@ -22,7 +22,6 @@ const animTime = ref(0)
|
||||
* while the `active` watch source returns true value.
|
||||
*
|
||||
* For performing simple easing animations, see [`useApproach`].
|
||||
*
|
||||
* @param active As long as it returns true value, the `fn` callback will be called every frame.
|
||||
* @param fn The callback to call every animation frame.
|
||||
* @param priority When multiple callbacks are registered, the one with the lowest priority number
|
||||
@ -90,7 +89,6 @@ function runRaf() {
|
||||
/**
|
||||
* Animate value over time using exponential approach.
|
||||
* http://badladns.com/stories/exp-approach
|
||||
*
|
||||
* @param to Target value to approach.
|
||||
* @param timeHorizon Time at which the approach will be at 63% of the target value. Effectively
|
||||
* represents a speed of the approach. Lower values means faster animation.
|
||||
@ -115,7 +113,6 @@ export function useApproach(to: WatchSource<number>, timeHorizon: number = 100,
|
||||
/**
|
||||
* Animate a vector value over time using exponential approach.
|
||||
* http://badladns.com/stories/exp-approach
|
||||
*
|
||||
* @param to Target vector value to approach.
|
||||
* @param timeHorizon Time at which the approach will be at 63% of the target value. Effectively
|
||||
* represents a speed of the approach. Lower values means faster animation.
|
||||
@ -159,6 +156,7 @@ function useApproachBase<T>(
|
||||
return readonly(proxyRefs({ value: current, skip }))
|
||||
}
|
||||
|
||||
/** TODO: Add docs */
|
||||
export function useTransitioning(observedProperties?: Set<string>) {
|
||||
const hasActiveAnimations = ref(false)
|
||||
let numActiveTransitions = 0
|
||||
|
@ -3,6 +3,7 @@ import { type ToValue } from '@/util/reactivity'
|
||||
import { computed, toValue } from 'vue'
|
||||
import type { Ast } from 'ydoc-shared/ast'
|
||||
|
||||
/** A composable for reactively retrieving and setting documentation from given Ast node. */
|
||||
export function useAstDocumentation(graphStore: GraphStore, ast: ToValue<Ast | undefined>) {
|
||||
return {
|
||||
documentation: {
|
||||
|
@ -37,6 +37,7 @@ function backendQueryOptions<Method extends BackendMethods>(
|
||||
}
|
||||
}
|
||||
|
||||
/** TODO: Add docs */
|
||||
export function useBackendQuery<Method extends BackendMethods>(
|
||||
method: Method,
|
||||
args: ToValue<Parameters<Backend[Method]> | undefined>,
|
||||
@ -45,6 +46,7 @@ export function useBackendQuery<Method extends BackendMethods>(
|
||||
return useQuery(backendQueryOptions(method, args, backend))
|
||||
}
|
||||
|
||||
/** TODO: Add docs */
|
||||
export function useBackendQueryPrefetching() {
|
||||
const queryClient = useQueryClient()
|
||||
const { backend } = injectBackend()
|
||||
|
@ -3,6 +3,7 @@ import { Rect } from '@/util/data/rect'
|
||||
import type { MaybeElement } from '@vueuse/core'
|
||||
import { shallowRef, watch, type Ref } from 'vue'
|
||||
|
||||
/** TODO: Add docs */
|
||||
export function useSelectionBounds(boundingElement: Ref<MaybeElement>, includeCollapsed = false) {
|
||||
const bounds = shallowRef<Rect>()
|
||||
const collapsed = shallowRef<boolean>()
|
||||
|
@ -1,9 +1,13 @@
|
||||
/** @file A Vue composable that calls one of two given callbacks, depending on whether a click is
|
||||
* a single click or a double click. */
|
||||
/**
|
||||
* @file A Vue composable that calls one of two given callbacks, depending on whether a click is
|
||||
* a single click or a double click.
|
||||
*/
|
||||
|
||||
/** Calls {@link onClick} if a click is a single click, or {@link onDoubleClick} if a click is
|
||||
/**
|
||||
* Calls {@link onClick} if a click is a single click, or {@link onDoubleClick} if a click is
|
||||
* a double click. For this function, a double click is defined as a second click that occurs within
|
||||
* 200ms of the first click. The click count is reset to 0 upon double click, or after 200ms. */
|
||||
* 200ms of the first click. The click count is reset to 0 upon double click, or after 200ms.
|
||||
*/
|
||||
export function useDoubleClick<Args extends any[]>(
|
||||
onClick: (...args: Args) => void,
|
||||
onDoubleClick: (...args: Args) => void,
|
||||
|
@ -19,17 +19,12 @@ import {
|
||||
} from 'vue'
|
||||
import { useRaf } from './animation'
|
||||
|
||||
/** TODO: Add docs */
|
||||
export function isTriggeredByKeyboard(e: MouseEvent | PointerEvent) {
|
||||
if (e instanceof PointerEvent) return e.pointerType !== 'mouse'
|
||||
else return false
|
||||
}
|
||||
|
||||
/**
|
||||
* Add an event listener for the duration of the component's lifetime.
|
||||
* @param target element on which to register the event
|
||||
* @param event name of event to register
|
||||
* @param handler event handler
|
||||
*/
|
||||
export function useEvent<K extends keyof DocumentEventMap>(
|
||||
target: Document,
|
||||
event: K,
|
||||
@ -54,6 +49,12 @@ export function useEvent(
|
||||
handler: EventListenerOrEventListenerObject,
|
||||
options?: boolean | AddEventListenerOptions,
|
||||
): void
|
||||
/**
|
||||
* Add an event listener for the duration of the component's lifetime.
|
||||
* @param target element on which to register the event
|
||||
* @param event name of event to register
|
||||
* @param handler event handler
|
||||
*/
|
||||
export function useEvent(
|
||||
target: EventTarget,
|
||||
event: string,
|
||||
@ -64,13 +65,6 @@ export function useEvent(
|
||||
onScopeDispose(() => target.removeEventListener(event, handler, options))
|
||||
}
|
||||
|
||||
/**
|
||||
* Add an event listener for the duration of condition being true.
|
||||
* @param target element on which to register the event
|
||||
* @param condition the condition that determines if event is bound
|
||||
* @param event name of event to register
|
||||
* @param handler event handler
|
||||
*/
|
||||
export function useEventConditional<K extends keyof DocumentEventMap>(
|
||||
target: Document,
|
||||
event: K,
|
||||
@ -99,6 +93,14 @@ export function useEventConditional(
|
||||
handler: (event: unknown) => void,
|
||||
options?: boolean | AddEventListenerOptions,
|
||||
): void
|
||||
/**
|
||||
* Add an event listener for the duration of condition being true.
|
||||
* @param target element on which to register the event
|
||||
* @param event name of event to register
|
||||
* @param condition the condition that determines if event is bound
|
||||
* @param handler event handler
|
||||
* @param options listener options
|
||||
*/
|
||||
export function useEventConditional(
|
||||
target: EventTarget,
|
||||
event: string,
|
||||
@ -148,11 +150,13 @@ const hasWindow = typeof window !== 'undefined'
|
||||
const platform = hasWindow ? window.navigator?.platform ?? '' : ''
|
||||
export const isMacLike = /(Mac|iPhone|iPod|iPad)/i.test(platform)
|
||||
|
||||
/** Check if `mod` key (ctrl or cmd) appropriate for current platform is used */
|
||||
export function modKey(e: KeyboardEvent | MouseEvent): boolean {
|
||||
return isMacLike ? e.metaKey : e.ctrlKey
|
||||
}
|
||||
|
||||
/** A helper for getting Element out of VueInstance, it allows using `useResizeObserver` with Vue components.
|
||||
/**
|
||||
* A helper for getting Element out of VueInstance, it allows using `useResizeObserver` with Vue components.
|
||||
*
|
||||
* Note that this function is only shallowly reactive: It will trigger its reactive scope if the value of `element`
|
||||
* changes, but not if the root `Element` of the provided `VueInstance` changes. This is because a
|
||||
@ -230,7 +234,6 @@ const sharedResizeObserver: ResizeObserver | undefined =
|
||||
* # Warning:
|
||||
* Updating DOM node layout based on values derived from their size can introduce unwanted feedback
|
||||
* loops across the script and layout reflow. Avoid doing that.
|
||||
*
|
||||
* @param elementRef DOM node to observe.
|
||||
* @returns Reactive value with the DOM node size.
|
||||
*/
|
||||
@ -321,10 +324,13 @@ export const enum PointerButtonMask {
|
||||
* Options for `usePointer` composable.
|
||||
*/
|
||||
export interface UsePointerOptions {
|
||||
/** Declare which buttons to look for. The value represents a `PointerEvent.buttons` mask.
|
||||
* Defaults to main mouse button. */
|
||||
/**
|
||||
* Declare which buttons to look for. The value represents a `PointerEvent.buttons` mask.
|
||||
* Defaults to main mouse button.
|
||||
*/
|
||||
requiredButtonMask?: number
|
||||
/** Which element should capture pointer when drag starts: event's `target`, `currentTarget`,
|
||||
/**
|
||||
* Which element should capture pointer when drag starts: event's `target`, `currentTarget`,
|
||||
* or none.
|
||||
*/
|
||||
pointerCapturedBy?: 'target' | 'currentTarget' | 'none'
|
||||
@ -334,11 +340,8 @@ export interface UsePointerOptions {
|
||||
|
||||
/**
|
||||
* Register for a pointer dragging events.
|
||||
*
|
||||
* @param handler callback on any pointer event. If `false` is returned from the callback, the
|
||||
* event will be considered _not_ handled and will propagate further.
|
||||
* @param options
|
||||
* @returns
|
||||
*/
|
||||
export function usePointer(
|
||||
handler: (pos: EventPosition, event: PointerEvent, eventType: PointerEventType) => void | boolean,
|
||||
@ -465,11 +468,8 @@ export interface UseArrowsOptions {
|
||||
* always be Vec2.Zero (and thus, the absolute and relative positions will be equal).
|
||||
*
|
||||
* The "drag" starts on first arrow keypress and ends with last arrow key release.
|
||||
*
|
||||
* @param handler callback on any event. The 'move' event is fired on every frame, and thus does
|
||||
* not have any event associated (`event` parameter will be undefined).
|
||||
* @param options
|
||||
* @returns
|
||||
*/
|
||||
export function useArrows(
|
||||
handler: (
|
||||
@ -572,7 +572,8 @@ export function useArrows(
|
||||
return { events, moving }
|
||||
}
|
||||
|
||||
/** Supports panning or zooming "capturing" wheel events.
|
||||
/**
|
||||
* Supports panning or zooming "capturing" wheel events.
|
||||
*
|
||||
* While events are captured, further events of the same type will continue the pan/zoom action.
|
||||
* The capture expires if no events are received within the specified `captureDurationMs`.
|
||||
|
@ -1,7 +1,8 @@
|
||||
import { syncRef, useFocus, type MaybeElement } from '@vueuse/core'
|
||||
import { effectScope, nextTick, ref, watch, type WatchSource } from 'vue'
|
||||
|
||||
/** Maintain bidirectional synchronization between an element's focus state and a model value, which is returned.
|
||||
/**
|
||||
* Maintain bidirectional synchronization between an element's focus state and a model value, which is returned.
|
||||
*
|
||||
* This is similar to `syncRef(model, useFocus(element).focused)`, but correctly handles
|
||||
* the `element` being updated immediately before it is made visible in the DOM by delaying
|
||||
|
@ -1,7 +1,9 @@
|
||||
import { isMacLike, useEvent } from '@/composables/events'
|
||||
import { proxyRefs, ref } from 'vue'
|
||||
|
||||
/** {@link useKeyboard} composable object */
|
||||
export type KeyboardComposable = ReturnType<typeof useKeyboard>
|
||||
/** Composable containing reactive flags for modifier's press state. */
|
||||
export function useKeyboard() {
|
||||
const state = {
|
||||
alt: ref(false),
|
||||
|
@ -31,9 +31,11 @@ const ZOOM_LEVELS = [
|
||||
]
|
||||
const DEFAULT_SCALE_RANGE: ScaleRange = [Math.min(...ZOOM_LEVELS), Math.max(...ZOOM_LEVELS)]
|
||||
const ZOOM_LEVELS_REVERSED = [...ZOOM_LEVELS].reverse()
|
||||
/** The fraction of the next zoom level.
|
||||
/**
|
||||
* The fraction of the next zoom level.
|
||||
* If we are that close to next zoom level, we should choose the next one instead
|
||||
* to avoid small unnoticeable changes to zoom. */
|
||||
* to avoid small unnoticeable changes to zoom.
|
||||
*/
|
||||
const ZOOM_SKIP_THRESHOLD = 0.05
|
||||
const WHEEL_CAPTURE_DURATION_MS = 250
|
||||
const LONGPRESS_TIMEOUT = 500
|
||||
@ -51,6 +53,7 @@ export interface NavigatorOptions {
|
||||
}
|
||||
|
||||
export type NavigatorComposable = ReturnType<typeof useNavigator>
|
||||
/** TODO: Add docs */
|
||||
export function useNavigator(
|
||||
viewportNode: Ref<HTMLElement | undefined>,
|
||||
keyboard: KeyboardComposable,
|
||||
@ -269,7 +272,8 @@ export function useNavigator(
|
||||
}
|
||||
})
|
||||
|
||||
/** As `panTo`, but also follow the points if the viewport size is changing.
|
||||
/**
|
||||
* As `panTo`, but also follow the points if the viewport size is changing.
|
||||
*
|
||||
* The following is working until manual panning by user input or until the next call to any `pan…` function.
|
||||
*/
|
||||
@ -278,7 +282,8 @@ export function useNavigator(
|
||||
panToImpl(points)
|
||||
}
|
||||
|
||||
/** Pan to include the given prioritized list of coordinates.
|
||||
/**
|
||||
* Pan to include the given prioritized list of coordinates.
|
||||
*
|
||||
* The view will be offset to include each coordinate, unless the coordinate cannot be fit in the viewport without
|
||||
* losing a previous (higher-priority) coordinate; in that case, shift the viewport as close as possible to the
|
||||
@ -353,8 +358,10 @@ export function useNavigator(
|
||||
eventMousePos.value ? clientToScenePos(eventMousePos.value) : null,
|
||||
)
|
||||
|
||||
/** Clamp the value to the given bounds, except if it is already outside the bounds allow the new value to be less
|
||||
* outside the bounds. */
|
||||
/**
|
||||
* Clamp the value to the given bounds, except if it is already outside the bounds allow the new value to be less
|
||||
* outside the bounds.
|
||||
*/
|
||||
function directedClamp(oldValue: number, newValue: number, [min, max]: ScaleRange): number {
|
||||
if (!Number.isFinite(newValue)) return oldValue
|
||||
else if (!Number.isFinite(oldValue)) return Math.max(min, Math.min(newValue, max))
|
||||
@ -368,9 +375,11 @@ export function useNavigator(
|
||||
scale.skip()
|
||||
}
|
||||
|
||||
/** Step to the next level from {@link ZOOM_LEVELS}.
|
||||
/**
|
||||
* Step to the next level from {@link ZOOM_LEVELS}.
|
||||
* @param zoomStepDelta step direction. If positive select larger zoom level; if negative select smaller.
|
||||
* If 0, resets zoom level to 1.0. */
|
||||
* If 0, resets zoom level to 1.0.
|
||||
*/
|
||||
function stepZoom(zoomStepDelta: number) {
|
||||
const oldValue = targetScale.value
|
||||
const insideThreshold = (level: number) =>
|
||||
|
@ -4,6 +4,7 @@ import { type Group } from '@/stores/suggestionDatabase'
|
||||
import { colorFromString } from '@/util/colors'
|
||||
import { computed } from 'vue'
|
||||
|
||||
/** TODO: Add docs */
|
||||
export function useNodeColors(graphStore: GraphStore, getCssValue: (variable: string) => string) {
|
||||
function getNodeColor(node: NodeId) {
|
||||
const color = graphStore.db.getNodeColorStyle(node)
|
||||
@ -34,6 +35,7 @@ export function useNodeColors(graphStore: GraphStore, getCssValue: (variable: st
|
||||
return { getNodeColor, getNodeColors }
|
||||
}
|
||||
|
||||
/** TODO: Add docs */
|
||||
export function computeNodeColor(
|
||||
getType: () => NodeType,
|
||||
getGroup: () => Group | undefined,
|
||||
@ -48,11 +50,13 @@ export function computeNodeColor(
|
||||
return 'var(--node-color-no-type)'
|
||||
}
|
||||
|
||||
/** TODO: Add docs */
|
||||
export function groupColorVar(group: Group | undefined): string {
|
||||
const name = group ? `${group.project}-${group.name}`.replace(/[^\w]/g, '-') : 'fallback'
|
||||
return `--group-color-${name}`
|
||||
}
|
||||
|
||||
/** TODO: Add docs */
|
||||
export function groupColorStyle(group: Group | undefined): string {
|
||||
return `var(${groupColorVar(group)})`
|
||||
}
|
||||
|
@ -53,6 +53,7 @@ export interface NodeCreationOptions<Placement extends PlacementStrategy = Place
|
||||
requiredImports?: RequiredImport[] | undefined
|
||||
}
|
||||
|
||||
/** TODO: Add docs */
|
||||
export function useNodeCreation(
|
||||
graphStore: GraphStore,
|
||||
viewport: ToValue<GraphNavigator['viewport']>,
|
||||
@ -240,7 +241,8 @@ function inferPrefixFromAst(expr: Ast.Ast): string | undefined {
|
||||
return undefined
|
||||
}
|
||||
|
||||
/** Convert Typename into short binding prefix.
|
||||
/**
|
||||
* Convert Typename into short binding prefix.
|
||||
* In general, we want to use the last segment of the qualified name.
|
||||
* In case of generic types, we want to discard any type parameters.
|
||||
*/
|
||||
@ -254,13 +256,16 @@ function typeToPrefix(type: Typename): string {
|
||||
}
|
||||
}
|
||||
|
||||
/** Strip number suffix from binding name, effectively returning a valid prefix.
|
||||
* The reverse of graphStore.generateLocallyUniqueIdent */
|
||||
/**
|
||||
* Strip number suffix from binding name, effectively returning a valid prefix.
|
||||
* The reverse of graphStore.generateLocallyUniqueIdent
|
||||
*/
|
||||
function existingNameToPrefix(name: string): string {
|
||||
return name.replace(/\d+$/, '')
|
||||
}
|
||||
|
||||
/** Insert the given statements into the given block, at a location appropriate for new nodes.
|
||||
/**
|
||||
* Insert the given statements into the given block, at a location appropriate for new nodes.
|
||||
*
|
||||
* The location will be after any statements in the block that bind identifiers; if the block ends in an expression
|
||||
* statement, the location will be before it so that the value of the block will not be affected.
|
||||
|
@ -20,7 +20,8 @@ interface BaseSelectionOptions<T> {
|
||||
onDeselected?: (element: T) => void
|
||||
}
|
||||
interface SelectionPackingOptions<T, PackedT> {
|
||||
/** The `pack` and `unpack` functions are used to maintain state in a transformed form.
|
||||
/**
|
||||
* The `pack` and `unpack` functions are used to maintain state in a transformed form.
|
||||
*
|
||||
* If provided, all operations that modify or query state will transparently operate on packed state. This can be
|
||||
* used to expose a selection interface based on one element type (`T`), while allowing the selection set to be
|
||||
@ -45,6 +46,7 @@ export function useSelection<T, PackedT>(
|
||||
elementRects: Map<T, Rect>,
|
||||
options: BaseSelectionOptions<T> & SelectionPackingOptions<T, PackedT>,
|
||||
): UseSelection<T, PackedT>
|
||||
/** TODO: Add docs */
|
||||
export function useSelection<T, PackedT>(
|
||||
navigator: NavigatorComposable,
|
||||
elementRects: Map<T, Rect>,
|
||||
@ -246,6 +248,7 @@ function useSelectionImpl<T, PackedT>(
|
||||
|
||||
// === Hover tracking for nodes and ports ===
|
||||
|
||||
/** TODO: Add docs */
|
||||
export function useGraphHover(isPortEnabled: (port: PortId) => boolean) {
|
||||
const hoveredElement = shallowRef<Element>()
|
||||
|
||||
|
@ -5,6 +5,7 @@ import { qnLastSegment, tryQualifiedName } from '@/util/qualifiedName'
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
import { methodPointerEquals, type StackItem } from 'ydoc-shared/languageServerTypes'
|
||||
|
||||
/** TODO: Add docs */
|
||||
export function useStackNavigator(projectStore: ProjectStore, graphStore: GraphStore) {
|
||||
const breadcrumbs = ref<StackItem[]>([])
|
||||
|
||||
|
@ -2,6 +2,7 @@ import type { ToValue } from '@/util/reactivity'
|
||||
import type { QueryKey } from '@tanstack/vue-query'
|
||||
import { computed, toValue } from 'vue'
|
||||
|
||||
/** TODO: Add docs */
|
||||
export function useQueryOptions<Parameters, Data>(
|
||||
parameters: ToValue<Parameters | undefined>,
|
||||
queryKey: (parameters: Parameters) => QueryKey,
|
||||
|
@ -51,12 +51,14 @@ window.addEventListener('resize', () => {
|
||||
|
||||
/** The entrypoint into the IDE. */
|
||||
function main() {
|
||||
/** Note: Signing out always redirects to `/`. It is impossible to make this work,
|
||||
/**
|
||||
* Note: Signing out always redirects to `/`. It is impossible to make this work,
|
||||
* as it is not possible to distinguish between having just logged out, and explicitly
|
||||
* opening a page with no URL parameters set.
|
||||
*
|
||||
* Client-side routing endpoints are explicitly not supported for live-reload, as they are
|
||||
* transitional pages that should not need live-reload when running `gui watch`. */
|
||||
* transitional pages that should not need live-reload when running `gui watch`.
|
||||
*/
|
||||
const url = new URL(location.href)
|
||||
const isInAuthenticationFlow = url.searchParams.has('code') && url.searchParams.has('state')
|
||||
const authenticationUrl = location.href
|
||||
|
@ -7,6 +7,7 @@ const { provideFn, injectFn: injectAppClassSet } = createContextStore('App Class
|
||||
return reactive(new Map<string, number>())
|
||||
})
|
||||
|
||||
/** TODO: Add docs */
|
||||
export function useAppClass(watchSource: WatchSource<Opt<Record<string, boolean>>>) {
|
||||
const classSet = injectAppClassSet(true)
|
||||
if (classSet == null) return
|
||||
|
@ -19,7 +19,6 @@ const MISSING = Symbol('MISSING')
|
||||
*
|
||||
* Under the hood, this uses Vue's [Context API], therefore it can only be used within a component's
|
||||
* setup function.
|
||||
*
|
||||
* @param name A user-friendly name for the store, used for error messages and debugging. The name
|
||||
* has no influence on the standard runtime behavior of the store, and doesn't have to be unique.
|
||||
* @param factory A factory function that creates the store. The parameters expected by the factory
|
||||
@ -36,7 +35,6 @@ export function createContextStore<F extends (...args: any[]) => any>(name: stri
|
||||
/**
|
||||
* Create the instance of a store and store it in the current component's context. All child
|
||||
* components will be able to access that store using the corresponding inject function.
|
||||
*
|
||||
* @param args The parameters that will be passed to the store factory function.
|
||||
* @returns The store instance created by the factory function.
|
||||
*/
|
||||
@ -66,7 +64,6 @@ export function createContextStore<F extends (...args: any[]) => any>(name: stri
|
||||
/**
|
||||
* Access a store instance provided by an ancestor component. When trying to access a store that
|
||||
* has never been provided, the behavior depends on the first parameter value.
|
||||
*
|
||||
* @param missingBehavior determines what happens when trying to inject a store has never been provided:
|
||||
* - If `missingBehavior` is `false` or it is not provided, an exception is thrown.
|
||||
* - If `missingBehavior` is `true`, `undefined` is returned. This is also reflected in the return
|
||||
|
@ -7,9 +7,11 @@ const { provideFn, injectFn } = createContextStore(
|
||||
() => new InteractionHandler(),
|
||||
)
|
||||
|
||||
/** TODO: Add docs */
|
||||
export class InteractionHandler {
|
||||
private currentInteraction = shallowRef<Interaction>()
|
||||
|
||||
/** TODO: Add docs */
|
||||
isActive(interaction: Interaction | undefined): interaction is Interaction {
|
||||
return interaction != null && interaction === this.currentInteraction.value
|
||||
}
|
||||
@ -25,6 +27,7 @@ export class InteractionHandler {
|
||||
})
|
||||
}
|
||||
|
||||
/** TODO: Add docs */
|
||||
setCurrent(interaction: Interaction | undefined) {
|
||||
if (!this.isActive(interaction)) {
|
||||
this.currentInteraction.value?.end?.()
|
||||
@ -32,6 +35,7 @@ export class InteractionHandler {
|
||||
}
|
||||
}
|
||||
|
||||
/** TODO: Add docs */
|
||||
getCurrent(): Interaction | undefined {
|
||||
return this.currentInteraction.value
|
||||
}
|
||||
@ -57,6 +61,7 @@ export class InteractionHandler {
|
||||
}
|
||||
}
|
||||
|
||||
/** TODO: Add docs */
|
||||
handleCancel(): boolean {
|
||||
const hasCurrent = this.currentInteraction.value != null
|
||||
this.currentInteraction.value?.cancel?.()
|
||||
@ -64,6 +69,7 @@ export class InteractionHandler {
|
||||
return hasCurrent
|
||||
}
|
||||
|
||||
/** TODO: Add docs */
|
||||
handlePointerEvent<HandlerName extends keyof Interaction>(
|
||||
event: PointerEvent,
|
||||
handlerName: Interaction[HandlerName] extends InteractionEventHandler | undefined ? HandlerName
|
||||
@ -90,8 +96,10 @@ export interface Interaction {
|
||||
end?(): void
|
||||
/** Uses a `capture` event handler to allow an interaction to respond to clicks over any element. */
|
||||
pointerdown?: InteractionEventHandler
|
||||
/** Uses a `capture` event handler to allow an interaction to respond to mouse button release
|
||||
/**
|
||||
* Uses a `capture` event handler to allow an interaction to respond to mouse button release
|
||||
* over any element. It is useful for interactions happening during mouse press (like dragging
|
||||
* edges) */
|
||||
* edges)
|
||||
*/
|
||||
pointerup?: InteractionEventHandler
|
||||
}
|
||||
|
@ -9,8 +9,10 @@ interface SelectionArrowInfo {
|
||||
id: AstId | PortId | TokenId | null
|
||||
/** Child widget can call this callback to request teleport of the arrow to specified element. */
|
||||
requestArrow: (to: RendererElement) => void
|
||||
/** Whether or not the arrow provided by this context instance was already requested.
|
||||
* Do not request the arrow twice, it will be stolen from other elements! */
|
||||
/**
|
||||
* Whether or not the arrow provided by this context instance was already requested.
|
||||
* Do not request the arrow twice, it will be stolen from other elements!
|
||||
*/
|
||||
handled: boolean
|
||||
/**
|
||||
* Child widget may set this flag to suppress arrow displaying.
|
||||
|
@ -36,6 +36,7 @@ const { provideFn, injectFn } = createContextStore(
|
||||
// The visualization config public API should not expose the `allowMissing` parameter. It should
|
||||
// look like an ordinary vue composable.
|
||||
|
||||
/** TODO: Add docs */
|
||||
export function useVisualizationConfig() {
|
||||
return injectFn()
|
||||
}
|
||||
|
@ -12,6 +12,7 @@ import type { WidgetEditHandlerParent } from './widgetRegistry/editHandler'
|
||||
export type WidgetComponent<T extends WidgetInput> = Component<WidgetProps<T>>
|
||||
|
||||
export namespace WidgetInput {
|
||||
/** TODO: Add docs */
|
||||
export function FromAst<A extends Ast.Ast | Ast.Token>(ast: A): WidgetInput & { value: A } {
|
||||
return {
|
||||
portId: ast.id,
|
||||
@ -19,6 +20,7 @@ export namespace WidgetInput {
|
||||
}
|
||||
}
|
||||
|
||||
/** TODO: Add docs */
|
||||
export function FromAstWithPort<A extends Ast.Ast | Ast.Token>(
|
||||
ast: A,
|
||||
): WidgetInput & { value: A } {
|
||||
@ -29,6 +31,7 @@ export namespace WidgetInput {
|
||||
}
|
||||
}
|
||||
|
||||
/** TODO: Add docs */
|
||||
export function valueRepr(input: WidgetInput): string | undefined {
|
||||
if (typeof input.value === 'string') return input.value
|
||||
else return input.value?.code()
|
||||
@ -53,6 +56,7 @@ export namespace WidgetInput {
|
||||
isPlaceholder(input) || input.value instanceof nodeType
|
||||
}
|
||||
|
||||
/** TODO: Add docs */
|
||||
export function isAst(input: WidgetInput): input is WidgetInput & { value: Ast.Ast } {
|
||||
return input.value instanceof Ast.Ast
|
||||
}
|
||||
@ -64,10 +68,12 @@ export namespace WidgetInput {
|
||||
return isPlaceholder(input) || isAst(input)
|
||||
}
|
||||
|
||||
/** TODO: Add docs */
|
||||
export function isToken(input: WidgetInput): input is WidgetInput & { value: Ast.Token } {
|
||||
return input.value instanceof Ast.Token
|
||||
}
|
||||
|
||||
/** TODO: Add docs */
|
||||
export function isFunctionCall(
|
||||
input: WidgetInput,
|
||||
): input is WidgetInput & { value: Ast.App | Ast.Ident | Ast.PropertyAccess | Ast.OprApp } {
|
||||
@ -198,7 +204,8 @@ type InputTy<M> =
|
||||
: never
|
||||
|
||||
export interface WidgetOptions<T extends WidgetInput> {
|
||||
/** The priority number determining the order in which the widgets are matched. Smaller numbers
|
||||
/**
|
||||
* The priority number determining the order in which the widgets are matched. Smaller numbers
|
||||
* have higher priority, and are matched first.
|
||||
*/
|
||||
priority: number
|
||||
@ -215,7 +222,8 @@ export interface WidgetOptions<T extends WidgetInput> {
|
||||
// A list of widget kinds that will be prevented from being used on the same node as this widget,
|
||||
// once this widget is used.
|
||||
prevent?: WidgetComponent<any>[]
|
||||
/** If false, this widget will be matched only when at least one another widget is guaranteed to
|
||||
/**
|
||||
* If false, this widget will be matched only when at least one another widget is guaranteed to
|
||||
* be matched for the same input. `true` by default.
|
||||
*
|
||||
* The widget marked with `false` *must* have a child with the same input,
|
||||
@ -331,6 +339,7 @@ const { provideFn, injectFn } = createContextStore(
|
||||
(db: GraphDb) => new WidgetRegistry(db),
|
||||
)
|
||||
|
||||
/** TODO: Add docs */
|
||||
export class WidgetRegistry {
|
||||
loadedModules: WidgetModule<any>[] = shallowReactive([])
|
||||
sortedModules = computed(() => {
|
||||
@ -338,8 +347,10 @@ export class WidgetRegistry {
|
||||
(a, b) => a.widgetDefinition.priority - b.widgetDefinition.priority,
|
||||
)
|
||||
})
|
||||
/** TODO: Add docs */
|
||||
constructor(private db: GraphDb) {}
|
||||
|
||||
/** TODO: Add docs */
|
||||
loadWidgets(modules: [path: string, module: unknown][]) {
|
||||
for (const [path, mod] of modules) {
|
||||
if (isWidgetModule(mod)) this.registerWidgetModule(mod)
|
||||
|
@ -1,6 +1,7 @@
|
||||
import { z } from 'zod'
|
||||
|
||||
/** Intermediate step in the parsing process, when we rename `constructor` field to `kind`.
|
||||
/**
|
||||
* Intermediate step in the parsing process, when we rename `constructor` field to `kind`.
|
||||
*
|
||||
* It helps to avoid issues with TypeScript, which considers `constructor` as a reserved keyword in many contexts.
|
||||
*/
|
||||
@ -115,22 +116,28 @@ export interface SingleChoice {
|
||||
values: Choice[]
|
||||
}
|
||||
|
||||
/** Dynamic configuration for a function call with a list of arguments with known dynamic configuration.
|
||||
* This kind of config is not provided by the engine directly, but is derived from other config types by widgets. */
|
||||
/**
|
||||
* Dynamic configuration for a function call with a list of arguments with known dynamic configuration.
|
||||
* This kind of config is not provided by the engine directly, but is derived from other config types by widgets.
|
||||
*/
|
||||
export interface FunctionCall {
|
||||
kind: 'FunctionCall'
|
||||
parameters: Map<string, (WidgetConfiguration & WithDisplay) | null>
|
||||
}
|
||||
|
||||
/** Dynamic configuration for one of the possible function calls. It is typically the case for dropdown widget.
|
||||
/**
|
||||
* Dynamic configuration for one of the possible function calls. It is typically the case for dropdown widget.
|
||||
* One of function calls will be chosen by WidgetFunction basing on the actual AST at the call site,
|
||||
* and the configuration will be used in child widgets.
|
||||
* This kind of config is not provided by the engine directly, but is derived from other config types by widgets. */
|
||||
* This kind of config is not provided by the engine directly, but is derived from other config types by widgets.
|
||||
*/
|
||||
export interface OneOfFunctionCalls {
|
||||
kind: 'OneOfFunctionCalls'
|
||||
/** A list of possible function calls and their corresponding configuration.
|
||||
/**
|
||||
* A list of possible function calls and their corresponding configuration.
|
||||
* The key is typically a fully qualified or autoscoped name of the function, but in general it can be anything,
|
||||
* depending on the widget implementation. */
|
||||
* depending on the widget implementation.
|
||||
*/
|
||||
possibleFunctions: Map<string, FunctionCall>
|
||||
}
|
||||
|
||||
|
@ -12,6 +12,7 @@ declare const brandWidgetId: unique symbol
|
||||
/** Uniquely identifies a widget type. */
|
||||
export type WidgetId = string & { [brandWidgetId]: true }
|
||||
|
||||
/** TODO: Add docs */
|
||||
export abstract class WidgetEditHandlerParent {
|
||||
private readonly activeChild: ShallowRef<WidgetEditHandlerParent | undefined> =
|
||||
shallowRef(undefined)
|
||||
@ -64,6 +65,7 @@ export abstract class WidgetEditHandlerParent {
|
||||
this.parent?.onEdit(origin, value)
|
||||
}
|
||||
|
||||
/** TODO: Add docs */
|
||||
addItem(): boolean {
|
||||
return this.hooks.addItem?.() ?? this.parent?.addItem() ?? false
|
||||
}
|
||||
@ -73,6 +75,7 @@ export abstract class WidgetEditHandlerParent {
|
||||
else return this.activeChild.value ? this.activeChild.value.pointerdown(event) : false
|
||||
}
|
||||
|
||||
/** TODO: Add docs */
|
||||
isActive() {
|
||||
return this.active.value
|
||||
}
|
||||
@ -132,7 +135,9 @@ type ResumeCallback = () => void
|
||||
type WidgetInstanceId = `${string}||${WidgetId}`
|
||||
type ResumableWidgetEdits = Map<WidgetInstanceId, ResumeCallback | undefined>
|
||||
|
||||
/** TODO: Add docs */
|
||||
export class WidgetEditHandlerRoot extends WidgetEditHandlerParent implements Interaction {
|
||||
/** TODO: Add docs */
|
||||
constructor(
|
||||
private readonly widgetTree: CurrentEdit,
|
||||
private readonly interactionHandler: InteractionHandler,
|
||||
@ -148,6 +153,7 @@ export class WidgetEditHandlerRoot extends WidgetEditHandlerParent implements In
|
||||
})
|
||||
}
|
||||
|
||||
/** TODO: Add docs */
|
||||
tryResumeRoot(widgetInstance: WidgetInstanceId) {
|
||||
const current = this.interactionHandler.getCurrent()
|
||||
if (current instanceof WidgetEditHandlerRoot) {
|
||||
@ -155,14 +161,17 @@ export class WidgetEditHandlerRoot extends WidgetEditHandlerParent implements In
|
||||
}
|
||||
}
|
||||
|
||||
/** TODO: Add docs */
|
||||
cancel() {
|
||||
this.onCancel()
|
||||
}
|
||||
|
||||
/** TODO: Add docs */
|
||||
end() {
|
||||
this.onEnd()
|
||||
}
|
||||
|
||||
/** TODO: Add docs */
|
||||
override pointerdown(event: PointerEvent) {
|
||||
return super.pointerdown(event)
|
||||
}
|
||||
@ -171,10 +180,12 @@ export class WidgetEditHandlerRoot extends WidgetEditHandlerParent implements In
|
||||
return this
|
||||
}
|
||||
|
||||
/** TODO: Add docs */
|
||||
override isActive() {
|
||||
return this.interactionHandler.isActive(this)
|
||||
}
|
||||
|
||||
/** TODO: Add docs */
|
||||
currentEdit() {
|
||||
const leaf = this.activeLeaf()
|
||||
if (leaf !== this) return leaf
|
||||
@ -210,6 +221,7 @@ export class WidgetEditHandlerRoot extends WidgetEditHandlerParent implements In
|
||||
* the top-most widget, and a widget may choose to delegate to its child (if any) by returning false.
|
||||
*/
|
||||
export class WidgetEditHandler extends WidgetEditHandlerParent {
|
||||
/** TODO: Add docs */
|
||||
constructor(
|
||||
readonly portId: PortId,
|
||||
hooks: WidgetEditHooks,
|
||||
@ -220,6 +232,7 @@ export class WidgetEditHandler extends WidgetEditHandlerParent {
|
||||
super(parent ?? new WidgetEditHandlerRoot(widgetTree, interactionHandler), hooks)
|
||||
}
|
||||
|
||||
/** TODO: Add docs */
|
||||
static New(
|
||||
widgetId: string,
|
||||
input: WidgetInput,
|
||||
@ -235,18 +248,22 @@ export class WidgetEditHandler extends WidgetEditHandlerParent {
|
||||
return editHandler
|
||||
}
|
||||
|
||||
/** TODO: Add docs */
|
||||
end() {
|
||||
this.onEnd(this.portId)
|
||||
}
|
||||
|
||||
/** TODO: Add docs */
|
||||
cancel() {
|
||||
this.root().cancel()
|
||||
}
|
||||
|
||||
/** TODO: Add docs */
|
||||
start() {
|
||||
this.onStart(this.portId)
|
||||
}
|
||||
|
||||
/** TODO: Add docs */
|
||||
edit(value: Ast.Owned | string) {
|
||||
this.onEdit(this.portId, value)
|
||||
}
|
||||
@ -270,7 +287,8 @@ export interface WidgetEditHooks extends Interaction {
|
||||
* to indicate that creating the new item has been handled and the child should not perform its action in this case.
|
||||
*/
|
||||
addItem?(): boolean
|
||||
/** Hook called when the edit is aborted because the component instance is about to be unmounted due to a change in
|
||||
/**
|
||||
* Hook called when the edit is aborted because the component instance is about to be unmounted due to a change in
|
||||
* the graph.
|
||||
*
|
||||
* In this case, if a successor is identified in the graph, the interaction will be restarted. If this hook is
|
||||
|
@ -40,6 +40,7 @@ const { provideFn, injectFn } = createContextStore(
|
||||
},
|
||||
)
|
||||
|
||||
/** TODO: Add docs */
|
||||
export function useCurrentEdit() {
|
||||
const currentEditRoot = shallowRef<WidgetEditHandlerRoot>()
|
||||
return {
|
||||
|
@ -23,6 +23,7 @@ export class Awareness {
|
||||
public internal: YjsAwareness
|
||||
private uploadingFiles: Map<ClientId, Uploads>
|
||||
|
||||
/** TODO: Add docs */
|
||||
constructor(doc: Y.Doc) {
|
||||
this.internal = new YjsAwareness(doc)
|
||||
this.internal.setLocalState(initialState())
|
||||
@ -39,18 +40,21 @@ export class Awareness {
|
||||
})
|
||||
}
|
||||
|
||||
/** TODO: Add docs */
|
||||
public addOrUpdateUpload(name: FileName, file: UploadingFile) {
|
||||
this.withUploads((uploads) => {
|
||||
uploads[name] = file
|
||||
})
|
||||
}
|
||||
|
||||
/** TODO: Add docs */
|
||||
public removeUpload(name: FileName) {
|
||||
this.withUploads((uploads) => {
|
||||
delete uploads[name]
|
||||
})
|
||||
}
|
||||
|
||||
/** TODO: Add docs */
|
||||
public allUploads(): Iterable<[FileName, UploadingFile]> {
|
||||
return [...this.uploadingFiles.values()].flatMap((uploads) => [...Object.entries(uploads)])
|
||||
}
|
||||
|
@ -6,6 +6,7 @@ import { watchEffect } from 'vue'
|
||||
import type { AstId } from 'ydoc-shared/ast'
|
||||
import { IdMap, type ExternalId, type SourceRange } from 'ydoc-shared/yjsModel'
|
||||
|
||||
/** TODO: Add docs */
|
||||
export function parseWithSpans<T extends Record<string, SourceRange>>(code: string, spans: T) {
|
||||
const nameToEid = new Map<keyof T, ExternalId>()
|
||||
const eid = (name: keyof T) => nameToEid.get(name)!
|
||||
|
@ -38,10 +38,12 @@ export interface BindingInfo {
|
||||
usages: Set<AstId>
|
||||
}
|
||||
|
||||
/** TODO: Add docs */
|
||||
export class BindingsDb {
|
||||
bindings = new ReactiveDb<AstId, BindingInfo>()
|
||||
identifierToBindingId = new ReactiveIndex(this.bindings, (id, info) => [[info.identifier, id]])
|
||||
|
||||
/** TODO: Add docs */
|
||||
readFunctionAst(
|
||||
func: Ast.Function,
|
||||
rawFunc: RawAst.Tree.Function | undefined,
|
||||
@ -100,7 +102,8 @@ export class BindingsDb {
|
||||
}
|
||||
}
|
||||
|
||||
/** Create mappings between bindings' ranges and AST
|
||||
/**
|
||||
* Create mappings between bindings' ranges and AST
|
||||
*
|
||||
* The AliasAnalyzer is general and returns ranges, but we're interested in AST nodes. This
|
||||
* method creates mappings in both ways. For given range, only the shallowest AST node will be
|
||||
@ -132,6 +135,7 @@ export class BindingsDb {
|
||||
}
|
||||
}
|
||||
|
||||
/** TODO: Add docs */
|
||||
export class GraphDb {
|
||||
nodeIdToNode = new ReactiveDb<NodeId, Node>()
|
||||
private readonly nodeSources = new Map<NodeId, { data: NodeSource; stop: WatchStopHandle }>()
|
||||
@ -140,6 +144,7 @@ export class GraphDb {
|
||||
private readonly idFromExternalMap = reactive(new Map<ExternalId, Ast.AstId>())
|
||||
private bindings = new BindingsDb()
|
||||
|
||||
/** TODO: Add docs */
|
||||
constructor(
|
||||
private suggestionDb: SuggestionDb,
|
||||
private groups: Ref<Group[]>,
|
||||
@ -207,6 +212,7 @@ export class GraphDb {
|
||||
return this.suggestionDb.findByMethodPointer(method)
|
||||
})
|
||||
|
||||
/** TODO: Add docs */
|
||||
getNodeMainSuggestion(id: NodeId) {
|
||||
const suggestionId = this.nodeMainSuggestionId.lookup(id)
|
||||
if (suggestionId == null) return
|
||||
@ -222,10 +228,12 @@ export class GraphDb {
|
||||
)
|
||||
})
|
||||
|
||||
/** TODO: Add docs */
|
||||
getNodeFirstOutputPort(id: NodeId | undefined): AstId | undefined {
|
||||
return id ? set.first(this.nodeOutputPorts.lookup(id)) ?? this.idFromExternal(id) : undefined
|
||||
}
|
||||
|
||||
/** TODO: Add docs */
|
||||
*getNodeUsages(id: NodeId): IterableIterator<AstId> {
|
||||
const outputPorts = this.nodeOutputPorts.lookup(id)
|
||||
for (const outputPort of outputPorts) {
|
||||
@ -233,44 +241,54 @@ export class GraphDb {
|
||||
}
|
||||
}
|
||||
|
||||
/** TODO: Add docs */
|
||||
getExpressionNodeId(exprId: AstId | undefined): NodeId | undefined {
|
||||
return exprId && set.first(this.nodeIdToExprIds.reverseLookup(exprId))
|
||||
}
|
||||
|
||||
/** TODO: Add docs */
|
||||
getPatternExpressionNodeId(exprId: AstId | undefined): NodeId | undefined {
|
||||
return exprId && set.first(this.nodeIdToPatternExprIds.reverseLookup(exprId))
|
||||
}
|
||||
|
||||
/** TODO: Add docs */
|
||||
getIdentDefiningNode(ident: string): NodeId | undefined {
|
||||
const binding = set.first(this.bindings.identifierToBindingId.lookup(ident))
|
||||
return this.getPatternExpressionNodeId(binding)
|
||||
}
|
||||
|
||||
/** TODO: Add docs */
|
||||
getExpressionInfo(id: AstId | ExternalId | undefined): ExpressionInfo | undefined {
|
||||
const externalId = isUuid(id) ? id : this.idToExternal(id)
|
||||
return externalId && this.valuesRegistry.getExpressionInfo(externalId)
|
||||
}
|
||||
|
||||
/** TODO: Add docs */
|
||||
getOutputPortIdentifier(source: AstId | undefined): string | undefined {
|
||||
return source ? this.bindings.bindings.get(source)?.identifier : undefined
|
||||
}
|
||||
|
||||
/** TODO: Add docs */
|
||||
identifierUsed(ident: string): boolean {
|
||||
return this.bindings.identifierToBindingId.hasKey(ident)
|
||||
}
|
||||
|
||||
/** TODO: Add docs */
|
||||
nodeIds(): IterableIterator<NodeId> {
|
||||
return this.nodeIdToNode.keys()
|
||||
}
|
||||
|
||||
/** TODO: Add docs */
|
||||
isNodeId(externalId: ExternalId): boolean {
|
||||
return this.nodeIdToNode.has(asNodeId(externalId))
|
||||
}
|
||||
|
||||
/** TODO: Add docs */
|
||||
isKnownFunctionCall(id: AstId): boolean {
|
||||
return this.getMethodCallInfo(id) != null
|
||||
}
|
||||
|
||||
/** TODO: Add docs */
|
||||
getMethodCall(id: AstId): MethodCall | undefined {
|
||||
const info = this.getExpressionInfo(id)
|
||||
if (info == null) return
|
||||
@ -279,6 +297,7 @@ export class GraphDb {
|
||||
)
|
||||
}
|
||||
|
||||
/** TODO: Add docs */
|
||||
getMethodCallInfo(id: AstId): MethodCallInfo | undefined {
|
||||
const methodCall = this.getMethodCall(id)
|
||||
if (methodCall == null) return
|
||||
@ -289,10 +308,12 @@ export class GraphDb {
|
||||
return { methodCall, methodCallSource: id, suggestion }
|
||||
}
|
||||
|
||||
/** TODO: Add docs */
|
||||
getNodeColorStyle(id: NodeId): string {
|
||||
return this.nodeColor.lookup(id) ?? 'var(--node-color-no-type)'
|
||||
}
|
||||
|
||||
/** TODO: Add docs */
|
||||
moveNodeToTop(id: NodeId) {
|
||||
const node = this.nodeIdToNode.get(id)
|
||||
if (!node) return
|
||||
@ -450,6 +471,7 @@ export class GraphDb {
|
||||
this.bindings.readFunctionAst(functionAst_, rawFunction, moduleCode, getSpan)
|
||||
}
|
||||
|
||||
/** TODO: Add docs */
|
||||
updateExternalIds(topLevel: Ast.Ast) {
|
||||
const idToExternalNew = new Map()
|
||||
const idFromExternalNew = new Map()
|
||||
@ -481,6 +503,7 @@ export class GraphDb {
|
||||
}
|
||||
}
|
||||
|
||||
/** TODO: Add docs */
|
||||
nodeByRootAstId(astId: Ast.AstId): Node | undefined {
|
||||
const nodeId = asNodeId(this.idToExternal(astId))
|
||||
return nodeId != null ? this.nodeIdToNode.get(nodeId) : undefined
|
||||
@ -490,7 +513,8 @@ export class GraphDb {
|
||||
idFromExternal(id: ExternalId | undefined): AstId | undefined {
|
||||
return id ? this.idFromExternalMap.get(id) : id
|
||||
}
|
||||
/** Get the external ID corresponding to the given `AstId` as of the last synchronization.
|
||||
/**
|
||||
* Get the external ID corresponding to the given `AstId` as of the last synchronization.
|
||||
*
|
||||
* Note that if there is an edit in progress (i.e. a `MutableModule` containing changes that haven't been committed
|
||||
* and observed), this may be different from the value return by calling `toExternal` on the edited `Ast` object.
|
||||
@ -508,10 +532,12 @@ export class GraphDb {
|
||||
return id ? this.idToExternalMap.get(id) : undefined
|
||||
}
|
||||
|
||||
/** TODO: Add docs */
|
||||
static Mock(registry = ComputedValueRegistry.Mock(), db = new SuggestionDb()): GraphDb {
|
||||
return new GraphDb(db, ref([]), registry)
|
||||
}
|
||||
|
||||
/** TODO: Add docs */
|
||||
mockNode(binding: string, id: NodeId, code?: string): Node {
|
||||
const edit = MutableModule.Transient()
|
||||
const pattern = Ast.parse(binding, edit)
|
||||
@ -550,11 +576,15 @@ export class GraphDb {
|
||||
interface NodeSource {
|
||||
/** The outer AST of the node (see {@link NodeDataFromAst.outerExpr}). */
|
||||
outerAst: Ast.Ast
|
||||
/** Whether the node is `output` of the function or not. Mutually exclusive with `isInput`.
|
||||
* Output node is the last node in a function body and has no pattern. */
|
||||
/**
|
||||
* Whether the node is `output` of the function or not. Mutually exclusive with `isInput`.
|
||||
* Output node is the last node in a function body and has no pattern.
|
||||
*/
|
||||
isOutput: boolean
|
||||
/** Whether the node is `input` of the function or not. Mutually exclusive with `isOutput`.
|
||||
* Input node is a function argument. */
|
||||
/**
|
||||
* Whether the node is `input` of the function or not. Mutually exclusive with `isOutput`.
|
||||
* Input node is a function argument.
|
||||
*/
|
||||
isInput: boolean
|
||||
/** The index of the argument in the function's argument list, if the node is an input node. */
|
||||
argIndex: number | undefined
|
||||
@ -567,6 +597,7 @@ export type NodeId = string & ExternalId & { [brandNodeId]: never }
|
||||
export type NodeType = 'component' | 'output' | 'input'
|
||||
export function asNodeId(id: ExternalId): NodeId
|
||||
export function asNodeId(id: ExternalId | undefined): NodeId | undefined
|
||||
/** TODO: Add docs */
|
||||
export function asNodeId(id: ExternalId | undefined): NodeId | undefined {
|
||||
return id != null ? (id as NodeId) : undefined
|
||||
}
|
||||
@ -583,13 +614,18 @@ export interface NodeDataFromAst {
|
||||
outerExpr: Ast.Ast
|
||||
/** The left side of the assignment expression, if `outerExpr` is an assignment expression. */
|
||||
pattern: Ast.Ast | undefined
|
||||
/** The value of the node. The right side of the assignment, if `outerExpr` is an assignment
|
||||
* expression, else the entire `outerExpr`. */
|
||||
/**
|
||||
* The value of the node. The right side of the assignment, if `outerExpr` is an assignment
|
||||
* expression, else the entire `outerExpr`.
|
||||
*/
|
||||
rootExpr: Ast.Ast
|
||||
/** The expression displayed by the node. This is `rootExpr`, minus the prefixes, which are in
|
||||
* `prefixes`. */
|
||||
/**
|
||||
* The expression displayed by the node. This is `rootExpr`, minus the prefixes, which are in
|
||||
* `prefixes`.
|
||||
*/
|
||||
innerExpr: Ast.Ast
|
||||
/** Prefixes that are present in `rootExpr` but omitted in `innerExpr` to ensure a clean output.
|
||||
/**
|
||||
Prefixes that are present in `rootExpr` but omitted in `innerExpr` to ensure a clean output.
|
||||
*/
|
||||
prefixes: Record<'enableRecording', Ast.AstId[] | undefined>
|
||||
/** A child AST in a syntactic position to be a self-argument input to the node. */
|
||||
|
@ -121,7 +121,8 @@ export function addImports(scope: Ast.MutableBodyBlock, importsToAdd: RequiredIm
|
||||
scope.insert(position, ...imports)
|
||||
}
|
||||
|
||||
/** Return a suitable location in the given block to insert an import statement.
|
||||
/**
|
||||
* Return a suitable location in the given block to insert an import statement.
|
||||
*
|
||||
* The location chosen will be before the first non-import line, and after all preexisting imports.
|
||||
* If there are any blank lines in that range, it will be before them.
|
||||
@ -142,8 +143,9 @@ function newImportsLocation(scope: Ast.BodyBlock): number {
|
||||
return lastImport === undefined ? 0 : lastImport + 1
|
||||
}
|
||||
|
||||
/** Create an AST representing the required import statement.
|
||||
* @internal */
|
||||
/**
|
||||
* Create an AST representing the required import statement.
|
||||
@internal */
|
||||
export function requiredImportToAst(value: RequiredImport, module?: MutableModule) {
|
||||
const module_ = module ?? MutableModule.Transient()
|
||||
switch (value.kind) {
|
||||
@ -210,6 +212,7 @@ export function requiredImports(
|
||||
}
|
||||
}
|
||||
|
||||
/** TODO: Add docs */
|
||||
export function requiredImportsByFQN(
|
||||
db: SuggestionDb,
|
||||
fqn: QualifiedName,
|
||||
@ -238,6 +241,7 @@ function entryFQNFromRequiredImport(importStatement: RequiredImport): QualifiedN
|
||||
}
|
||||
}
|
||||
|
||||
/** TODO: Add docs */
|
||||
export function requiredImportEquals(left: RequiredImport, right: RequiredImport): boolean {
|
||||
if (left.kind != right.kind) return false
|
||||
switch (left.kind) {
|
||||
@ -274,6 +278,7 @@ export function covers(existing: Import, required: RequiredImport): boolean {
|
||||
return directlyImported || importedInList || importedWithAll
|
||||
}
|
||||
|
||||
/** TODO: Add docs */
|
||||
export function filterOutRedundantImports(
|
||||
existing: Import[],
|
||||
required: RequiredImport[],
|
||||
@ -294,6 +299,7 @@ export interface DetectedConflict {
|
||||
export type ConflictInfo = DetectedConflict | undefined
|
||||
|
||||
/* Detect possible name clash when adding `importsForEntry` with `existingImports` present. */
|
||||
/** TODO: Add docs */
|
||||
export function detectImportConflicts(
|
||||
suggestionDb: SuggestionDb,
|
||||
existingImports: Import[],
|
||||
|
@ -73,7 +73,9 @@ export interface NodeEditInfo {
|
||||
initialCursorPos: number
|
||||
}
|
||||
|
||||
/** TODO: Add docs */
|
||||
export class PortViewInstance {
|
||||
/** TODO: Add docs */
|
||||
constructor(
|
||||
public rect: ShallowRef<Rect | undefined>,
|
||||
public nodeId: NodeId,
|
||||
@ -222,10 +224,11 @@ export const { injectFn: useGraphStore, provideFn: provideGraphStore } = createC
|
||||
return Ok(method)
|
||||
}
|
||||
|
||||
/** Generate unique identifier from `prefix` and some numeric suffix.
|
||||
/**
|
||||
* Generate unique identifier from `prefix` and some numeric suffix.
|
||||
* @param prefix - of the identifier
|
||||
* @param ignore - a list of identifiers to consider as unavailable. Useful when creating multiple identifiers in a batch.
|
||||
* */
|
||||
*/
|
||||
function generateLocallyUniqueIdent(
|
||||
prefix?: string | undefined,
|
||||
ignore: Set<Identifier> = new Set(),
|
||||
@ -524,8 +527,10 @@ export const { injectFn: useGraphStore, provideFn: provideGraphStore } = createC
|
||||
return getPortRelativeRect(id) != null
|
||||
}
|
||||
|
||||
/** Return the node ID that has the given `id` as its pattern or primary port.
|
||||
* Technically this is either a component or the input node, as input nodes do not have patterns. */
|
||||
/**
|
||||
* Return the node ID that has the given `id` as its pattern or primary port.
|
||||
* Technically this is either a component or the input node, as input nodes do not have patterns.
|
||||
*/
|
||||
function getSourceNodeId(id: AstId): NodeId | undefined {
|
||||
return db.getPatternExpressionNodeId(id) || getPortPrimaryInstance(id)?.nodeId
|
||||
}
|
||||
@ -556,8 +561,8 @@ export const { injectFn: useGraphStore, provideFn: provideGraphStore } = createC
|
||||
return syncModule.value!.edit()
|
||||
}
|
||||
|
||||
/** Apply the given `edit` to the state.
|
||||
*
|
||||
/**
|
||||
* Apply the given `edit` to the state.
|
||||
* @param skipTreeRepair - If the edit is known not to require any parenthesis insertion, this may be set to `true`
|
||||
* for better performance.
|
||||
*/
|
||||
@ -575,10 +580,10 @@ export const { injectFn: useGraphStore, provideFn: provideGraphStore } = createC
|
||||
syncModule.value!.applyEdit(edit, origin)
|
||||
}
|
||||
|
||||
/** Edit the AST module.
|
||||
*
|
||||
* Optimization options: These are safe to use for metadata-only edits; otherwise, they require extreme caution.
|
||||
/**
|
||||
* Edit the AST module.
|
||||
*
|
||||
* Optimization options: These are safe to use for metadata-only edits; otherwise, they require extreme caution.
|
||||
* @param skipTreeRepair - If the edit is certain not to produce incorrect or non-canonical syntax, this may be set
|
||||
* to `true` for better performance.
|
||||
*/
|
||||
@ -598,7 +603,8 @@ export const { injectFn: useGraphStore, provideFn: provideGraphStore } = createC
|
||||
return result!
|
||||
}
|
||||
|
||||
/** Obtain a version of the given `Ast` for direct mutation. The `ast` must exist in the current module.
|
||||
/**
|
||||
* Obtain a version of the given `Ast` for direct mutation. The `ast` must exist in the current module.
|
||||
* This can be more efficient than creating and committing an edit, but skips tree-repair and cannot be aborted.
|
||||
*/
|
||||
function getMutable<T extends Ast.Ast>(ast: T): Ast.Mutable<T> {
|
||||
@ -799,6 +805,7 @@ export interface ConnectedEdge {
|
||||
target: PortId
|
||||
}
|
||||
|
||||
/** TODO: Add docs */
|
||||
export function isConnected(edge: Edge): edge is ConnectedEdge {
|
||||
return edge.source != null && edge.target != null
|
||||
}
|
||||
|
@ -36,6 +36,7 @@ export interface MouseEditedEdge {
|
||||
event: PointerEvent | undefined
|
||||
}
|
||||
|
||||
/** TODO: Add docs */
|
||||
export function useUnconnectedEdges() {
|
||||
const mouseEditedEdge = ref<UnconnectedEdge & MouseEditedEdge>()
|
||||
const cbEditedEdge = ref<UnconnectedTarget>()
|
||||
|
@ -30,6 +30,7 @@ export class ComputedValueRegistry {
|
||||
markRaw(this)
|
||||
}
|
||||
|
||||
/** TODO: Add docs */
|
||||
static WithExecutionContext(executionContext: ExecutionContext): ComputedValueRegistry {
|
||||
const self = new ComputedValueRegistry()
|
||||
self.executionContext = executionContext
|
||||
@ -37,10 +38,12 @@ export class ComputedValueRegistry {
|
||||
return self
|
||||
}
|
||||
|
||||
/** TODO: Add docs */
|
||||
static Mock(): ComputedValueRegistry {
|
||||
return new ComputedValueRegistry()
|
||||
}
|
||||
|
||||
/** TODO: Add docs */
|
||||
processUpdates(updates: ExpressionUpdate[]) {
|
||||
for (const update of updates) {
|
||||
const info = this.db.get(update.expressionId)
|
||||
@ -49,10 +52,12 @@ export class ComputedValueRegistry {
|
||||
}
|
||||
}
|
||||
|
||||
/** TODO: Add docs */
|
||||
getExpressionInfo(exprId: ExpressionId): ExpressionInfo | undefined {
|
||||
return this.db.get(exprId)
|
||||
}
|
||||
|
||||
/** TODO: Add docs */
|
||||
dispose() {
|
||||
this.executionContext?.off('expressionUpdates', this._updateHandler)
|
||||
}
|
||||
|
@ -109,6 +109,7 @@ export class ExecutionContext extends ObservableV2<ExecutionContextNotification>
|
||||
private visualizationConfigs: Map<Uuid, NodeVisualizationConfiguration> = new Map()
|
||||
private _executionEnvironment: ExecutionEnvironment = 'Design'
|
||||
|
||||
/** TODO: Add docs */
|
||||
constructor(
|
||||
private lsRpc: LanguageServer,
|
||||
entryPoint: EntryPoint,
|
||||
@ -215,20 +216,24 @@ export class ExecutionContext extends ObservableV2<ExecutionContextNotification>
|
||||
}
|
||||
}
|
||||
|
||||
/** TODO: Add docs */
|
||||
get desiredStack() {
|
||||
return this._desiredStack
|
||||
}
|
||||
|
||||
/** TODO: Add docs */
|
||||
set desiredStack(stack: StackItem[]) {
|
||||
this._desiredStack.length = 0
|
||||
this._desiredStack.push(...stack)
|
||||
this.sync()
|
||||
}
|
||||
|
||||
/** TODO: Add docs */
|
||||
push(expressionId: ExpressionId) {
|
||||
this.pushItem({ type: 'LocalCall', expressionId })
|
||||
}
|
||||
|
||||
/** TODO: Add docs */
|
||||
pop() {
|
||||
if (this._desiredStack.length === 1) {
|
||||
console.debug('Cannot pop last item from execution context stack')
|
||||
@ -238,6 +243,7 @@ export class ExecutionContext extends ObservableV2<ExecutionContextNotification>
|
||||
this.sync()
|
||||
}
|
||||
|
||||
/** TODO: Add docs */
|
||||
setVisualization(id: Uuid, configuration: Opt<NodeVisualizationConfiguration>) {
|
||||
if (configuration == null) {
|
||||
this.visualizationConfigs.delete(id)
|
||||
@ -247,6 +253,7 @@ export class ExecutionContext extends ObservableV2<ExecutionContextNotification>
|
||||
this.sync()
|
||||
}
|
||||
|
||||
/** TODO: Add docs */
|
||||
recompute(
|
||||
expressionIds: 'all' | ExternalId[] = 'all',
|
||||
executionEnvironment?: ExecutionEnvironment,
|
||||
@ -260,23 +267,28 @@ export class ExecutionContext extends ObservableV2<ExecutionContextNotification>
|
||||
})
|
||||
}
|
||||
|
||||
/** TODO: Add docs */
|
||||
getStackBottom(): StackItem {
|
||||
return this._desiredStack[0]!
|
||||
}
|
||||
|
||||
/** TODO: Add docs */
|
||||
getStackTop(): StackItem {
|
||||
return this._desiredStack[this._desiredStack.length - 1]!
|
||||
}
|
||||
|
||||
/** TODO: Add docs */
|
||||
get executionEnvironment() {
|
||||
return this._executionEnvironment
|
||||
}
|
||||
|
||||
/** TODO: Add docs */
|
||||
set executionEnvironment(env: ExecutionEnvironment) {
|
||||
this._executionEnvironment = env
|
||||
this.sync()
|
||||
}
|
||||
|
||||
/** TODO: Add docs */
|
||||
dispose() {
|
||||
this.queue.pushTask(async (state) => {
|
||||
if (state.status === 'created') {
|
||||
|
@ -21,8 +21,10 @@ export interface ExpressionInfo {
|
||||
|
||||
/** This class holds the computed values that have been received from the language server. */
|
||||
export class VisualizationDataRegistry {
|
||||
/** This map stores only keys representing attached visualization. The responses for
|
||||
* executeExpression are handled by project store's `executeExpression` method. */
|
||||
/**
|
||||
* This map stores only keys representing attached visualization. The responses for
|
||||
* executeExpression are handled by project store's `executeExpression` method.
|
||||
*/
|
||||
private visualizationValues: Map<Uuid, Result<string> | null>
|
||||
private dataServer: DataServer
|
||||
private executionContext: ExecutionContext
|
||||
@ -30,6 +32,7 @@ export class VisualizationDataRegistry {
|
||||
private dataHandler = this.visualizationUpdate.bind(this)
|
||||
private errorHandler = this.visualizationError.bind(this)
|
||||
|
||||
/** TODO: Add docs */
|
||||
constructor(executionContext: ExecutionContext, dataServer: DataServer) {
|
||||
this.executionContext = executionContext
|
||||
this.dataServer = dataServer
|
||||
@ -80,10 +83,12 @@ export class VisualizationDataRegistry {
|
||||
}
|
||||
}
|
||||
|
||||
/** TODO: Add docs */
|
||||
getRawData(visualizationId: Uuid): Result<string> | null {
|
||||
return this.visualizationValues.get(visualizationId) ?? null
|
||||
}
|
||||
|
||||
/** TODO: Add docs */
|
||||
dispose() {
|
||||
this.executionContext.off('visualizationsConfigured', this.reconfiguredHandler)
|
||||
this.dataServer.off(`${OutboundPayload.VISUALIZATION_UPDATE}`, this.dataHandler)
|
||||
|
@ -44,6 +44,7 @@ export function getGroupIndex(
|
||||
return findIndexOpt(groups, (group) => `${group.project}.${group.name}` == normalized)
|
||||
}
|
||||
|
||||
/** TODO: Add docs */
|
||||
export function documentationData(
|
||||
documentation: Opt<string>,
|
||||
definedIn: QualifiedName,
|
||||
|
@ -22,7 +22,8 @@ export type {
|
||||
SuggestionId,
|
||||
} from 'ydoc-shared/languageServerTypes/suggestions'
|
||||
|
||||
/** An alias type for typename (for entry fields like `returnType`).
|
||||
/**
|
||||
* An alias type for typename (for entry fields like `returnType`).
|
||||
*
|
||||
* It's not QualifiedName, because it may be a type with parameters, or
|
||||
* a type union.
|
||||
@ -51,8 +52,10 @@ export interface SuggestionEntry {
|
||||
aliases: string[]
|
||||
/** A type of the "self" argument. This field is present only for instance methods. */
|
||||
selfType?: Typename
|
||||
/** Argument lists of suggested object (atom or function). If the object does not take any
|
||||
* arguments, the list is empty. */
|
||||
/**
|
||||
* Argument lists of suggested object (atom or function). If the object does not take any
|
||||
* arguments, the list is empty.
|
||||
*/
|
||||
arguments: SuggestionEntryArgument[]
|
||||
/** A type returned by the suggested object. */
|
||||
returnType: Typename
|
||||
@ -93,6 +96,7 @@ export function entryMethodPointer(entry: SuggestionEntry): MethodPointer | unde
|
||||
}
|
||||
}
|
||||
|
||||
/** TODO: Add docs */
|
||||
export function entryOwnerQn(entry: SuggestionEntry): QualifiedName | null {
|
||||
if (entry.kind == SuggestionKind.Module) {
|
||||
return qnParent(entry.definedIn)
|
||||
@ -103,6 +107,7 @@ export function entryOwnerQn(entry: SuggestionEntry): QualifiedName | null {
|
||||
|
||||
const DOCUMENTATION_ROOT = 'https://help.enso.org/docs/api'
|
||||
|
||||
/** TODO: Add docs */
|
||||
export function suggestionDocumentationUrl(entry: SuggestionEntry): string | undefined {
|
||||
if (entry.kind !== SuggestionKind.Method && entry.kind !== SuggestionKind.Function) return
|
||||
const location = entry.memberOf ?? entry.definedIn
|
||||
@ -136,11 +141,13 @@ function makeSimpleEntry(
|
||||
}
|
||||
}
|
||||
|
||||
/** TODO: Add docs */
|
||||
export function makeModule(fqn: string): SuggestionEntry {
|
||||
assert(isQualifiedName(fqn))
|
||||
return makeSimpleEntry(SuggestionKind.Module, fqn, qnLastSegment(fqn), fqn)
|
||||
}
|
||||
|
||||
/** TODO: Add docs */
|
||||
export function makeType(fqn: string): SuggestionEntry {
|
||||
assert(isQualifiedName(fqn))
|
||||
const [definedIn, name] = qnSplit(fqn)
|
||||
@ -148,6 +155,7 @@ export function makeType(fqn: string): SuggestionEntry {
|
||||
return makeSimpleEntry(SuggestionKind.Type, definedIn, name, fqn)
|
||||
}
|
||||
|
||||
/** TODO: Add docs */
|
||||
export function makeConstructor(fqn: string): SuggestionEntry {
|
||||
assert(isQualifiedName(fqn))
|
||||
const [type, name] = qnSplit(fqn)
|
||||
@ -160,6 +168,7 @@ export function makeConstructor(fqn: string): SuggestionEntry {
|
||||
}
|
||||
}
|
||||
|
||||
/** TODO: Add docs */
|
||||
export function makeMethod(fqn: string, returnType: string = 'Any'): SuggestionEntry {
|
||||
assert(isQualifiedName(fqn))
|
||||
assert(isQualifiedName(returnType))
|
||||
@ -174,6 +183,7 @@ export function makeMethod(fqn: string, returnType: string = 'Any'): SuggestionE
|
||||
}
|
||||
}
|
||||
|
||||
/** TODO: Add docs */
|
||||
export function makeStaticMethod(fqn: string, returnType: string = 'Any'): SuggestionEntry {
|
||||
assert(isQualifiedName(fqn))
|
||||
assert(isQualifiedName(returnType))
|
||||
@ -187,6 +197,7 @@ export function makeStaticMethod(fqn: string, returnType: string = 'Any'): Sugge
|
||||
}
|
||||
}
|
||||
|
||||
/** TODO: Add docs */
|
||||
export function makeModuleMethod(fqn: string, returnType: string = 'Any'): SuggestionEntry {
|
||||
assert(isQualifiedName(fqn))
|
||||
assert(isQualifiedName(returnType))
|
||||
@ -198,6 +209,7 @@ export function makeModuleMethod(fqn: string, returnType: string = 'Any'): Sugge
|
||||
}
|
||||
}
|
||||
|
||||
/** TODO: Add docs */
|
||||
export function makeFunction(
|
||||
definedIn: string,
|
||||
name: string,
|
||||
@ -209,6 +221,7 @@ export function makeFunction(
|
||||
return makeSimpleEntry(SuggestionKind.Function, definedIn, name, returnType)
|
||||
}
|
||||
|
||||
/** TODO: Add docs */
|
||||
export function makeLocal(
|
||||
definedIn: string,
|
||||
name: string,
|
||||
@ -220,6 +233,7 @@ export function makeLocal(
|
||||
return makeSimpleEntry(SuggestionKind.Local, definedIn, name, returnType)
|
||||
}
|
||||
|
||||
/** TODO: Add docs */
|
||||
export function makeArgument(name: string, type: string = 'Any'): SuggestionEntryArgument {
|
||||
return {
|
||||
name,
|
||||
|
@ -17,6 +17,15 @@ import type { MethodPointer } from 'ydoc-shared/languageServerTypes'
|
||||
import * as lsTypes from 'ydoc-shared/languageServerTypes/suggestions'
|
||||
import { exponentialBackoff } from 'ydoc-shared/util/net'
|
||||
|
||||
/**
|
||||
* Suggestion Database.
|
||||
*
|
||||
* The entries are retrieved (and updated) from engine throug Language Server API. They represent
|
||||
* all entities available in current project (from the project and all imported libraries).
|
||||
*
|
||||
* It is used for code completion/component browser suggestions (thence the name), but also for
|
||||
* retrieving information about method/function in widgets, and many more.
|
||||
*/
|
||||
export class SuggestionDb extends ReactiveDb<SuggestionId, SuggestionEntry> {
|
||||
nameToId = new ReactiveIndex(this, (id, entry) => [[entryQn(entry), id]])
|
||||
childIdToParentId = new ReactiveIndex(this, (id, entry) => {
|
||||
@ -29,6 +38,7 @@ export class SuggestionDb extends ReactiveDb<SuggestionId, SuggestionEntry> {
|
||||
})
|
||||
conflictingNames = new ReactiveIndex(this, (id, entry) => [[entry.name, id]])
|
||||
|
||||
/** Get entry by its fully qualified name */
|
||||
getEntryByQualifiedName(name: QualifiedName): SuggestionEntry | undefined {
|
||||
const [id] = this.nameToId.lookup(name)
|
||||
if (id) {
|
||||
@ -36,6 +46,10 @@ export class SuggestionDb extends ReactiveDb<SuggestionId, SuggestionEntry> {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get entry of method/function by MethodPointer structure (received through expression
|
||||
* updates.
|
||||
*/
|
||||
findByMethodPointer(method: MethodPointer): SuggestionId | undefined {
|
||||
if (method == null) return
|
||||
const moduleName = tryQualifiedName(method.definedOnType)
|
||||
@ -47,6 +61,12 @@ export class SuggestionDb extends ReactiveDb<SuggestionId, SuggestionEntry> {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Component Group.
|
||||
*
|
||||
* These are groups displayed in the Component Browser. Also, nodes being a call to method from
|
||||
* given group will inherit its color.
|
||||
*/
|
||||
export interface Group {
|
||||
color?: string
|
||||
name: string
|
||||
@ -149,6 +169,7 @@ class Synchronizer {
|
||||
}
|
||||
}
|
||||
|
||||
/** {@link useSuggestionDbStore} composable object */
|
||||
export type SuggestionDbStore = ReturnType<typeof useSuggestionDbStore>
|
||||
export const { provideFn: provideSuggestionDbStore, injectFn: useSuggestionDbStore } =
|
||||
createContextStore('suggestionDatabase', (projectStore: ProjectStore) => {
|
||||
|
@ -122,6 +122,7 @@ function setLsDocumentation(
|
||||
if (data.iconName == null) delete entry.iconName
|
||||
}
|
||||
|
||||
/** TODO: Add docs */
|
||||
export function entryFromLs(
|
||||
lsEntry: lsTypes.SuggestionEntry,
|
||||
groups: Group[],
|
||||
@ -319,6 +320,7 @@ function applyArgumentsUpdate(
|
||||
}
|
||||
}
|
||||
|
||||
/** TODO: Add docs */
|
||||
export function applyUpdate(
|
||||
entries: SuggestionDb,
|
||||
update: lsTypes.SuggestionsDatabaseUpdate,
|
||||
@ -394,6 +396,7 @@ export function applyUpdate(
|
||||
}
|
||||
}
|
||||
|
||||
/** TODO: Add docs */
|
||||
export function applyUpdates(
|
||||
entries: SuggestionDb,
|
||||
updates: lsTypes.SuggestionsDatabaseUpdate[],
|
||||
|
@ -25,7 +25,8 @@
|
||||
* excluding the style and script compilation steps.
|
||||
* - (end `importVue`) An `AddUrlNotification` with path `/Viz.vue` is sent to the main
|
||||
* thread.
|
||||
* - A `CompilationResultResponse` with id `1` and path `/Viz.vue` is sent to the main thread. */
|
||||
* - A `CompilationResultResponse` with id `1` and path `/Viz.vue` is sent to the main thread.
|
||||
*/
|
||||
|
||||
import { assertNever } from '@/util/assert'
|
||||
import { parse as babelParse } from '@babel/parser'
|
||||
@ -39,14 +40,16 @@ import { compileScript, compileStyle, parse } from 'vue/compiler-sfc'
|
||||
// === Requests (Main Thread to Worker) ===
|
||||
// ========================================
|
||||
|
||||
/** A request to compile a visualization module. The Worker MUST reply with a
|
||||
/**
|
||||
* A request to compile a visualization module. The Worker MUST reply with a
|
||||
* {@link CompilationResultResponse} when compilation is done, or a {@link CompilationErrorResponse}
|
||||
* when compilation fails. The `id` is an arbitrary number that uniquely identifies the request.
|
||||
* The `path` is either an absolute URL (`http://doma.in/path/to/TheScript.vue`), or a root-relative
|
||||
* URL (`/visualizations/TheScript.vue`). Relative URLs (`./TheScript.vue`) are NOT valid.
|
||||
*
|
||||
* Note that compiling files other than Vue files (TypeScript, SVG etc.) is currently NOT
|
||||
* supported. */
|
||||
* supported.
|
||||
*/
|
||||
export interface CompileRequest {
|
||||
type: 'compile-request'
|
||||
id: number
|
||||
@ -55,8 +58,10 @@ export interface CompileRequest {
|
||||
recompile?: boolean
|
||||
}
|
||||
|
||||
/** A request to mark modules as built-in, indicating that the compiler should re-write the imports
|
||||
* into object destructures. */
|
||||
/**
|
||||
* A request to mark modules as built-in, indicating that the compiler should re-write the imports
|
||||
* into object destructures.
|
||||
*/
|
||||
export interface RegisterBuiltinModulesRequest {
|
||||
type: 'register-builtin-modules-request'
|
||||
modules: string[]
|
||||
@ -68,19 +73,23 @@ export interface RegisterBuiltinModulesRequest {
|
||||
|
||||
// These are messages sent in response to a query. They contain the `id` of the original query.
|
||||
|
||||
/** Sent in response to a {@link CompileRequest}, with an `id` matching the `id` of the original
|
||||
/**
|
||||
* Sent in response to a {@link CompileRequest}, with an `id` matching the `id` of the original
|
||||
* request. Contains only the `path` of the resulting file (which should have also been sent in the
|
||||
* {@link CompileRequest}).
|
||||
* The content itself will have been sent earlier as an {@link AddImportNotification}. */
|
||||
* The content itself will have been sent earlier as an {@link AddImportNotification}.
|
||||
*/
|
||||
export interface CompilationResultResponse {
|
||||
type: 'compilation-result-response'
|
||||
id: number
|
||||
path: string
|
||||
}
|
||||
|
||||
/** Sent in response to a {@link CompileRequest}, with an `id` matching the `id` of the original
|
||||
/**
|
||||
* Sent in response to a {@link CompileRequest}, with an `id` matching the `id` of the original
|
||||
* request. Contains the `path` of the resulting file (which should have also been sent in the
|
||||
* {@link CompileRequest}), and the `error` thrown during compilation. */
|
||||
* {@link CompileRequest}), and the `error` thrown during compilation.
|
||||
*/
|
||||
export interface CompilationErrorResponse {
|
||||
type: 'compilation-error-response'
|
||||
id: number
|
||||
@ -114,8 +123,10 @@ export interface FetchResultWorkerResponse {
|
||||
// === Worker Errors (Main Thread to Worker) ===
|
||||
// =============================================
|
||||
|
||||
/** Sent when fetching a dependency failed. Currently, the worker will forward this back to the
|
||||
* main thread as a {@link FetchError}. */
|
||||
/**
|
||||
* Sent when fetching a dependency failed. Currently, the worker will forward this back to the
|
||||
* main thread as a {@link FetchError}.
|
||||
*/
|
||||
export interface FetchWorkerError {
|
||||
type: 'fetch-worker-error'
|
||||
path: string
|
||||
@ -128,30 +139,36 @@ export interface FetchWorkerError {
|
||||
|
||||
// These are sent when a subtask successfully completes execution.
|
||||
|
||||
/** Sent after compiling `<style>` and `<style scoped>` sections.
|
||||
* These should be attached to the DOM - placement does not matter. */
|
||||
/**
|
||||
* Sent after compiling `<style>` and `<style scoped>` sections.
|
||||
* These should be attached to the DOM - placement does not matter.
|
||||
*/
|
||||
export interface AddStyleNotification {
|
||||
type: 'add-style-notification'
|
||||
path: string
|
||||
code: string
|
||||
}
|
||||
|
||||
/** Currently unused.
|
||||
/**
|
||||
* Currently unused.
|
||||
*
|
||||
* Sent after compiling an import which does not result in a URL.
|
||||
*
|
||||
* Should be added to the cache using `cache[path] = value`. */
|
||||
* Should be added to the cache using `cache[path] = value`.
|
||||
*/
|
||||
export interface AddRawImportNotification {
|
||||
type: 'add-raw-import-notification'
|
||||
path: string
|
||||
value: unknown
|
||||
}
|
||||
|
||||
/** Sent after compiling an import which results in a URL as its default export.
|
||||
/**
|
||||
* Sent after compiling an import which results in a URL as its default export.
|
||||
* This is usually the case for assets.
|
||||
*
|
||||
* Should be added to the cache using
|
||||
* `cache[path] = { default: URL.createObjectURL(new Blob([value], { type: mimeType })) }`. */
|
||||
* `cache[path] = { default: URL.createObjectURL(new Blob([value], { type: mimeType })) }`.
|
||||
*/
|
||||
export interface AddURLImportNotification {
|
||||
type: 'add-url-import-notification'
|
||||
path: string
|
||||
@ -159,10 +176,12 @@ export interface AddURLImportNotification {
|
||||
value: BlobPart
|
||||
}
|
||||
|
||||
/** Sent after compiling a JavaScript import.
|
||||
/**
|
||||
* Sent after compiling a JavaScript import.
|
||||
*
|
||||
* Should be added to the cache using
|
||||
* `cache[path] = import(URL.createObjectURL(new Blob([code], { type: 'text/javascript' })))`. */
|
||||
* `cache[path] = import(URL.createObjectURL(new Blob([code], { type: 'text/javascript' })))`.
|
||||
*/
|
||||
export interface AddImportNotification {
|
||||
type: 'add-import-notification'
|
||||
path: string
|
||||
@ -245,7 +264,8 @@ function toError(error: unknown) {
|
||||
return error instanceof Error ? error : new Error(String(error))
|
||||
}
|
||||
|
||||
/** Extract the file extension from a URL. If no extension was found, it returns the empty string.
|
||||
/**
|
||||
Extract the file extension from a URL. If no extension was found, it returns the empty string.
|
||||
*/
|
||||
function extractExtension(path: string) {
|
||||
return path.match(/(?<=^|[.])[^.]+?(?=[#?]|$)/)?.[0] ?? ''
|
||||
@ -296,8 +316,10 @@ const fetchCallbacks = new Map<
|
||||
}
|
||||
>()
|
||||
|
||||
/** Fetch on the main thread. This is useful because the main thread may have custom logic for
|
||||
* importing - for example, getting a custom visualization from the project directory. */
|
||||
/**
|
||||
* Fetch on the main thread. This is useful because the main thread may have custom logic for
|
||||
* importing - for example, getting a custom visualization from the project directory.
|
||||
*/
|
||||
function fetchOnMainThread(path: string) {
|
||||
return new Promise<FetchResponse>((resolve, reject) => {
|
||||
fetchCallbacks.set(path, { resolve, reject })
|
||||
|
@ -41,7 +41,9 @@ const moduleCache: Record<string, unknown> = {
|
||||
// anywhere else.
|
||||
window.__visualizationModules = moduleCache
|
||||
|
||||
/** TODO: Add docs */
|
||||
export class InvalidVisualizationModuleError extends TypeError {
|
||||
/** TODO: Add docs */
|
||||
constructor(public path: string) {
|
||||
super(`The module '${path}' is not a visualization.`)
|
||||
}
|
||||
@ -54,14 +56,17 @@ const workerCallbacks = new Map<
|
||||
{ resolve: (result: VisualizationModule) => void; reject: (error: Error) => void }
|
||||
>()
|
||||
|
||||
/** A map from the path of the module to the code of the module.
|
||||
* This is used to prevent duplicated modules, at the cost of increased memory usage. */
|
||||
/**
|
||||
* A map from the path of the module to the code of the module.
|
||||
* This is used to prevent duplicated modules, at the cost of increased memory usage.
|
||||
*/
|
||||
const moduleCode = new Map<string, string>()
|
||||
|
||||
function postMessage<T>(worker: Worker, message: T) {
|
||||
worker.postMessage(message)
|
||||
}
|
||||
|
||||
/** TODO: Add docs */
|
||||
export async function compile(path: string, projectRoot: Opt<Uuid>, data: DataServer) {
|
||||
if (worker == null) {
|
||||
const worker_ = (worker = new Compiler())
|
||||
|
@ -81,11 +81,15 @@ const builtinVisualizationsByName = Object.fromEntries(
|
||||
export const { provideFn: provideVisualizationStore, injectFn: useVisualizationStore } =
|
||||
createContextStore('visualization', (proj: ProjectStore) => {
|
||||
const cache = reactive(new Map<VisualizationId, Promise<VisualizationModule>>())
|
||||
/** A map from file path to {@link AbortController}, so that a file change event can stop previous
|
||||
* file change event handlers for the same path. */
|
||||
/**
|
||||
* A map from file path to {@link AbortController}, so that a file change event can stop previous
|
||||
* file change event handlers for the same path.
|
||||
*/
|
||||
const compilationAbortControllers = reactive(new Map<string, AbortController>())
|
||||
/** A map from file path in the current project, to visualization name. This is required so that
|
||||
* file delete events can remove the cached visualization. */
|
||||
/**
|
||||
* A map from file path in the current project, to visualization name. This is required so that
|
||||
* file delete events can remove the cached visualization.
|
||||
*/
|
||||
const currentProjectVisualizationsByPath = new Map<string, string>()
|
||||
const metadata = new VisualizationMetadataDb()
|
||||
const projectRoot = proj.projectRootId
|
||||
|
@ -13,6 +13,7 @@ function getTypesFromUnion(inputType: Opt<string>) {
|
||||
declare const visualizationIdBrand: unique symbol
|
||||
export type VisualizationId = string & { [visualizationIdBrand]: never }
|
||||
|
||||
/** TODO: Add docs */
|
||||
export function toVisualizationId(meta: VisualizationIdentifier) {
|
||||
return JSON.stringify({
|
||||
// All fields MUST be explicitly written so that the order is consistent.
|
||||
@ -24,10 +25,12 @@ export function toVisualizationId(meta: VisualizationIdentifier) {
|
||||
}) as VisualizationId
|
||||
}
|
||||
|
||||
/** TODO: Add docs */
|
||||
export function fromVisualizationId(key: VisualizationId): VisualizationIdentifier {
|
||||
return JSON.parse(key)
|
||||
}
|
||||
|
||||
/** TODO: Add docs */
|
||||
export class VisualizationMetadataDb extends ReactiveDb<VisualizationId, VisualizationMetadata> {
|
||||
visualizationIdToType = new ReactiveIndex(this, (key, metadata) =>
|
||||
getTypesFromUnion(metadata.inputType).map((type) => [key, type]),
|
||||
|
@ -10,6 +10,7 @@ export type TestCase<T extends StringsWithTypeValues> = {
|
||||
module: Ast.Module
|
||||
}
|
||||
|
||||
/** TODO: Add docs */
|
||||
export function testCase<T extends StringsWithTypeValues>(spec: T): TestCase<T> {
|
||||
let code = ''
|
||||
for (const lineCode of Object.keys(spec)) {
|
||||
@ -46,6 +47,7 @@ export function testCase<T extends StringsWithTypeValues>(spec: T): TestCase<T>
|
||||
return { statements: result as any, module: parsed.module }
|
||||
}
|
||||
|
||||
/** TODO: Add docs */
|
||||
export function tryFindExpressions<T extends StringsWithTypeValues>(
|
||||
root: Ast.Ast,
|
||||
expressions: T,
|
||||
@ -64,6 +66,7 @@ export function tryFindExpressions<T extends StringsWithTypeValues>(
|
||||
return result
|
||||
}
|
||||
|
||||
/** TODO: Add docs */
|
||||
export function findExpressions<T extends StringsWithTypeValues>(
|
||||
root: Ast.Ast,
|
||||
expressions: T,
|
||||
|
@ -29,6 +29,7 @@ import {
|
||||
} from 'ydoc-shared/ast'
|
||||
export * from 'ydoc-shared/ast'
|
||||
|
||||
/** TODO: Add docs */
|
||||
export function deserialize(serialized: string): Owned {
|
||||
const parsed: SerializedPrintedSource = JSON.parse(serialized)
|
||||
// Not implemented: restoring serialized external IDs. This is not the best approach anyway;
|
||||
@ -46,11 +47,13 @@ interface SerializedPrintedSource {
|
||||
code: string
|
||||
}
|
||||
|
||||
/** TODO: Add docs */
|
||||
export function serialize(ast: Ast): string {
|
||||
return JSON.stringify(print(ast))
|
||||
}
|
||||
|
||||
export type TokenTree = (TokenTree | string)[]
|
||||
/** TODO: Add docs */
|
||||
export function tokenTree(root: Ast): TokenTree {
|
||||
const module = root.module
|
||||
return Array.from(root.concreteChildren(), (child) => {
|
||||
@ -63,6 +66,7 @@ export function tokenTree(root: Ast): TokenTree {
|
||||
})
|
||||
}
|
||||
|
||||
/** TODO: Add docs */
|
||||
export function tokenTreeWithIds(root: Ast): TokenTree {
|
||||
const module = root.module
|
||||
return [
|
||||
@ -78,6 +82,7 @@ export function tokenTreeWithIds(root: Ast): TokenTree {
|
||||
]
|
||||
}
|
||||
|
||||
/** TODO: Add docs */
|
||||
export function moduleMethodNames(topLevel: BodyBlock): Set<string> {
|
||||
const result = new Set<string>()
|
||||
for (const statement of topLevel.statements()) {
|
||||
@ -90,6 +95,7 @@ export function moduleMethodNames(topLevel: BodyBlock): Set<string> {
|
||||
}
|
||||
|
||||
// FIXME: We should use alias analysis to handle ambiguous names correctly.
|
||||
/** TODO: Add docs */
|
||||
export function findModuleMethod(topLevel: BodyBlock, name: string): Function | undefined {
|
||||
for (const statement of topLevel.statements()) {
|
||||
const inner = statement.innerExpression()
|
||||
@ -100,19 +106,22 @@ export function findModuleMethod(topLevel: BodyBlock, name: string): Function |
|
||||
return undefined
|
||||
}
|
||||
|
||||
/** TODO: Add docs */
|
||||
export function functionBlock(topLevel: BodyBlock, name: string) {
|
||||
const func = findModuleMethod(topLevel, name)
|
||||
if (!(func?.body instanceof BodyBlock)) return undefined
|
||||
return func.body
|
||||
}
|
||||
|
||||
/** TODO: Add docs */
|
||||
export function deleteFromParentBlock(ast: MutableAst) {
|
||||
const parent = ast.mutableParent()
|
||||
if (parent instanceof MutableBodyBlock)
|
||||
parent.updateLines((lines) => lines.filter((line) => line.expression?.node.id !== ast.id))
|
||||
}
|
||||
|
||||
/** If the input is a chain of applications of the given left-associative operator, and all the leaves of the
|
||||
/**
|
||||
* If the input is a chain of applications of the given left-associative operator, and all the leaves of the
|
||||
* operator-application tree are identifier expressions, return the identifiers from left to right.
|
||||
* This is analogous to `ast.code().split(operator)`, but type-enforcing.
|
||||
*/
|
||||
@ -136,7 +145,8 @@ export function unrollOprChain(
|
||||
return idents
|
||||
}
|
||||
|
||||
/** If the input is a chain of property accesses (uses of the `.` operator with a syntactic identifier on the RHS), and
|
||||
/**
|
||||
* If the input is a chain of property accesses (uses of the `.` operator with a syntactic identifier on the RHS), and
|
||||
* the value at the beginning of the sequence is an identifier expression, return all the identifiers from left to
|
||||
* right. This is analogous to `ast.code().split('.')`, but type-enforcing.
|
||||
*/
|
||||
@ -152,6 +162,7 @@ export function unrollPropertyAccess(ast: Ast): IdentifierOrOperatorIdentifier[]
|
||||
return idents
|
||||
}
|
||||
|
||||
/** TODO: Add docs */
|
||||
export function parseIdent(ast: Ast): IdentifierOrOperatorIdentifier | null {
|
||||
if (ast instanceof Ident) {
|
||||
return ast.code()
|
||||
@ -160,17 +171,21 @@ export function parseIdent(ast: Ast): IdentifierOrOperatorIdentifier | null {
|
||||
}
|
||||
}
|
||||
|
||||
/** TODO: Add docs */
|
||||
export function parseIdents(ast: Ast): IdentifierOrOperatorIdentifier[] | null {
|
||||
return unrollOprChain(ast, ',')
|
||||
}
|
||||
|
||||
/** TODO: Add docs */
|
||||
export function parseQualifiedName(ast: Ast): QualifiedName | null {
|
||||
const idents = unrollPropertyAccess(ast)
|
||||
return idents && normalizeQualifiedName(qnFromSegments(idents))
|
||||
}
|
||||
|
||||
/** Substitute `pattern` inside `expression` with `to`.
|
||||
* Will only replace the first item in the property acccess chain. */
|
||||
/**
|
||||
* Substitute `pattern` inside `expression` with `to`.
|
||||
* Will only replace the first item in the property acccess chain.
|
||||
*/
|
||||
export function substituteIdentifier(
|
||||
expr: MutableAst,
|
||||
pattern: IdentifierOrOperatorIdentifier,
|
||||
@ -192,8 +207,10 @@ export function substituteIdentifier(
|
||||
}
|
||||
}
|
||||
|
||||
/** Substitute `pattern` inside `expression` with `to`.
|
||||
* Replaces identifier, the whole qualified name, or the beginning of the qualified name (first segments of property access chain). */
|
||||
/**
|
||||
* Substitute `pattern` inside `expression` with `to`.
|
||||
* Replaces identifier, the whole qualified name, or the beginning of the qualified name (first segments of property access chain).
|
||||
*/
|
||||
export function substituteQualifiedName(
|
||||
expr: MutableAst,
|
||||
pattern: QualifiedName | IdentifierOrOperatorIdentifier,
|
||||
@ -218,7 +235,8 @@ export function substituteQualifiedName(
|
||||
}
|
||||
}
|
||||
|
||||
/** Try to convert the number to an Enso value.
|
||||
/**
|
||||
* Try to convert the number to an Enso value.
|
||||
*
|
||||
* Returns `undefined` if the input is not a real number. NOTE: The current implementation doesn't support numbers that
|
||||
* JS prints in scientific notation.
|
||||
@ -235,6 +253,7 @@ export function tryNumberToEnso(value: number, module: MutableModule) {
|
||||
}
|
||||
}
|
||||
|
||||
/** TODO: Add docs */
|
||||
export function tryEnsoToNumber(ast: Ast) {
|
||||
const [sign, literal] = ast instanceof NegationApp ? [-1, ast.argument] : [1, ast]
|
||||
if (!(literal instanceof NumericLiteral)) return
|
||||
@ -243,6 +262,7 @@ export function tryEnsoToNumber(ast: Ast) {
|
||||
return sign * Number(literal.code().replace(/_/g, ''))
|
||||
}
|
||||
|
||||
/** TODO: Add docs */
|
||||
export function copyIntoNewModule<T extends Ast>(ast: T): Owned<Mutable<T>> {
|
||||
const module = MutableModule.Transient()
|
||||
module.importCopy(ast)
|
||||
|
@ -15,7 +15,8 @@ const ACCESSOR_OPERATOR = '.'
|
||||
|
||||
const LAMBDA_OPERATOR = '->'
|
||||
|
||||
/** Whether the debug logs of the alias analyzer should be enabled.
|
||||
/**
|
||||
* Whether the debug logs of the alias analyzer should be enabled.
|
||||
*
|
||||
* It is recommended to keep them disabled (unless debugging this module), as they are very noisy and can.
|
||||
*/
|
||||
@ -25,9 +26,9 @@ class Scope {
|
||||
/** The variables defined in this scope. */
|
||||
bindings: Map<string, RawAst.Token> = new Map()
|
||||
|
||||
/** Construct a new scope for the given range. If the parent scope is provided, the new scope will be added to its
|
||||
/**
|
||||
* Construct a new scope for the given range. If the parent scope is provided, the new scope will be added to its
|
||||
* children.
|
||||
*
|
||||
* @param range The range of the code that is covered by this scope.
|
||||
* @param parent The parent scope.
|
||||
*/
|
||||
@ -36,8 +37,8 @@ class Scope {
|
||||
public parent?: Scope,
|
||||
) {}
|
||||
|
||||
/** Resolve the given identifier to a token that defines it.
|
||||
*
|
||||
/**
|
||||
* Resolve the given identifier to a token that defines it.
|
||||
* @param identifier The identifier to resolve.
|
||||
* @param location The location of the usage of the identifier. It affects visibility of the bindings within this
|
||||
* scope, so the variables are not visible before they are defined. If not provided, the lookup will include all
|
||||
@ -86,6 +87,7 @@ export function identifierKind(token: RawAst.Token.Ident): IdentifierType {
|
||||
}
|
||||
}
|
||||
|
||||
/** TODO: Add docs */
|
||||
export class AliasAnalyzer {
|
||||
/** All symbols that are not yet resolved (i.e. that were not bound in the analyzed tree). */
|
||||
readonly unresolvedSymbols = new MappedSet<SourceRange>(sourceRangeKey)
|
||||
@ -140,6 +142,7 @@ export class AliasAnalyzer {
|
||||
this.aliases.set(range, new MappedSet<SourceRange>(sourceRangeKey))
|
||||
}
|
||||
|
||||
/** TODO: Add docs */
|
||||
addConnection(source: RawAst.Token, target: RawAst.Token) {
|
||||
const sourceRange = parsedTreeOrTokenRange(source)
|
||||
const targetRange = parsedTreeOrTokenRange(target)
|
||||
@ -176,6 +179,7 @@ export class AliasAnalyzer {
|
||||
this.processTree(this.ast)
|
||||
}
|
||||
|
||||
/** TODO: Add docs */
|
||||
processToken(token?: RawAst.Token): void {
|
||||
if (token?.type !== RawAst.Token.Type.Ident) return
|
||||
if (identifierKind(token) === IdentifierType.Variable) {
|
||||
@ -187,7 +191,8 @@ export class AliasAnalyzer {
|
||||
}
|
||||
}
|
||||
|
||||
/** Process given AST node, assuming it does not change the alias analysis context.
|
||||
/**
|
||||
* Process given AST node, assuming it does not change the alias analysis context.
|
||||
*
|
||||
* All AST children will be processed recursively.
|
||||
*/
|
||||
@ -207,6 +212,7 @@ export class AliasAnalyzer {
|
||||
})
|
||||
}
|
||||
|
||||
/** TODO: Add docs */
|
||||
processNode(node?: LazyObject): void {
|
||||
if (node == null) {
|
||||
return
|
||||
@ -333,10 +339,10 @@ export class AliasAnalyzer {
|
||||
// === LOG ===
|
||||
// ===========
|
||||
|
||||
/** Provisional logging function. Delegates to `console.log` if {@link LOGGING_ENABLED} is `true`.
|
||||
*
|
||||
/**
|
||||
* Provisional logging function. Delegates to `console.log` if {@link LOGGING_ENABLED} is `true`.
|
||||
* @param messages The messages to log. They are functions, so that they are only evaluated if logging is enabled.
|
||||
**/
|
||||
*/
|
||||
function log(...messages: Array<() => any>) {
|
||||
if (LOGGING_ENABLED ?? false) {
|
||||
console.log(...messages.map((message) => message()))
|
||||
|
@ -164,11 +164,8 @@ class AstExtended<T extends Tree | Token = Tree | Token, HasIdMap extends boolea
|
||||
}
|
||||
|
||||
/**
|
||||
* Recursively visit AST nodes in depth-first order. The children of a node will be skipped when
|
||||
* `visit` callback returns `false`.
|
||||
*
|
||||
* @param node Root node of the tree to walk. It will be visited first.
|
||||
* @param visit Callback that is called for each node. If it returns `false`, the children of that
|
||||
* Recursively visit AST nodes in depth-first order.
|
||||
* @param visitor Callback that is called for each node. If it returns `false`, the children of that
|
||||
* node will be skipped, and the walk will continue to the next sibling.
|
||||
*/
|
||||
visitRecursive(visitor: (t: AstExtended<Tree | Token, HasIdMap>) => boolean) {
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user