diff --git a/app/gui2/e2e/rightDock.spec.ts b/app/gui2/e2e/rightDock.spec.ts new file mode 100644 index 000000000000..6dc3d8fdeba0 --- /dev/null +++ b/app/gui2/e2e/rightDock.spec.ts @@ -0,0 +1,19 @@ +import { expect, test } from 'playwright/test' +import * as actions from './actions' +import { CONTROL_KEY } from './keyboard' + +test('Main method documentation', async ({ page }) => { + await actions.goToGraph(page) + + // Documentation panel hotkey opens right-dock. + await expect(page.getByTestId('rightDock')).not.toBeVisible() + await page.keyboard.press(`${CONTROL_KEY}+D`) + await expect(page.getByTestId('rightDock')).toBeVisible() + + // Right-dock displays main method documentation. + await expect(page.getByTestId('rightDock')).toHaveText('The main method') + + // Documentation hotkey closes right-dock.p + await page.keyboard.press(`${CONTROL_KEY}+D`) + await expect(page.getByTestId('rightDock')).not.toBeVisible() +}) diff --git a/app/gui2/mock/engine.ts b/app/gui2/mock/engine.ts index 5fbdeec09e89..83324aa0f7a8 100644 --- a/app/gui2/mock/engine.ts +++ b/app/gui2/mock/engine.ts @@ -45,6 +45,7 @@ export function placeholderGroups(): LibraryComponentGroup[] { } let mainFile = `\ +## Module documentation from Standard.Base import all func1 arg = @@ -56,6 +57,7 @@ func2 a = r = 42 + a r +## The main method main = five = 5 ten = 10 diff --git a/app/gui2/package.json b/app/gui2/package.json index 13815f07f58f..d25713633db3 100644 --- a/app/gui2/package.json +++ b/app/gui2/package.json @@ -47,6 +47,12 @@ "@floating-ui/vue": "^1.0.6", "@lezer/common": "^1.1.0", "@lezer/highlight": "^1.1.6", + "@milkdown/core": "^7.3.6", + "@milkdown/ctx": "^7.3.6", + "@milkdown/preset-commonmark": "^7.3.6", + "@milkdown/prose": "^7.3.6", + "@milkdown/theme-nord": "^7.3.6", + "@milkdown/vue": "^7.3.6", "@noble/hashes": "^1.3.2", "@open-rpc/client-js": "^1.8.1", "@pinia/testing": "^0.1.3", diff --git a/app/gui2/shared/ast/tree.ts b/app/gui2/shared/ast/tree.ts index e2953e96cdbc..323a6dd0a1db 100644 --- a/app/gui2/shared/ast/tree.ts +++ b/app/gui2/shared/ast/tree.ts @@ -117,8 +117,24 @@ export abstract class Ast { } innerExpression(): Ast { - // TODO: Override this in `Documented`, `Annotated`, `AnnotatedBuiltin` - return this + return this.wrappedExpression()?.innerExpression() ?? this + } + + wrappedExpression(): Ast | undefined { + return undefined + } + + wrappingExpression(): Ast | undefined { + const parent = this.parent() + return parent?.wrappedExpression()?.is(this) ? parent : undefined + } + + wrappingExpressionRoot(): Ast { + return this.wrappingExpression()?.wrappingExpressionRoot() ?? this + } + + documentingAncestor(): Documented | undefined { + return this.wrappingExpression()?.documentingAncestor() } code(): string { @@ -328,6 +344,14 @@ export abstract class MutableAst extends Ast { applyTextEditsToAst(this, textEdits, metadataSource ?? this.module) } + getOrInitDocumentation(): MutableDocumented { + const existing = this.documentingAncestor() + if (existing) return this.module.getVersion(existing) + return this.module + .getVersion(this.wrappingExpressionRoot()) + .updateValue((ast) => Documented.new('', ast)) + } + /////////////////// /** @internal */ @@ -1575,6 +1599,14 @@ export class Documented extends Ast { return raw.startsWith(' ') ? raw.slice(1) : raw } + wrappedExpression(): Ast | undefined { + return this.expression + } + + documentingAncestor(): Documented | undefined { + return this + } + *concreteChildren(_verbatim?: boolean): IterableIterator { const { open, elements, newlines, expression } = getAll(this.fields) yield open diff --git a/app/gui2/src/assets/base.css b/app/gui2/src/assets/base.css index 2d6370f8aecc..1262c0fef31a 100644 --- a/app/gui2/src/assets/base.css +++ b/app/gui2/src/assets/base.css @@ -10,6 +10,7 @@ --color-text-inversed: rgb(255 255 255); --color-app-bg: rgb(255 255 255 / 0.8); --color-menu-entry-hover-bg: rgb(0 0 0 / 0.1); + --color-menu-entry-selected-bg: rgb(0 0 0 / 0.05); --color-visualization-bg: rgb(255 242 242); --color-dim: rgb(0 0 0 / 0.25); --color-frame-bg: rgb(255 255 255 / 0.3); @@ -47,8 +48,13 @@ --font-mono: 'DejaVu Sans Mono', /* System monspace font stack */ ui-monospace, Menlo, Monaco, 'Cascadia Mono', 'Segoe UI Mono', 'Roboto Mono', 'Oxygen Mono', 'Ubuntu Monospace', 'Source Code Pro', 'Fira Mono', 'Droid Sans Mono', 'Courier New', monospace; + /* Default resize handle widths, used for panels. */ + --resize-handle-inside: 3px; + --resize-handle-outside: 3px; + /* Resize handle override for the visualization container. */ --visualization-resize-handle-inside: 3px; --visualization-resize-handle-outside: 3px; + --right-dock-default-width: 40%; } *, diff --git a/app/gui2/src/bindings.ts b/app/gui2/src/bindings.ts index f27cc87fb6c7..e567d41f5fa5 100644 --- a/app/gui2/src/bindings.ts +++ b/app/gui2/src/bindings.ts @@ -1,9 +1,18 @@ import { defineKeybinds } from '@/util/shortcuts' +export const undoBindings = defineKeybinds('undo', { + undo: ['Mod+Z'], + redo: ['Mod+Y', 'Mod+Shift+Z'], +}) + export const codeEditorBindings = defineKeybinds('code-editor', { toggle: ['Mod+`'], }) +export const documentationEditorBindings = defineKeybinds('documentation-editor', { + toggle: ['Mod+D'], +}) + export const interactionBindings = defineKeybinds('current-interaction', { cancel: ['Escape'], }) @@ -18,8 +27,6 @@ export const componentBrowserBindings = defineKeybinds('component-browser', { }) export const graphBindings = defineKeybinds('graph-editor', { - undo: ['Mod+Z'], - redo: ['Mod+Y', 'Mod+Shift+Z'], openComponentBrowser: ['Enter'], toggleVisualization: ['Space'], deleteSelected: ['OsDelete'], diff --git a/app/gui2/src/components/AstDocumentation.vue b/app/gui2/src/components/AstDocumentation.vue new file mode 100644 index 000000000000..8fd96aa573fc --- /dev/null +++ b/app/gui2/src/components/AstDocumentation.vue @@ -0,0 +1,44 @@ + + + diff --git a/app/gui2/src/components/ColorRing/__tests__/gradient.test.ts b/app/gui2/src/components/ColorRing/__tests__/gradient.test.ts index c84f69220fab..07765a7865fa 100644 --- a/app/gui2/src/components/ColorRing/__tests__/gradient.test.ts +++ b/app/gui2/src/components/ColorRing/__tests__/gradient.test.ts @@ -41,7 +41,7 @@ function angularStops(points: Iterable) { return stops } -function stopSpans(stops: Iterable, radius: number) { +function stopSpans(stops: Iterable) { const spans = new Array<{ start: number; end: number; hue: number }>() let prev: AngularStop | undefined = undefined for (const stop of stops) { @@ -78,7 +78,7 @@ function testGradients({ hues, radius }: { hues: number[]; radius: number }) { const stops = angularStops(points) expect(stops[0]?.angle).toBe(0) expect(stops[stops.length - 1]?.angle).toBe(1) - const spans = stopSpans(stops, radius) + const spans = stopSpans(stops) for (const span of spans) { expect(approximateHues).toContain(approximate(span.hue)) if (span.start < span.end) { diff --git a/app/gui2/src/components/ComponentBrowser.vue b/app/gui2/src/components/ComponentBrowser.vue index 7472c3755089..3486e11d4878 100644 --- a/app/gui2/src/components/ComponentBrowser.vue +++ b/app/gui2/src/components/ComponentBrowser.vue @@ -1,10 +1,10 @@ + + diff --git a/app/gui2/src/components/DocumentationEditor/MilkdownEditor.vue b/app/gui2/src/components/DocumentationEditor/MilkdownEditor.vue new file mode 100644 index 000000000000..7fe876d3ff68 --- /dev/null +++ b/app/gui2/src/components/DocumentationEditor/MilkdownEditor.vue @@ -0,0 +1,166 @@ + + + + + + + diff --git a/app/gui2/src/components/ExtendedMenu.vue b/app/gui2/src/components/ExtendedMenu.vue index 136b9fccc281..387bc030254a 100644 --- a/app/gui2/src/components/ExtendedMenu.vue +++ b/app/gui2/src/components/ExtendedMenu.vue @@ -1,23 +1,57 @@ diff --git a/app/gui2/src/components/VisualizationContainer.vue b/app/gui2/src/components/VisualizationContainer.vue index 8ed8d09724b8..d46ea5a684d6 100644 --- a/app/gui2/src/components/VisualizationContainer.vue +++ b/app/gui2/src/components/VisualizationContainer.vue @@ -1,9 +1,11 @@