Skip to content

Commit

Permalink
Documentation editor (#9910)
Browse files Browse the repository at this point in the history
#### 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.
  • Loading branch information
kazcw authored May 10, 2024
1 parent a14a95c commit 4af33f0
Show file tree
Hide file tree
Showing 28 changed files with 1,862 additions and 384 deletions.
19 changes: 19 additions & 0 deletions app/gui2/e2e/rightDock.spec.ts
Original file line number Diff line number Diff line change
@@ -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()
})
2 changes: 2 additions & 0 deletions app/gui2/mock/engine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ export function placeholderGroups(): LibraryComponentGroup[] {
}

let mainFile = `\
## Module documentation
from Standard.Base import all
func1 arg =
Expand All @@ -56,6 +57,7 @@ func2 a =
r = 42 + a
r
## The main method
main =
five = 5
ten = 10
Expand Down
6 changes: 6 additions & 0 deletions app/gui2/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
36 changes: 34 additions & 2 deletions app/gui2/shared/ast/tree.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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 */
Expand Down Expand Up @@ -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<RawNodeChild> {
const { open, elements, newlines, expression } = getAll(this.fields)
yield open
Expand Down
6 changes: 6 additions & 0 deletions app/gui2/src/assets/base.css
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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%;
}

*,
Expand Down
11 changes: 9 additions & 2 deletions app/gui2/src/bindings.ts
Original file line number Diff line number Diff line change
@@ -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'],
})
Expand All @@ -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'],
Expand Down
44 changes: 44 additions & 0 deletions app/gui2/src/components/AstDocumentation.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
<script setup lang="ts">
import DocumentationEditor from '@/components/DocumentationEditor.vue'
import { useGraphStore } from '@/stores/graph'
import { Ast } from '@/util/ast'
import { computed } from 'vue'
const editing = defineModel<boolean>('editing', { default: false })
const props = defineProps<{
ast: Ast.Ast | undefined
/** If provided, this property will be read to obtain the current documentation text instead of finding it in the AST.
* This can be used to reduce reactive dependencies. */
documentation?: string
/** If set, the Enter key will end editing instead of inserting a newline, unless the Shift key is held. */
preferSingleLine?: boolean | undefined
}>()
const graphStore = useGraphStore()
const documentation = computed({
get: () =>
props?.documentation != null ?
props.documentation
: props.ast?.documentingAncestor()?.documentation() ?? '',
set: (value) => {
const ast = props.ast
if (!ast) return
if (value.trimStart() !== '') {
graphStore.edit((edit) =>
edit.getVersion(ast).getOrInitDocumentation().setDocumentationText(value),
)
} else {
// Remove the documentation node.
const documented = props.ast?.documentingAncestor()
if (documented && documented.expression)
graphStore.edit((edit) =>
edit.getVersion(documented).update((documented) => documented.expression!.take()),
)
}
},
})
</script>

<template>
<DocumentationEditor v-model="documentation" v-model:editing="editing" :preferSingleLine />
</template>
4 changes: 2 additions & 2 deletions app/gui2/src/components/ColorRing/__tests__/gradient.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ function angularStops(points: Iterable<GradientPoint>) {
return stops
}

function stopSpans(stops: Iterable<AngularStop>, radius: number) {
function stopSpans(stops: Iterable<AngularStop>) {
const spans = new Array<{ start: number; end: number; hue: number }>()
let prev: AngularStop | undefined = undefined
for (const stop of stops) {
Expand Down Expand Up @@ -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) {
Expand Down
2 changes: 1 addition & 1 deletion app/gui2/src/components/ComponentBrowser.vue
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
<script setup lang="ts">
import { componentBrowserBindings } from '@/bindings'
import { default as DocumentationPanel } from '@/components/ComponentBrowser/DocumentationPanel.vue'
import { makeComponentList, type Component } from '@/components/ComponentBrowser/component'
import { Filtering } from '@/components/ComponentBrowser/filtering'
import { useComponentBrowserInput, type Usage } from '@/components/ComponentBrowser/input'
import { useScrolling } from '@/components/ComponentBrowser/scrolling'
import { default as DocumentationPanel } from '@/components/DocumentationPanel.vue'
import GraphVisualization from '@/components/GraphEditor/GraphVisualization.vue'
import SvgIcon from '@/components/SvgIcon.vue'
import ToggleIcon from '@/components/ToggleIcon.vue'
Expand Down
26 changes: 26 additions & 0 deletions app/gui2/src/components/DocumentationEditor.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<script setup lang="ts">
import { MilkdownProvider } from '@milkdown/vue'
import { defineAsyncComponent } from 'vue'
const documentation = defineModel<string>({ required: true })
const editing = defineModel<boolean>('editing', { default: false })
const props = defineProps<{
preferSingleLine?: boolean | undefined
}>()
const LazyMilkdownEditor = defineAsyncComponent(
() => import('@/components/DocumentationEditor/MilkdownEditor.vue'),
)
</script>

<template>
<Suspense>
<MilkdownProvider>
<LazyMilkdownEditor
v-model="documentation"
v-model:editing="editing"
:preferSingleLine="props.preferSingleLine"
/>
</MilkdownProvider>
</Suspense>
</template>
Loading

0 comments on commit 4af33f0

Please sign in to comment.