Skip to content

Commit 867c77d

Browse files
authored
New markdown editor (#11469)
Implements #11240. https://github.com/user-attachments/assets/4d2f8021-3e0f-4d39-95df-bcd72bf7545b # Important Notes - Fix a Yjs document corruption bug caused by `DeepReadonly` being replaced by a stub; introduce a `DeepReadonly` implementation without Vue dependency. - Fix right panel sizing when code editor is open. - Fix right panel slide-in animation. - `Ast.Function` renamed to `Ast.FunctionDef`.
1 parent 701bba6 commit 867c77d

File tree

111 files changed

+3218
-2485
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

111 files changed

+3218
-2485
lines changed

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@
2222
- [Table Input Widget has now a limit of 256 cells.][11448]
2323
- [Added an error message screen displayed when viewing a deleted
2424
component.][11452]
25+
- [New documentation editor provides improved Markdown editing experience, and
26+
paves the way for new documentation features.][11469]
2527

2628
[11151]: https://github.com/enso-org/enso/pull/11151
2729
[11271]: https://github.com/enso-org/enso/pull/11271
@@ -37,6 +39,7 @@
3739
[11447]: https://github.com/enso-org/enso/pull/11447
3840
[11448]: https://github.com/enso-org/enso/pull/11448
3941
[11452]: https://github.com/enso-org/enso/pull/11452
42+
[11469]: https://github.com/enso-org/enso/pull/11469
4043

4144
#### Enso Standard Library
4245

app/common/package.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@
1818
"./src/utilities/data/dateTime": "./src/utilities/data/dateTime.ts",
1919
"./src/utilities/data/newtype": "./src/utilities/data/newtype.ts",
2020
"./src/utilities/data/object": "./src/utilities/data/object.ts",
21+
"./src/utilities/data/string": "./src/utilities/data/string.ts",
22+
"./src/utilities/data/iter": "./src/utilities/data/iter.ts",
2123
"./src/utilities/style/tabBar": "./src/utilities/style/tabBar.ts",
2224
"./src/utilities/uniqueString": "./src/utilities/uniqueString.ts",
2325
"./src/text": "./src/text/index.ts",
@@ -37,6 +39,7 @@
3739
"@tanstack/query-persist-client-core": "^5.54.0",
3840
"@tanstack/vue-query": ">= 5.54.0 < 5.56.0",
3941
"idb-keyval": "^6.2.1",
42+
"lib0": "^0.2.85",
4043
"react": "^18.3.1",
4144
"vitest": "^1.3.1",
4245
"vue": "^3.5.2"
Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
import { expect, test } from 'vitest'
2+
import * as iter from '../iter'
3+
4+
interface IteratorCase<T> {
5+
iterable: Iterable<T>
6+
soleValue: T | undefined
7+
first: T | undefined
8+
last: T | undefined
9+
count: number
10+
}
11+
12+
function makeCases(): IteratorCase<unknown>[] {
13+
return [
14+
{
15+
iterable: iter.empty(),
16+
soleValue: undefined,
17+
first: undefined,
18+
last: undefined,
19+
count: 0,
20+
},
21+
{
22+
iterable: iter.chain(iter.empty(), iter.empty()),
23+
soleValue: undefined,
24+
first: undefined,
25+
last: undefined,
26+
count: 0,
27+
},
28+
{
29+
iterable: iter.chain(iter.empty(), ['a'], iter.empty()),
30+
soleValue: 'a',
31+
first: 'a',
32+
last: 'a',
33+
count: 1,
34+
},
35+
{
36+
iterable: iter.range(10, 11),
37+
soleValue: 10,
38+
first: 10,
39+
last: 10,
40+
count: 1,
41+
},
42+
{
43+
iterable: iter.range(10, 20),
44+
soleValue: undefined,
45+
first: 10,
46+
last: 19,
47+
count: 10,
48+
},
49+
{
50+
iterable: iter.range(20, 10),
51+
soleValue: undefined,
52+
first: 20,
53+
last: 11,
54+
count: 10,
55+
},
56+
{
57+
iterable: [],
58+
soleValue: undefined,
59+
first: undefined,
60+
last: undefined,
61+
count: 0,
62+
},
63+
{
64+
iterable: ['a'],
65+
soleValue: 'a',
66+
first: 'a',
67+
last: 'a',
68+
count: 1,
69+
},
70+
{
71+
iterable: ['a', 'b'],
72+
soleValue: undefined,
73+
first: 'a',
74+
last: 'b',
75+
count: 2,
76+
},
77+
{
78+
iterable: iter.filterDefined([undefined, 'a', undefined, 'b', undefined]),
79+
soleValue: undefined,
80+
first: 'a',
81+
last: 'b',
82+
count: 2,
83+
},
84+
{
85+
iterable: iter.filter([7, 'a', 8, 'b', 9], el => typeof el === 'string'),
86+
soleValue: undefined,
87+
first: 'a',
88+
last: 'b',
89+
count: 2,
90+
},
91+
{
92+
iterable: iter.zip(['a', 'b'], iter.range(1, 2)),
93+
soleValue: ['a', 1],
94+
first: ['a', 1],
95+
last: ['a', 1],
96+
count: 1,
97+
},
98+
{
99+
iterable: iter.zip(['a', 'b'], iter.range(1, 3)),
100+
soleValue: undefined,
101+
first: ['a', 1],
102+
last: ['b', 2],
103+
count: 2,
104+
},
105+
{
106+
iterable: iter.zip(['a', 'b'], iter.range(1, 4)),
107+
soleValue: undefined,
108+
first: ['a', 1],
109+
last: ['b', 2],
110+
count: 2,
111+
},
112+
{
113+
iterable: iter.zipLongest(['a', 'b'], iter.range(1, 2)),
114+
soleValue: undefined,
115+
first: ['a', 1],
116+
last: ['b', undefined],
117+
count: 2,
118+
},
119+
{
120+
iterable: iter.zipLongest(['a', 'b'], iter.range(1, 3)),
121+
soleValue: undefined,
122+
first: ['a', 1],
123+
last: ['b', 2],
124+
count: 2,
125+
},
126+
{
127+
iterable: iter.zipLongest(['a', 'b'], iter.range(1, 4)),
128+
soleValue: undefined,
129+
first: ['a', 1],
130+
last: [undefined, 3],
131+
count: 3,
132+
},
133+
]
134+
}
135+
136+
test.each(makeCases())('tryGetSoleValue: case %#', ({ iterable, soleValue }) => {
137+
expect(iter.tryGetSoleValue(iterable)).toEqual(soleValue)
138+
})
139+
140+
test.each(makeCases())('last: case %#', ({ iterable, last }) => {
141+
expect(iter.last(iterable)).toEqual(last)
142+
})
143+
144+
test.each(makeCases())('count: case %#', ({ iterable, count }) => {
145+
expect(iter.count(iterable)).toEqual(count)
146+
})

app/ydoc-shared/src/util/data/iterable.ts renamed to app/common/src/utilities/data/iter.ts

Lines changed: 74 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,30 @@
1-
/** @file Functions for manipulating {@link Iterable}s. */
1+
/** @file Utilities for manipulating {@link Iterator}s and {@link Iterable}s. */
2+
3+
import { iteratorFilter, mapIterator } from 'lib0/iterator'
4+
5+
/** Similar to {@link Array.prototype.reduce|}, but consumes elements from any iterable. */
6+
export function reduce<T, A>(
7+
iterable: Iterable<T>,
8+
f: (accumulator: A, element: T) => A,
9+
initialAccumulator: A,
10+
): A {
11+
const iterator = iterable[Symbol.iterator]()
12+
let accumulator = initialAccumulator
13+
let result = iterator.next()
14+
while (!result.done) {
15+
accumulator = f(accumulator, result.value)
16+
result = iterator.next()
17+
}
18+
return accumulator
19+
}
20+
21+
/**
22+
* Iterates the provided iterable, returning the number of elements it yielded. Note that if the input is an iterator,
23+
* it will be consumed.
24+
*/
25+
export function count(it: Iterable<unknown>): number {
26+
return reduce(it, a => a + 1, 0)
27+
}
228

329
/** An iterable with zero elements. */
430
export function* empty(): Generator<never> {}
@@ -26,22 +52,17 @@ export function* range(start: number, stop: number, step = start <= stop ? 1 : -
2652
}
2753
}
2854

