Skip to content

Commit 4af33f0

Browse files
authored
Documentation editor (#9910)
#### New documentation panel: - Shows documentation of currently-entered method. - Open/close with Ctrl+D or the extended menu. - Renders markdown; supports WYSIWYG editing. - Formatting can be added by typing the same markdown special characters that will appear in the source code, e.g.: - `# Heading` - `## Subheading` - `*emphasis*` - Panel left edge can be dragged to resize similarly to visualization container. https://github.com/enso-org/enso/assets/1047859/6feb5d23-1525-48f7-933e-c9371312decf #### Node comments are now markdown: ![image](https://github.com/enso-org/enso/assets/1047859/c5df13fe-0290-4f1d-abb2-b2f42df274d3) #### Top bar extended menu improvements: - Now closes after any menu action except +/- buttons, and on defocus/Esc. - Editor/doc-panel buttons now colored to indicate whether editor/panel is open. https://github.com/enso-org/enso/assets/1047859/345af322-c1a8-4717-8ffc-a5c919494fed Closes #9786. # Important Notes New APIs: - `DocumentationEditor` component: Lazily-loads and instantiates the implementation component (`MilkdownEditor`). - `AstDocumentation` component: Connects a `DocumentationEditor` to the documentation of an `Ast` node. - `ResizeHandles` component: Supports reuse of the resize handles used by the visualization container. - `graphStore.undoManager`: Facade for the Y.UndoManager in the project store.
1 parent a14a95c commit 4af33f0

28 files changed

+1862
-384
lines changed

app/gui2/e2e/rightDock.spec.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import { expect, test } from 'playwright/test'
2+
import * as actions from './actions'
3+
import { CONTROL_KEY } from './keyboard'
4+
5+
test('Main method documentation', async ({ page }) => {
6+
await actions.goToGraph(page)
7+
8+
// Documentation panel hotkey opens right-dock.
9+
await expect(page.getByTestId('rightDock')).not.toBeVisible()
10+
await page.keyboard.press(`${CONTROL_KEY}+D`)
11+
await expect(page.getByTestId('rightDock')).toBeVisible()
12+
13+
// Right-dock displays main method documentation.
14+
await expect(page.getByTestId('rightDock')).toHaveText('The main method')
15+
16+
// Documentation hotkey closes right-dock.p
17+
await page.keyboard.press(`${CONTROL_KEY}+D`)
18+
await expect(page.getByTestId('rightDock')).not.toBeVisible()
19+
})

app/gui2/mock/engine.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ export function placeholderGroups(): LibraryComponentGroup[] {
4545
}
4646

4747
let mainFile = `\
48+
## Module documentation
4849
from Standard.Base import all
4950
5051
func1 arg =
@@ -56,6 +57,7 @@ func2 a =
5657
r = 42 + a
5758
r
5859
60+
## The main method
5961
main =
6062
five = 5
6163
ten = 10

app/gui2/package.json

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,12 @@
4747
"@floating-ui/vue": "^1.0.6",
4848
"@lezer/common": "^1.1.0",
4949
"@lezer/highlight": "^1.1.6",
50+
"@milkdown/core": "^7.3.6",
51+
"@milkdown/ctx": "^7.3.6",
52+
"@milkdown/preset-commonmark": "^7.3.6",
53+
"@milkdown/prose": "^7.3.6",
54+
"@milkdown/theme-nord": "^7.3.6",
55+
"@milkdown/vue": "^7.3.6",
5056
"@noble/hashes": "^1.3.2",
5157
"@open-rpc/client-js": "^1.8.1",
5258
"@pinia/testing": "^0.1.3",

app/gui2/shared/ast/tree.ts

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -117,8 +117,24 @@ export abstract class Ast {
117117
}
118118

119119
innerExpression(): Ast {
120-
// TODO: Override this in `Documented`, `Annotated`, `AnnotatedBuiltin`
121-
return this
120+
return this.wrappedExpression()?.innerExpression() ?? this
121+
}
122+
123+
wrappedExpression(): Ast | undefined {
124+
return undefined
125+
}
126+
127+
wrappingExpression(): Ast | undefined {
128+
const parent = this.parent()
129+
return parent?.wrappedExpression()?.is(this) ? parent : undefined
130+
}
131+
132+
wrappingExpressionRoot(): Ast {
133+
return this.wrappingExpression()?.wrappingExpressionRoot() ?? this
134+
}
135+
136+
documentingAncestor(): Documented | undefined {
137+
return this.wrappingExpression()?.documentingAncestor()
122138
}
123139

124140
code(): string {
@@ -328,6 +344,14 @@ export abstract class MutableAst extends Ast {
328344
applyTextEditsToAst(this, textEdits, metadataSource ?? this.module)
329345
}
330346

347+
getOrInitDocumentation(): MutableDocumented {
348+
const existing = this.documentingAncestor()
349+
if (existing) return this.module.getVersion(existing)
350+
return this.module
351+
.getVersion(this.wrappingExpressionRoot())
352+
.updateValue((ast) => Documented.new('', ast))
353+
}
354+
331355
///////////////////
332356

333357
/** @internal */
@@ -1575,6 +1599,14 @@ export class Documented extends Ast {
15751599
return raw.startsWith(' ') ? raw.slice(1) : raw
15761600
}
15771601

1602+
wrappedExpression(): Ast | undefined {
1603+
return this.expression
1604+
}
1605+
1606+
documentingAncestor(): Documented | undefined {
1607+
return this
1608+
}
1609+
15781610
*concreteChildren(_verbatim?: boolean): IterableIterator<RawNodeChild> {
15791611
const { open, elements, newlines, expression } = getAll(this.fields)
15801612
yield open

app/gui2/src/assets/base.css

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
--color-text-inversed: rgb(255 255 255);
1111
--color-app-bg: rgb(255 255 255 / 0.8);
1212
--color-menu-entry-hover-bg: rgb(0 0 0 / 0.1);
13+
--color-menu-entry-selected-bg: rgb(0 0 0 / 0.05);
1314
--color-visualization-bg: rgb(255 242 242);
1415
--color-dim: rgb(0 0 0 / 0.25);
1516
--color-frame-bg: rgb(255 255 255 / 0.3);
@@ -47,8 +48,13 @@
4748
--font-mono: 'DejaVu Sans Mono', /* System monspace font stack */ ui-monospace, Menlo, Monaco,
4849
'Cascadia Mono', 'Segoe UI Mono', 'Roboto Mono', 'Oxygen Mono', 'Ubuntu Monospace',
4950
'Source Code Pro', 'Fira Mono', 'Droid Sans Mono', 'Courier New', monospace;
51+
/* Default resize handle widths, used for panels. */
52+
--resize-handle-inside: 3px;
53+
--resize-handle-outside: 3px;
54+
/* Resize handle override for the visualization container. */
5055
--visualization-resize-handle-inside: 3px;
5156
--visualization-resize-handle-outside: 3px;
57+
--right-dock-default-width: 40%;
5258
}
5359

5460
*,

app/gui2/src/bindings.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,18 @@
11
import { defineKeybinds } from '@/util/shortcuts'
22

3+
export const undoBindings = defineKeybinds('undo', {
4+
undo: ['Mod+Z'],
5+
redo: ['Mod+Y', 'Mod+Shift+Z'],
6+
})
7+
38
export const codeEditorBindings = defineKeybinds('code-editor', {
49
toggle: ['Mod+`'],
510
})
611

