Skip to content

Commit

Permalink
New markdown editor (#11469)
Browse files Browse the repository at this point in the history
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`.
  • Loading branch information
kazcw authored Nov 6, 2024
1 parent 701bba6 commit 867c77d
Show file tree
Hide file tree
Showing 111 changed files with 3,218 additions and 2,485 deletions.
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@
- [Table Input Widget has now a limit of 256 cells.][11448]
- [Added an error message screen displayed when viewing a deleted
component.][11452]
- [New documentation editor provides improved Markdown editing experience, and
paves the way for new documentation features.][11469]

[11151]: https://github.com/enso-org/enso/pull/11151
[11271]: https://github.com/enso-org/enso/pull/11271
Expand All @@ -37,6 +39,7 @@
[11447]: https://github.com/enso-org/enso/pull/11447
[11448]: https://github.com/enso-org/enso/pull/11448
[11452]: https://github.com/enso-org/enso/pull/11452
[11469]: https://github.com/enso-org/enso/pull/11469

#### Enso Standard Library

Expand Down
3 changes: 3 additions & 0 deletions app/common/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@
"./src/utilities/data/dateTime": "./src/utilities/data/dateTime.ts",
"./src/utilities/data/newtype": "./src/utilities/data/newtype.ts",
"./src/utilities/data/object": "./src/utilities/data/object.ts",
"./src/utilities/data/string": "./src/utilities/data/string.ts",
"./src/utilities/data/iter": "./src/utilities/data/iter.ts",
"./src/utilities/style/tabBar": "./src/utilities/style/tabBar.ts",
"./src/utilities/uniqueString": "./src/utilities/uniqueString.ts",
"./src/text": "./src/text/index.ts",
Expand All @@ -37,6 +39,7 @@
"@tanstack/query-persist-client-core": "^5.54.0",
"@tanstack/vue-query": ">= 5.54.0 < 5.56.0",
"idb-keyval": "^6.2.1",
"lib0": "^0.2.85",
"react": "^18.3.1",
"vitest": "^1.3.1",
"vue": "^3.5.2"
Expand Down
146 changes: 146 additions & 0 deletions app/common/src/utilities/data/__tests__/iterator.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
import { expect, test } from 'vitest'
import * as iter from '../iter'

interface IteratorCase<T> {
iterable: Iterable<T>
soleValue: T | undefined
first: T | undefined
last: T | undefined
count: number
}

function makeCases(): IteratorCase<unknown>[] {
return [
{
iterable: iter.empty(),
soleValue: undefined,
first: undefined,
last: undefined,
count: 0,
},
{
iterable: iter.chain(iter.empty(), iter.empty()),
soleValue: undefined,
first: undefined,
last: undefined,
count: 0,
},
{
iterable: iter.chain(iter.empty(), ['a'], iter.empty()),
soleValue: 'a',
first: 'a',
last: 'a',
count: 1,
},
{
iterable: iter.range(10, 11),
soleValue: 10,
first: 10,
last: 10,
count: 1,
},
{
iterable: iter.range(10, 20),
soleValue: undefined,
first: 10,
last: 19,
count: 10,
},
{
iterable: iter.range(20, 10),
soleValue: undefined,
first: 20,
last: 11,
count: 10,
},
{
iterable: [],
soleValue: undefined,
first: undefined,
last: undefined,
count: 0,
},
{
iterable: ['a'],
soleValue: 'a',
first: 'a',
last: 'a',
count: 1,
},
{
iterable: ['a', 'b'],
soleValue: undefined,
first: 'a',
last: 'b',
count: 2,
},
{
iterable: iter.filterDefined([undefined, 'a', undefined, 'b', undefined]),
soleValue: undefined,
first: 'a',
last: 'b',
count: 2,
},
{
iterable: iter.filter([7, 'a', 8, 'b', 9], el => typeof el === 'string'),
soleValue: undefined,
first: 'a',
last: 'b',
count: 2,
},
{
iterable: iter.zip(['a', 'b'], iter.range(1, 2)),
soleValue: ['a', 1],
first: ['a', 1],
last: ['a', 1],
count: 1,
},
{
iterable: iter.zip(['a', 'b'], iter.range(1, 3)),
soleValue: undefined,
first: ['a', 1],
last: ['b', 2],
count: 2,
},
{
iterable: iter.zip(['a', 'b'], iter.range(1, 4)),
soleValue: undefined,
first: ['a', 1],
last: ['b', 2],
count: 2,
},
{
iterable: iter.zipLongest(['a', 'b'], iter.range(1, 2)),
soleValue: undefined,
first: ['a', 1],
last: ['b', undefined],
count: 2,
},
{
iterable: iter.zipLongest(['a', 'b'], iter.range(1, 3)),
soleValue: undefined,
first: ['a', 1],
last: ['b', 2],
count: 2,
},
{
iterable: iter.zipLongest(['a', 'b'], iter.range(1, 4)),
soleValue: undefined,
first: ['a', 1],
last: [undefined, 3],
count: 3,
},
]
}

test.each(makeCases())('tryGetSoleValue: case %#', ({ iterable, soleValue }) => {
expect(iter.tryGetSoleValue(iterable)).toEqual(soleValue)
})

test.each(makeCases())('last: case %#', ({ iterable, last }) => {
expect(iter.last(iterable)).toEqual(last)
})

test.each(makeCases())('count: case %#', ({ iterable, count }) => {
expect(iter.count(iterable)).toEqual(count)
})
Original file line number Diff line number Diff line change
@@ -1,4 +1,30 @@
/** @file Functions for manipulating {@link Iterable}s. */
/** @file Utilities for manipulating {@link Iterator}s and {@link Iterable}s. */

import { iteratorFilter, mapIterator } from 'lib0/iterator'

/** Similar to {@link Array.prototype.reduce|}, but consumes elements from any iterable. */
export function reduce<T, A>(
iterable: Iterable<T>,
f: (accumulator: A, element: T) => A,
initialAccumulator: A,
): A {
const iterator = iterable[Symbol.iterator]()
let accumulator = initialAccumulator
let result = iterator.next()
while (!result.done) {
accumulator = f(accumulator, result.value)
result = iterator.next()
}
return accumulator
}

/**
* Iterates the provided iterable, returning the number of elements it yielded. Note that if the input is an iterator,
* it will be consumed.
*/
export function count(it: Iterable<unknown>): number {
return reduce(it, a => a + 1, 0)
}

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

/**
* Return an {@link Iterable} that `yield`s values that are the result of calling the given
* function on the next value of the given source iterable.
*/
export function* map<T, U>(iter: Iterable<T>, map: (value: T) => U): IterableIterator<U> {
for (const value of iter) {
yield map(value)
}
/** @returns An iterator that yields the results of applying the given function to each value of the given iterable. */
export function map<T, U>(it: Iterable<T>, f: (value: T) => U): IterableIterator<U> {
return mapIterator(it[Symbol.iterator](), f)
}

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

/**
Expand Down Expand Up @@ -141,3 +162,45 @@ export class Resumable<T> {
}
}
}

/** Returns an iterator that yields the values of the provided iterator that are not strictly-equal to `undefined`. */
export function* filterDefined<T>(iterable: Iterable<T | undefined>): IterableIterator<T> {
for (const value of iterable) {
if (value !== undefined) yield value
}
}

/**
* Returns whether the predicate returned `true` for all values yielded by the provided iterator. Short-circuiting.
* Returns `true` if the iterator doesn't yield any values.
*/
export function every<T>(iter: Iterable<T>, f: (value: T) => boolean): boolean {
for (const value of iter) if (!f(value)) return false
return true
}

/** Return the first element returned by the iterable which meets the condition. */
export function find<T>(iter: Iterable<T>, f: (value: T) => boolean): T | undefined {
for (const value of iter) {
if (f(value)) return value
}
return undefined
}

/** Returns the first element yielded by the iterable. */
export function first<T>(iterable: Iterable<T>): T | undefined {
const iterator = iterable[Symbol.iterator]()
const result = iterator.next()
return result.done ? undefined : result.value
}

/**
* Return last element returned by the iterable.
* NOTE: Linear complexity. This function always visits the whole iterable. Using this with an
* infinite generator will cause an infinite loop.
*/
export function last<T>(iter: Iterable<T>): T | undefined {
let last
for (const el of iter) last = el
return last
}
21 changes: 21 additions & 0 deletions app/common/src/utilities/data/object.ts
Original file line number Diff line number Diff line change
Expand Up @@ -162,3 +162,24 @@ export type ExtractKeys<T, U> = {

/** An instance method of the given type. */
export type MethodOf<T> = (this: T, ...args: never) => unknown

// ===================
// === useObjectId ===
// ===================

/** Composable providing support for managing object identities. */
export function useObjectId() {
let lastId = 0
const idNumbers = new WeakMap<object, number>()
/** @returns A value that can be used to compare object identity. */
function objectId(o: object): number {
const id = idNumbers.get(o)
if (id == null) {
lastId += 1
idNumbers.set(o, lastId)
return lastId
}
return id
}
return { objectId }
}
2 changes: 2 additions & 0 deletions app/common/src/utilities/data/string.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
/** See http://www.unicode.org/reports/tr18/#Line_Boundaries */
export const LINE_BOUNDARIES = /\r\n|[\n\v\f\r\x85\u2028\u2029]/g
2 changes: 1 addition & 1 deletion app/gui/e2e/project-view/locate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ export const addNewNodeButton = componentLocator('.PlusButton')
export const componentBrowser = componentLocator('.ComponentBrowser')
export const nodeOutputPort = componentLocator('.outputPortHoverArea')
export const smallPlusButton = componentLocator('.SmallPlusButton')
export const lexicalContent = componentLocator('.LexicalContent')
export const editorRoot = componentLocator('.EditorRoot')

/**
* A not-selected variant of Component Browser Entry.
Expand Down
21 changes: 19 additions & 2 deletions app/gui/e2e/project-view/rightPanel.spec.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { expect, test } from 'playwright/test'
import * as actions from './actions'
import { mockMethodCallInfo } from './expressionUpdates'
import { mockCollapsedFunctionInfo, mockMethodCallInfo } from './expressionUpdates'
import { CONTROL_KEY } from './keyboard'
import * as locate from './locate'

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

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

// Documentation hotkey closes right-dock.p
await page.keyboard.press(`${CONTROL_KEY}+D`)
Expand Down Expand Up @@ -70,3 +70,20 @@ test('Component help', async ({ page }) => {
await locate.graphNodeByBinding(page, 'data').click()
await expect(locate.rightDock(page)).toHaveText(/Reads a file into Enso/)
})

test('Documentation reflects entered function', async ({ page }) => {
await actions.goToGraph(page)

// Open the panel
await expect(locate.rightDock(page)).toBeHidden()
await page.keyboard.press(`${CONTROL_KEY}+D`)
await expect(locate.rightDock(page)).toBeVisible()

// Enter the collapsed function
await mockCollapsedFunctionInfo(page, 'final', 'func1')
await locate.graphNodeByBinding(page, 'final').dblclick()
await expect(locate.navBreadcrumb(page)).toHaveText(['Mock Project', 'func1'])

// Editor should contain collapsed function's docs
await expect(locate.editorRoot(locate.rightDock(page))).toHaveText('A collapsed function')
})
8 changes: 2 additions & 6 deletions app/gui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -83,22 +83,18 @@
"babel-plugin-react-compiler": "19.0.0-beta-9ee70a1-20241017",
"@codemirror/commands": "^6.6.0",
"@codemirror/language": "^6.10.2",
"@codemirror/lang-markdown": "^v6.3.0",
"@codemirror/lint": "^6.8.1",
"@codemirror/search": "^6.5.6",
"@codemirror/state": "^6.4.1",
"@codemirror/view": "^6.28.3",
"@fast-check/vitest": "^0.0.8",
"@floating-ui/vue": "^1.0.6",
"@lexical/code": "^0.16.0",
"@lexical/link": "^0.16.0",
"@lexical/list": "^0.16.0",
"@lexical/markdown": "^0.16.0",
"@lexical/plain-text": "^0.16.0",
"@lexical/rich-text": "^0.16.0",
"@lexical/selection": "^0.16.0",
"@lexical/table": "^0.16.0",
"@lexical/utils": "^0.16.0",
"@lezer/common": "^1.1.0",
"@lezer/markdown": "^1.3.1",
"@lezer/highlight": "^1.1.6",
"@noble/hashes": "^1.4.0",
"@vueuse/core": "^10.4.1",
Expand Down
2 changes: 1 addition & 1 deletion app/gui/src/project-view/assets/base.css
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@
/* Resize handle override for the visualization container. */
--visualization-resize-handle-inside: 3px;
--visualization-resize-handle-outside: 3px;
--right-dock-default-width: 40%;
--right-dock-default-width: 40vw;
--code-editor-default-height: 30%;
--scrollbar-scrollable-opacity: 100%;
}
Loading

0 comments on commit 867c77d

Please sign in to comment.