29-
/**
30-
* Return an {@link Iterable} that `yield`s values that are the result of calling the given
31-
* function on the next value of the given source iterable.
32-
*/
33-
export function* map<T, U>(iter: Iterable<T>, map: (value: T) => U): IterableIterator<U> {
34-
for (const value of iter) {
35-
yield map(value)
36-
}
55+
/** @returns An iterator that yields the results of applying the given function to each value of the given iterable. */
56+
export function map<T, U>(it: Iterable<T>, f: (value: T) => U): IterableIterator<U> {
57+
return mapIterator(it[Symbol.iterator](), f)
3758
}
3859

3960
/**
4061
* Return an {@link Iterable} that `yield`s only the values from the given source iterable
4162
* that pass the given predicate.
4263
*/
43-
export function* filter<T>(iter: Iterable<T>, include: (value: T) => boolean): IterableIterator<T> {
44-
for (const value of iter) if (include(value)) yield value
64+
export function filter<T>(iter: Iterable<T>, include: (value: T) => boolean): IterableIterator<T> {
65+
return iteratorFilter(iter[Symbol.iterator](), include)
4566
}
4667

4768
/**
@@ -141,3 +162,45 @@ export class Resumable<T> {
141162
}
142163
}
143164
}
165+
166+
/** Returns an iterator that yields the values of the provided iterator that are not strictly-equal to `undefined`. */
167+
export function* filterDefined<T>(iterable: Iterable<T | undefined>): IterableIterator<T> {
168+
for (const value of iterable) {
169+
if (value !== undefined) yield value
170+
}
171+
}
172+
173+
/**
174+
* Returns whether the predicate returned `true` for all values yielded by the provided iterator. Short-circuiting.
175+
* Returns `true` if the iterator doesn't yield any values.
176+
*/
177+
export function every<T>(iter: Iterable<T>, f: (value: T) => boolean): boolean {
178+
for (const value of iter) if (!f(value)) return false
179+
return true
180+
}
181+
182+
/** Return the first element returned by the iterable which meets the condition. */
183+
export function find<T>(iter: Iterable<T>, f: (value: T) => boolean): T | undefined {
184+
for (const value of iter) {
185+
if (f(value)) return value
186+
}
187+
return undefined
188+
}
189+
190+
/** Returns the first element yielded by the iterable. */
191+
export function first<T>(iterable: Iterable<T>): T | undefined {
192+
const iterator = iterable[Symbol.iterator]()
193+
const result = iterator.next()
194+
return result.done ? undefined : result.value
195+
}
196+
197+
/**
198+
* Return last element returned by the iterable.
199+
* NOTE: Linear complexity. This function always visits the whole iterable. Using this with an
200+
* infinite generator will cause an infinite loop.
201+
*/
202+
export function last<T>(iter: Iterable<T>): T | undefined {
203+
let last
204+
for (const el of iter) last = el
205+
return last
206+
}

app/common/src/utilities/data/object.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,3 +162,24 @@ export type ExtractKeys<T, U> = {
162162

163163
/** An instance method of the given type. */
164164
export type MethodOf<T> = (this: T, ...args: never) => unknown
165+
166+
// ===================
167+
// === useObjectId ===
168+
// ===================
169+
170+
/** Composable providing support for managing object identities. */
171+
export function useObjectId() {
172+
let lastId = 0
173+
const idNumbers = new WeakMap<object, number>()
174+
/** @returns A value that can be used to compare object identity. */
175+
function objectId(o: object): number {
176+
const id = idNumbers.get(o)
177+
if (id == null) {
178+
lastId += 1
179+
idNumbers.set(o, lastId)
180+
return lastId
181+
}
182+
return id
183+
}
184+
return { objectId }
185+
}
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
/** See http://www.unicode.org/reports/tr18/#Line_Boundaries */
2+
export const LINE_BOUNDARIES = /\r\n|[\n\v\f\r\x85\u2028\u2029]/g

app/gui/e2e/project-view/locate.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@ export const addNewNodeButton = componentLocator('.PlusButton')
8484
export const componentBrowser = componentLocator('.ComponentBrowser')
8585
export const nodeOutputPort = componentLocator('.outputPortHoverArea')
8686
export const smallPlusButton = componentLocator('.SmallPlusButton')
87-
export const lexicalContent = componentLocator('.LexicalContent')
87+
export const editorRoot = componentLocator('.EditorRoot')
8888

8989
/**
9090
* A not-selected variant of Component Browser Entry.

app/gui/e2e/project-view/rightPanel.spec.ts

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { expect, test } from 'playwright/test'
22
import * as actions from './actions'
3-
import { mockMethodCallInfo } from './expressionUpdates'
3+
import { mockCollapsedFunctionInfo, mockMethodCallInfo } from './expressionUpdates'
44
import { CONTROL_KEY } from './keyboard'
55
import * as locate from './locate'
66

@@ -13,7 +13,7 @@ test('Main method documentation', async ({ page }) => {
1313
await expect(locate.rightDock(page)).toBeVisible()
1414

1515
// Right-dock displays main method documentation.
16-
await expect(locate.lexicalContent(locate.rightDock(page))).toHaveText('The main method')
16+
await expect(locate.editorRoot(locate.rightDock(page))).toHaveText('The main method')
1717

1818
// Documentation hotkey closes right-dock.p
1919
await page.keyboard.press(`${CONTROL_KEY}+D`)
@@ -70,3 +70,20 @@ test('Component help', async ({ page }) => {
7070
await locate.graphNodeByBinding(page, 'data').click()
7171
await expect(locate.rightDock(page)).toHaveText(/Reads a file into Enso/)
7272
})
73+
74+
test('Documentation reflects entered function', async ({ page }) => {
75+
await actions.goToGraph(page)
76+
77+
// Open the panel
78+
await expect(locate.rightDock(page)).toBeHidden()
79+
await page.keyboard.press(`${CONTROL_KEY}+D`)
80+
await expect(locate.rightDock(page)).toBeVisible()
81+
82+
// Enter the collapsed function
83+
await mockCollapsedFunctionInfo(page, 'final', 'func1')
84+
await locate.graphNodeByBinding(page, 'final').dblclick()
85+
await expect(locate.navBreadcrumb(page)).toHaveText(['Mock Project', 'func1'])
86+
87+
// Editor should contain collapsed function's docs
88+
await expect(locate.editorRoot(locate.rightDock(page))).toHaveText('A collapsed function')
89+
})

app/gui/package.json

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -83,22 +83,18 @@
8383
"babel-plugin-react-compiler": "19.0.0-beta-9ee70a1-20241017",
8484
"@codemirror/commands": "^6.6.0",
8585
"@codemirror/language": "^6.10.2",
86+
"@codemirror/lang-markdown": "^v6.3.0",
8687
"@codemirror/lint": "^6.8.1",
8788
"@codemirror/search": "^6.5.6",
8889
"@codemirror/state": "^6.4.1",
8990
"@codemirror/view": "^6.28.3",
9091
"@fast-check/vitest": "^0.0.8",
9192
"@floating-ui/vue": "^1.0.6",
92-
"@lexical/code": "^0.16.0",
9393
"@lexical/link": "^0.16.0",
94-
"@lexical/list": "^0.16.0",
95-
"@lexical/markdown": "^0.16.0",
9694
"@lexical/plain-text": "^0.16.0",
97-
"@lexical/rich-text": "^0.16.0",
98-
"@lexical/selection": "^0.16.0",
99-
"@lexical/table": "^0.16.0",
10095
"@lexical/utils": "^0.16.0",
10196
"@lezer/common": "^1.1.0",
97+
"@lezer/markdown": "^1.3.1",
10298
"@lezer/highlight": "^1.1.6",
10399
"@noble/hashes": "^1.4.0",
104100
"@vueuse/core": "^10.4.1",

app/gui/src/project-view/assets/base.css

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@
8585
/* Resize handle override for the visualization container. */
8686
--visualization-resize-handle-inside: 3px;
8787
--visualization-resize-handle-outside: 3px;
88-
--right-dock-default-width: 40%;
88+
--right-dock-default-width: 40vw;
8989
--code-editor-default-height: 30%;
9090
--scrollbar-scrollable-opacity: 100%;
9191
}

0 commit comments

Comments
 (0)