12+
export const documentationEditorBindings = defineKeybinds('documentation-editor', {
13+
toggle: ['Mod+D'],
14+
})
15+
716
export const interactionBindings = defineKeybinds('current-interaction', {
817
cancel: ['Escape'],
918
})
@@ -18,8 +27,6 @@ export const componentBrowserBindings = defineKeybinds('component-browser', {
1827
})
1928

2029
export const graphBindings = defineKeybinds('graph-editor', {
21-
undo: ['Mod+Z'],
22-
redo: ['Mod+Y', 'Mod+Shift+Z'],
2330
openComponentBrowser: ['Enter'],
2431
toggleVisualization: ['Space'],
2532
deleteSelected: ['OsDelete'],
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
<script setup lang="ts">
2+
import DocumentationEditor from '@/components/DocumentationEditor.vue'
3+
import { useGraphStore } from '@/stores/graph'
4+
import { Ast } from '@/util/ast'
5+
import { computed } from 'vue'
6+
7+
const editing = defineModel<boolean>('editing', { default: false })
8+
const props = defineProps<{
9+
ast: Ast.Ast | undefined
10+
/** If provided, this property will be read to obtain the current documentation text instead of finding it in the AST.
11+
* This can be used to reduce reactive dependencies. */
12+
documentation?: string
13+
/** If set, the Enter key will end editing instead of inserting a newline, unless the Shift key is held. */
14+
preferSingleLine?: boolean | undefined
15+
}>()
16+
17+
const graphStore = useGraphStore()
18+
const documentation = computed({
19+
get: () =>
20+
props?.documentation != null ?
21+
props.documentation
22+
: props.ast?.documentingAncestor()?.documentation() ?? '',
23+
set: (value) => {
24+
const ast = props.ast
25+
if (!ast) return
26+
if (value.trimStart() !== '') {
27+
graphStore.edit((edit) =>
28+
edit.getVersion(ast).getOrInitDocumentation().setDocumentationText(value),
29+
)
30+
} else {
31+
// Remove the documentation node.
32+
const documented = props.ast?.documentingAncestor()
33+
if (documented && documented.expression)
34+
graphStore.edit((edit) =>
35+
edit.getVersion(documented).update((documented) => documented.expression!.take()),
36+
)
37+
}
38+
},
39+
})
40+
</script>
41+
42+
<template>
43+
<DocumentationEditor v-model="documentation" v-model:editing="editing" :preferSingleLine />
44+
</template>

app/gui2/src/components/ColorRing/__tests__/gradient.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ function angularStops(points: Iterable<GradientPoint>) {
4141
return stops
4242
}
4343

44-
function stopSpans(stops: Iterable<AngularStop>, radius: number) {
44+
function stopSpans(stops: Iterable<AngularStop>) {
4545
const spans = new Array<{ start: number; end: number; hue: number }>()
4646
let prev: AngularStop | undefined = undefined
4747
for (const stop of stops) {
@@ -78,7 +78,7 @@ function testGradients({ hues, radius }: { hues: number[]; radius: number }) {
7878
const stops = angularStops(points)
7979
expect(stops[0]?.angle).toBe(0)
8080
expect(stops[stops.length - 1]?.angle).toBe(1)
81-
const spans = stopSpans(stops, radius)
81+
const spans = stopSpans(stops)
8282
for (const span of spans) {
8383
expect(approximateHues).toContain(approximate(span.hue))
8484
if (span.start < span.end) {

app/gui2/src/components/ComponentBrowser.vue

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
<script setup lang="ts">
22
import { componentBrowserBindings } from '@/bindings'
3+
import { default as DocumentationPanel } from '@/components/ComponentBrowser/DocumentationPanel.vue'
34
import { makeComponentList, type Component } from '@/components/ComponentBrowser/component'
45
import { Filtering } from '@/components/ComponentBrowser/filtering'
56
import { useComponentBrowserInput, type Usage } from '@/components/ComponentBrowser/input'
67
import { useScrolling } from '@/components/ComponentBrowser/scrolling'
7-
import { default as DocumentationPanel } from '@/components/DocumentationPanel.vue'
88
import GraphVisualization from '@/components/GraphEditor/GraphVisualization.vue'
99
import SvgIcon from '@/components/SvgIcon.vue'
1010
import ToggleIcon from '@/components/ToggleIcon.vue'
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
<script setup lang="ts">
2+
import { MilkdownProvider } from '@milkdown/vue'
3+
import { defineAsyncComponent } from 'vue'
4+
5+
const documentation = defineModel<string>({ required: true })
6+
const editing = defineModel<boolean>('editing', { default: false })
7+
const props = defineProps<{
8+
preferSingleLine?: boolean | undefined
9+
}>()
10+
11+
const LazyMilkdownEditor = defineAsyncComponent(
12+
() => import('@/components/DocumentationEditor/MilkdownEditor.vue'),
13+
)
14+
</script>
15+
16+
<template>
17+
<Suspense>
18+
<MilkdownProvider>
19+
<LazyMilkdownEditor
20+
v-model="documentation"
21+
v-model:editing="editing"
22+
:preferSingleLine="props.preferSingleLine"
23+
/>
24+
</MilkdownProvider>
25+
</Suspense>
26+
</template>

0 commit comments

Comments
 (0)