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:
Adam Obuchowicz 2024-10-09 14:26:56 +02:00 committed by GitHub
parent 78993a0d1a
commit 7c5124094e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
138 changed files with 1131 additions and 391 deletions

View File

@ -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)

View File

@ -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) => {

View File

@ -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

View File

@ -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()

View File

@ -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 }) => {

View File

@ -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
View File

@ -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

View File

@ -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'],

View File

@ -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,

View File

@ -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'() {

View File

@ -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()
},
}
}

View File

@ -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'

View File

@ -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' ||

View File

@ -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)) {

View File

@ -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:'

View File

@ -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)
}

View File

@ -1,5 +1,8 @@
import '@/assets/base.css'
/**
* Load App.vue asynchronously.
*/
export async function AsyncApp() {
const app = await import('@/App.vue')
return app

View File

@ -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 }
}

View File

@ -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,

View File

@ -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: {

View File

@ -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()) {

View File

@ -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

View File

@ -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.
*/

View File

@ -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(),

View File

@ -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(() => {

View File

@ -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
}

View File

@ -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)

View File

@ -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.
*/

View File

@ -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

View File

@ -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,

View File

@ -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)),

View File

@ -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[]

View File

@ -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)

View File

@ -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

View File

@ -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 })

View File

@ -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) {

View File

@ -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

View File

@ -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

View File

@ -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.

View File

@ -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
}

View File

@ -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) {

View File

@ -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>,

View File

@ -19,6 +19,7 @@ export interface LexicalMarkdownPlugin extends LexicalPlugin {
transformers?: Transformer[]
}
/** TODO: Add docs */
export function markdownPlugin(
model: Ref<string>,
extensions: LexicalMarkdownPlugin[],

View File

@ -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>,

View File

@ -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(

View File

@ -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,

View File

@ -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()

View File

@ -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.

View File

@ -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) {

View File

@ -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) {

View File

@ -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,

View File

@ -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

View File

@ -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)

View File

@ -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
}

View File

@ -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.
*/

View File

@ -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

View File

@ -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: {

View File

@ -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()

View File

@ -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>()

View File

@ -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,

View File

@ -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`.

View File

@ -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

View File

@ -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),

View File

@ -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) =>

View File

@ -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)})`
}

View File

@ -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.

View File

@ -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>()

View File

@ -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[]>([])

View File

@ -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,

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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
}

View File

@ -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.

View File

@ -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()
}

View File

@ -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)

View File

@ -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>
}

View File

@ -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

View File

@ -40,6 +40,7 @@ const { provideFn, injectFn } = createContextStore(
},
)
/** TODO: Add docs */
export function useCurrentEdit() {
const currentEditRoot = shallowRef<WidgetEditHandlerRoot>()
return {

View File

@ -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)])
}

View File

@ -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)!

View File

@ -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. */

View File

@ -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[],

View File

@ -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
}

View File

@ -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>()

View File

@ -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)
}

View File

@ -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') {

View File

@ -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)

View File

@ -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,

View File

@ -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,

View File

@ -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) => {

View File

@ -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[],

View File

@ -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 })

View File

@ -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())

View File

@ -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

View File

@ -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]),

View File

@ -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,

View File

@ -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)

View File

@ -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()))

View File

@ -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