@dxos/react-ui-editor 0.8.2-staging.7ac8446 → 0.8.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/lib/browser/index.mjs +4152 -2852
- package/dist/lib/browser/index.mjs.map +4 -4
- package/dist/lib/browser/meta.json +1 -1
- package/dist/lib/browser/testing/index.mjs +6 -0
- package/dist/lib/browser/testing/index.mjs.map +7 -0
- package/dist/lib/node/index.cjs +3318 -2009
- package/dist/lib/node/index.cjs.map +4 -4
- package/dist/lib/node/meta.json +1 -1
- package/dist/lib/node/testing/index.cjs +29 -0
- package/dist/lib/node/testing/index.cjs.map +7 -0
- package/dist/lib/node-esm/index.mjs +4152 -2852
- package/dist/lib/node-esm/index.mjs.map +4 -4
- package/dist/lib/node-esm/meta.json +1 -1
- package/dist/lib/node-esm/testing/index.mjs +8 -0
- package/dist/lib/node-esm/testing/index.mjs.map +7 -0
- package/dist/types/src/components/EditorToolbar/EditorToolbar.d.ts +1 -1
- package/dist/types/src/components/EditorToolbar/EditorToolbar.d.ts.map +1 -1
- package/dist/types/src/components/EditorToolbar/blocks.d.ts +4 -3
- package/dist/types/src/components/EditorToolbar/blocks.d.ts.map +1 -1
- package/dist/types/src/components/EditorToolbar/formatting.d.ts +4 -3
- package/dist/types/src/components/EditorToolbar/formatting.d.ts.map +1 -1
- package/dist/types/src/components/EditorToolbar/headings.d.ts +4 -3
- package/dist/types/src/components/EditorToolbar/headings.d.ts.map +1 -1
- package/dist/types/src/components/EditorToolbar/{comment.d.ts → image.d.ts} +4 -5
- package/dist/types/src/components/EditorToolbar/image.d.ts.map +1 -0
- package/dist/types/src/components/EditorToolbar/index.d.ts +1 -1
- package/dist/types/src/components/EditorToolbar/index.d.ts.map +1 -1
- package/dist/types/src/components/EditorToolbar/lists.d.ts +4 -3
- package/dist/types/src/components/EditorToolbar/lists.d.ts.map +1 -1
- package/dist/types/src/components/EditorToolbar/search.d.ts +17 -0
- package/dist/types/src/components/EditorToolbar/search.d.ts.map +1 -0
- package/dist/types/src/components/EditorToolbar/util.d.ts +17 -25
- package/dist/types/src/components/EditorToolbar/util.d.ts.map +1 -1
- package/dist/types/src/components/EditorToolbar/{viewMode.d.ts → view-mode.d.ts} +5 -4
- package/dist/types/src/components/EditorToolbar/view-mode.d.ts.map +1 -0
- package/dist/types/src/components/Popover/RefDropdownMenu.d.ts +21 -0
- package/dist/types/src/components/Popover/RefDropdownMenu.d.ts.map +1 -0
- package/dist/types/src/components/Popover/RefPopover.d.ts +21 -0
- package/dist/types/src/components/Popover/RefPopover.d.ts.map +1 -0
- package/dist/types/src/components/Popover/index.d.ts +3 -0
- package/dist/types/src/components/Popover/index.d.ts.map +1 -0
- package/dist/types/src/components/index.d.ts +1 -0
- package/dist/types/src/components/index.d.ts.map +1 -1
- package/dist/types/src/defaults.d.ts +3 -5
- package/dist/types/src/defaults.d.ts.map +1 -1
- package/dist/types/src/extensions/annotations.d.ts +4 -1
- package/dist/types/src/extensions/annotations.d.ts.map +1 -1
- package/dist/types/src/extensions/autocomplete.d.ts +1 -2
- package/dist/types/src/extensions/autocomplete.d.ts.map +1 -1
- package/dist/types/src/extensions/automerge/automerge.d.ts.map +1 -1
- package/dist/types/src/extensions/automerge/automerge.stories.d.ts.map +1 -1
- package/dist/types/src/extensions/automerge/cursor.d.ts.map +1 -1
- package/dist/types/src/extensions/automerge/defs.d.ts +1 -1
- package/dist/types/src/extensions/automerge/defs.d.ts.map +1 -1
- package/dist/types/src/extensions/automerge/sync.d.ts.map +1 -1
- package/dist/types/src/extensions/automerge/update-automerge.d.ts +1 -1
- package/dist/types/src/extensions/automerge/update-automerge.d.ts.map +1 -1
- package/dist/types/src/extensions/automerge/update-codemirror.d.ts +1 -1
- package/dist/types/src/extensions/automerge/update-codemirror.d.ts.map +1 -1
- package/dist/types/src/extensions/awareness/awareness-provider.d.ts.map +1 -1
- package/dist/types/src/extensions/awareness/awareness.d.ts.map +1 -1
- package/dist/types/src/extensions/blast.d.ts.map +1 -1
- package/dist/types/src/extensions/command/action.d.ts +17 -0
- package/dist/types/src/extensions/command/action.d.ts.map +1 -0
- package/dist/types/src/extensions/command/command.d.ts +4 -10
- package/dist/types/src/extensions/command/command.d.ts.map +1 -1
- package/dist/types/src/extensions/command/hint.d.ts +18 -4
- package/dist/types/src/extensions/command/hint.d.ts.map +1 -1
- package/dist/types/src/extensions/command/index.d.ts +3 -0
- package/dist/types/src/extensions/command/index.d.ts.map +1 -1
- package/dist/types/src/extensions/command/menu.d.ts +6 -11
- package/dist/types/src/extensions/command/menu.d.ts.map +1 -1
- package/dist/types/src/extensions/command/state.d.ts +9 -11
- package/dist/types/src/extensions/command/state.d.ts.map +1 -1
- package/dist/types/src/extensions/command/typeahead.d.ts +17 -0
- package/dist/types/src/extensions/command/typeahead.d.ts.map +1 -0
- package/dist/types/src/extensions/comments.d.ts +9 -17
- package/dist/types/src/extensions/comments.d.ts.map +1 -1
- package/dist/types/src/extensions/debug.d.ts.map +1 -1
- package/dist/types/src/extensions/dnd.d.ts.map +1 -1
- package/dist/types/src/extensions/factories.d.ts +4 -0
- package/dist/types/src/extensions/factories.d.ts.map +1 -1
- package/dist/types/src/extensions/folding.d.ts.map +1 -1
- package/dist/types/src/extensions/index.d.ts +3 -0
- package/dist/types/src/extensions/index.d.ts.map +1 -1
- package/dist/types/src/extensions/json.d.ts +7 -0
- package/dist/types/src/extensions/json.d.ts.map +1 -0
- package/dist/types/src/extensions/listener.d.ts.map +1 -1
- package/dist/types/src/extensions/markdown/{editorAction.d.ts → action.d.ts} +1 -1
- package/dist/types/src/extensions/markdown/action.d.ts.map +1 -0
- package/dist/types/src/extensions/markdown/bundle.d.ts +2 -1
- package/dist/types/src/extensions/markdown/bundle.d.ts.map +1 -1
- package/dist/types/src/extensions/markdown/changes.d.ts.map +1 -1
- package/dist/types/src/extensions/markdown/debug.d.ts.map +1 -1
- package/dist/types/src/extensions/markdown/decorate.d.ts +5 -1
- package/dist/types/src/extensions/markdown/decorate.d.ts.map +1 -1
- package/dist/types/src/extensions/markdown/formatting.d.ts +3 -3
- package/dist/types/src/extensions/markdown/formatting.d.ts.map +1 -1
- package/dist/types/src/extensions/markdown/highlight.d.ts.map +1 -1
- package/dist/types/src/extensions/markdown/image.d.ts.map +1 -1
- package/dist/types/src/extensions/markdown/index.d.ts +1 -1
- package/dist/types/src/extensions/markdown/index.d.ts.map +1 -1
- package/dist/types/src/extensions/markdown/link.d.ts +4 -1
- package/dist/types/src/extensions/markdown/link.d.ts.map +1 -1
- package/dist/types/src/extensions/markdown/styles.d.ts.map +1 -1
- package/dist/types/src/extensions/markdown/table.d.ts.map +1 -1
- package/dist/types/src/extensions/mention.d.ts.map +1 -1
- package/dist/types/src/extensions/modes.d.ts +5 -5
- package/dist/types/src/extensions/modes.d.ts.map +1 -1
- package/dist/types/src/extensions/outliner/commands.d.ts +10 -0
- package/dist/types/src/extensions/outliner/commands.d.ts.map +1 -0
- package/dist/types/src/extensions/outliner/editor.d.ts +5 -0
- package/dist/types/src/extensions/outliner/editor.d.ts.map +1 -0
- package/dist/types/src/extensions/outliner/editor.test.d.ts +2 -0
- package/dist/types/src/extensions/outliner/editor.test.d.ts.map +1 -0
- package/dist/types/src/extensions/outliner/index.d.ts +4 -0
- package/dist/types/src/extensions/outliner/index.d.ts.map +1 -0
- package/dist/types/src/extensions/outliner/outliner.d.ts +13 -0
- package/dist/types/src/extensions/outliner/outliner.d.ts.map +1 -0
- package/dist/types/src/extensions/outliner/outliner.test.d.ts +2 -0
- package/dist/types/src/extensions/outliner/outliner.test.d.ts.map +1 -0
- package/dist/types/src/extensions/outliner/selection.d.ts +12 -0
- package/dist/types/src/extensions/outliner/selection.d.ts.map +1 -0
- package/dist/types/src/extensions/outliner/tree.d.ts +79 -0
- package/dist/types/src/extensions/outliner/tree.d.ts.map +1 -0
- package/dist/types/src/extensions/outliner/tree.test.d.ts +2 -0
- package/dist/types/src/extensions/outliner/tree.test.d.ts.map +1 -0
- package/dist/types/src/extensions/preview/index.d.ts +2 -0
- package/dist/types/src/extensions/preview/index.d.ts.map +1 -0
- package/dist/types/src/extensions/preview/preview.d.ts +39 -0
- package/dist/types/src/extensions/preview/preview.d.ts.map +1 -0
- package/dist/types/src/extensions/selection.d.ts.map +1 -1
- package/dist/types/src/extensions/typewriter.d.ts.map +1 -1
- package/dist/types/src/hooks/index.d.ts +0 -1
- package/dist/types/src/hooks/index.d.ts.map +1 -1
- package/dist/types/src/hooks/useTextEditor.d.ts +2 -1
- package/dist/types/src/hooks/useTextEditor.d.ts.map +1 -1
- package/dist/types/src/stories/Command.stories.d.ts +7 -0
- package/dist/types/src/stories/Command.stories.d.ts.map +1 -0
- package/dist/types/src/stories/Comments.stories.d.ts +13 -0
- package/dist/types/src/stories/Comments.stories.d.ts.map +1 -0
- package/dist/types/src/stories/EditorToolbar.stories.d.ts +12 -0
- package/dist/types/src/stories/EditorToolbar.stories.d.ts.map +1 -0
- package/dist/types/src/stories/Experimental.stories.d.ts +16 -0
- package/dist/types/src/stories/Experimental.stories.d.ts.map +1 -0
- package/dist/types/src/stories/Markdown.stories.d.ts +46 -0
- package/dist/types/src/stories/Markdown.stories.d.ts.map +1 -0
- package/dist/types/src/stories/Outliner.stories.d.ts +26 -0
- package/dist/types/src/stories/Outliner.stories.d.ts.map +1 -0
- package/dist/types/src/stories/Preview.stories.d.ts +10 -0
- package/dist/types/src/stories/Preview.stories.d.ts.map +1 -0
- package/dist/types/src/stories/TextEditor.stories.d.ts +55 -0
- package/dist/types/src/stories/TextEditor.stories.d.ts.map +1 -0
- package/dist/types/src/stories/util.d.ts +53 -0
- package/dist/types/src/stories/util.d.ts.map +1 -0
- package/dist/types/src/styles/theme.d.ts.map +1 -1
- package/dist/types/src/styles/tokens.d.ts.map +1 -1
- package/dist/types/src/testing/index.d.ts +2 -0
- package/dist/types/src/testing/index.d.ts.map +1 -0
- package/dist/types/src/testing/util.d.ts +2 -0
- package/dist/types/src/testing/util.d.ts.map +1 -0
- package/dist/types/src/types.d.ts +5 -0
- package/dist/types/src/types.d.ts.map +1 -1
- package/dist/types/src/util/cursor.d.ts.map +1 -1
- package/dist/types/src/util/debug.d.ts.map +1 -1
- package/dist/types/src/util/dom.d.ts.map +1 -1
- package/dist/types/src/util/facet.d.ts.map +1 -1
- package/dist/types/src/util/react.d.ts +6 -1
- package/dist/types/src/util/react.d.ts.map +1 -1
- package/dist/types/tsconfig.tsbuildinfo +1 -1
- package/package.json +46 -30
- package/src/components/EditorToolbar/EditorToolbar.tsx +95 -72
- package/src/components/EditorToolbar/blocks.ts +27 -6
- package/src/components/EditorToolbar/formatting.ts +34 -7
- package/src/components/EditorToolbar/headings.ts +9 -8
- package/src/components/EditorToolbar/image.ts +16 -0
- package/src/components/EditorToolbar/index.ts +7 -1
- package/src/components/EditorToolbar/lists.ts +26 -7
- package/src/components/EditorToolbar/search.ts +19 -0
- package/src/components/EditorToolbar/util.ts +19 -20
- package/src/components/EditorToolbar/{viewMode.ts → view-mode.ts} +9 -8
- package/src/components/Popover/RefDropdownMenu.tsx +77 -0
- package/src/components/Popover/RefPopover.tsx +75 -0
- package/src/components/Popover/index.ts +6 -0
- package/src/components/index.ts +1 -0
- package/src/defaults.ts +12 -13
- package/src/extensions/annotations.ts +41 -64
- package/src/extensions/autocomplete.ts +5 -6
- package/src/extensions/automerge/automerge.stories.tsx +13 -24
- package/src/extensions/automerge/automerge.test.tsx +6 -5
- package/src/extensions/automerge/automerge.ts +2 -2
- package/src/extensions/automerge/defs.ts +1 -2
- package/src/extensions/automerge/sync.ts +7 -7
- package/src/extensions/automerge/update-automerge.ts +1 -1
- package/src/extensions/automerge/update-codemirror.ts +3 -4
- package/src/extensions/awareness/awareness-provider.ts +4 -4
- package/src/extensions/awareness/awareness.ts +7 -7
- package/src/extensions/blast.ts +9 -9
- package/src/extensions/command/action.ts +49 -0
- package/src/extensions/command/command.ts +7 -27
- package/src/extensions/command/hint.ts +36 -33
- package/src/extensions/command/index.ts +3 -0
- package/src/extensions/command/menu.ts +79 -51
- package/src/extensions/command/state.ts +41 -61
- package/src/extensions/command/typeahead.ts +116 -0
- package/src/extensions/comments.ts +11 -76
- package/src/extensions/factories.ts +13 -0
- package/src/extensions/folding.tsx +1 -1
- package/src/extensions/index.ts +3 -0
- package/src/extensions/json.ts +56 -0
- package/src/extensions/markdown/bundle.ts +13 -9
- package/src/extensions/markdown/changes.ts +3 -2
- package/src/extensions/markdown/decorate.ts +19 -17
- package/src/extensions/markdown/formatting.ts +6 -6
- package/src/extensions/markdown/image.ts +14 -13
- package/src/extensions/markdown/index.ts +1 -1
- package/src/extensions/markdown/link.ts +33 -24
- package/src/extensions/markdown/styles.ts +4 -3
- package/src/extensions/markdown/table.ts +3 -3
- package/src/extensions/modes.ts +5 -6
- package/src/extensions/outliner/commands.ts +270 -0
- package/src/extensions/outliner/editor.test.ts +33 -0
- package/src/extensions/outliner/editor.ts +184 -0
- package/src/extensions/outliner/index.ts +7 -0
- package/src/extensions/outliner/outliner.test.ts +99 -0
- package/src/extensions/outliner/outliner.ts +168 -0
- package/src/extensions/outliner/selection.ts +50 -0
- package/src/extensions/outliner/tree.test.ts +164 -0
- package/src/extensions/outliner/tree.ts +315 -0
- package/src/extensions/preview/index.ts +5 -0
- package/src/extensions/preview/preview.ts +271 -0
- package/src/hooks/index.ts +0 -1
- package/src/hooks/useTextEditor.ts +4 -3
- package/src/stories/Command.stories.tsx +97 -0
- package/src/stories/Comments.stories.tsx +98 -0
- package/src/stories/EditorToolbar.stories.tsx +96 -0
- package/src/stories/Experimental.stories.tsx +86 -0
- package/src/stories/Markdown.stories.tsx +121 -0
- package/src/stories/Outliner.stories.tsx +108 -0
- package/src/stories/Preview.stories.tsx +149 -0
- package/src/stories/TextEditor.stories.tsx +256 -0
- package/src/stories/util.tsx +326 -0
- package/src/styles/theme.ts +15 -5
- package/src/styles/tokens.ts +1 -2
- package/src/testing/index.ts +5 -0
- package/src/testing/util.ts +5 -0
- package/src/types.ts +7 -0
- package/src/util/react.tsx +20 -2
- package/dist/types/src/InputMode.stories.d.ts +0 -57
- package/dist/types/src/InputMode.stories.d.ts.map +0 -1
- package/dist/types/src/TextEditor.stories.d.ts +0 -115
- package/dist/types/src/TextEditor.stories.d.ts.map +0 -1
- package/dist/types/src/components/EditorToolbar/comment.d.ts.map +0 -1
- package/dist/types/src/components/EditorToolbar/viewMode.d.ts.map +0 -1
- package/dist/types/src/extensions/command/preview.d.ts +0 -12
- package/dist/types/src/extensions/command/preview.d.ts.map +0 -1
- package/dist/types/src/extensions/markdown/editorAction.d.ts.map +0 -1
- package/dist/types/src/fragments.d.ts +0 -3
- package/dist/types/src/fragments.d.ts.map +0 -1
- package/dist/types/src/hooks/useActionHandler.d.ts +0 -4
- package/dist/types/src/hooks/useActionHandler.d.ts.map +0 -1
- package/src/InputMode.stories.tsx +0 -124
- package/src/TextEditor.stories.tsx +0 -856
- package/src/components/EditorToolbar/comment.ts +0 -23
- package/src/extensions/command/preview.ts +0 -79
- package/src/fragments.ts +0 -19
- package/src/hooks/useActionHandler.ts +0 -12
- /package/src/extensions/markdown/{editorAction.ts → action.ts} +0 -0
@@ -0,0 +1,99 @@
|
|
1
|
+
//
|
2
|
+
// Copyright 2025 DXOS.org
|
3
|
+
//
|
4
|
+
|
5
|
+
import { EditorSelection, EditorState } from '@codemirror/state';
|
6
|
+
import { EditorView } from '@codemirror/view';
|
7
|
+
import { describe, test } from 'vitest';
|
8
|
+
|
9
|
+
import { indentItemLess, indentItemMore, moveItemDown, moveItemUp } from './commands';
|
10
|
+
import { listItemToString, outlinerTree, treeFacet } from './tree';
|
11
|
+
import { str } from '../../testing';
|
12
|
+
import { createMarkdownExtensions } from '../markdown';
|
13
|
+
|
14
|
+
const lines = [
|
15
|
+
'- [ ] 1',
|
16
|
+
'- [ ] 2',
|
17
|
+
' - [ ] 2.1',
|
18
|
+
' - [ ] 2.2',
|
19
|
+
' - 2.2.1',
|
20
|
+
' - 2.2.2',
|
21
|
+
' - 2.2.3',
|
22
|
+
' - [ ] 2.3',
|
23
|
+
'- [ ] 3',
|
24
|
+
];
|
25
|
+
|
26
|
+
const getPos = (line: number) => {
|
27
|
+
return lines.slice(0, line).reduce((acc, line) => acc + line.length + 1, 0);
|
28
|
+
};
|
29
|
+
|
30
|
+
const extensions = [createMarkdownExtensions(), outlinerTree()];
|
31
|
+
|
32
|
+
// TODO(burdon): Flaky.
|
33
|
+
describe.runIf(!process.env.CI)('outliner', () => {
|
34
|
+
const state = EditorState.create({ doc: str(...lines), extensions });
|
35
|
+
|
36
|
+
test('sanity', ({ expect }) => {
|
37
|
+
const tree = state.facet(treeFacet);
|
38
|
+
let i = 0;
|
39
|
+
tree.traverse((item, level) => {
|
40
|
+
const pos = getPos(i++);
|
41
|
+
expect(item.lineRange.from).toBe(pos);
|
42
|
+
console.log(listItemToString(item, level), pos);
|
43
|
+
});
|
44
|
+
});
|
45
|
+
|
46
|
+
test('indent', ({ expect }) => {
|
47
|
+
const view = new EditorView({ state });
|
48
|
+
const pos = getPos(1);
|
49
|
+
|
50
|
+
{
|
51
|
+
const tree = view.state.facet(treeFacet);
|
52
|
+
const item = tree.find(pos);
|
53
|
+
expect(item?.level).toBe(0);
|
54
|
+
}
|
55
|
+
|
56
|
+
view.dispatch({ selection: EditorSelection.cursor(pos) });
|
57
|
+
indentItemMore(view);
|
58
|
+
|
59
|
+
{
|
60
|
+
const tree = view.state.facet(treeFacet);
|
61
|
+
const item = tree.find(pos);
|
62
|
+
expect(item?.level).toBe(1);
|
63
|
+
}
|
64
|
+
});
|
65
|
+
|
66
|
+
test('unindent', ({ expect }) => {
|
67
|
+
const view = new EditorView({ state });
|
68
|
+
const pos = getPos(2);
|
69
|
+
|
70
|
+
{
|
71
|
+
const tree = view.state.facet(treeFacet);
|
72
|
+
const item = tree.find(pos);
|
73
|
+
expect(item?.level).toBe(1);
|
74
|
+
}
|
75
|
+
|
76
|
+
view.dispatch({ selection: EditorSelection.cursor(pos) });
|
77
|
+
indentItemLess(view);
|
78
|
+
|
79
|
+
{
|
80
|
+
const tree = view.state.facet(treeFacet);
|
81
|
+
const item = tree.find(pos);
|
82
|
+
expect(item?.level).toBe(0);
|
83
|
+
}
|
84
|
+
});
|
85
|
+
|
86
|
+
test('move down', ({ expect }) => {
|
87
|
+
const view = new EditorView({ state });
|
88
|
+
view.dispatch({ selection: EditorSelection.cursor(getPos(0)) });
|
89
|
+
moveItemDown(view);
|
90
|
+
expect(view.state.doc.sliceString(0, view.state.doc.length)).toBe(str(...lines.slice(1, 8), lines[0], lines[8]));
|
91
|
+
});
|
92
|
+
|
93
|
+
test('move up', ({ expect }) => {
|
94
|
+
const view = new EditorView({ state });
|
95
|
+
view.dispatch({ selection: EditorSelection.cursor(getPos(8)) });
|
96
|
+
moveItemUp(view);
|
97
|
+
expect(view.state.doc.sliceString(0, view.state.doc.length)).toBe(str(lines[0], lines[8], ...lines.slice(1, 8)));
|
98
|
+
});
|
99
|
+
});
|
@@ -0,0 +1,168 @@
|
|
1
|
+
//
|
2
|
+
// Copyright 2025 DXOS.org
|
3
|
+
//
|
4
|
+
|
5
|
+
import { type EditorState, type Extension, Prec, type Range } from '@codemirror/state';
|
6
|
+
import { Decoration, type DecorationSet, EditorView, ViewPlugin, type ViewUpdate } from '@codemirror/view';
|
7
|
+
|
8
|
+
import { mx } from '@dxos/react-ui-theme';
|
9
|
+
|
10
|
+
import { commands } from './commands';
|
11
|
+
import { editor } from './editor';
|
12
|
+
import { selectionCompartment, selectionFacet, selectionEquals } from './selection';
|
13
|
+
import { outlinerTree, treeFacet } from './tree';
|
14
|
+
import { floatingMenu } from '../command';
|
15
|
+
import { decorateMarkdown } from '../markdown';
|
16
|
+
|
17
|
+
// ISSUES:
|
18
|
+
// TODO(burdon): Remove requirement for continuous lines to be indented (so that user's can't accidentally delete them and break the layout).
|
19
|
+
// TODO(burdon): Prevent unterminated fenced code from breaking subsequent items ("firewall" markdown parsing within each item?)
|
20
|
+
// TODO(burdon): What if a different editor "breaks" the layout?
|
21
|
+
// TODO(burdon): Check Automerge recognizes text that is moved/indented (e.g., concurrent editing item while being moved).
|
22
|
+
|
23
|
+
// NEXT:
|
24
|
+
// TODO(burdon): Update selection when adding/removing items.
|
25
|
+
// TODO(burdon): When selecting across items, select entire items (don't show selection that spans the gaps).
|
26
|
+
// TODO(burdon): Handle backspace at start of line (or empty line).
|
27
|
+
// TODO(burdon): Convert to task object and insert link (menu button).
|
28
|
+
// TODO(burdon): Smart Cut-and-paste.
|
29
|
+
// TODO(burdon): DND.
|
30
|
+
|
31
|
+
export type OutlinerProps = {
|
32
|
+
showSelected?: boolean;
|
33
|
+
};
|
34
|
+
|
35
|
+
/**
|
36
|
+
* Outliner extension.
|
37
|
+
* - Stores outline as a standard markdown document with task and list markers.
|
38
|
+
* - Supports continuation lines and rich formatting (with Shift+Enter).
|
39
|
+
* - Constrains editor to outline structure.
|
40
|
+
* - Supports smart cut-and-paste.
|
41
|
+
*/
|
42
|
+
export const outliner = (options: OutlinerProps = {}): Extension => [
|
43
|
+
// Commands.
|
44
|
+
Prec.highest(commands()),
|
45
|
+
|
46
|
+
// Selection.
|
47
|
+
selectionCompartment.of(selectionFacet.of([])),
|
48
|
+
|
49
|
+
// State.
|
50
|
+
outlinerTree(),
|
51
|
+
|
52
|
+
// Filter and possibly modify changes.
|
53
|
+
editor(),
|
54
|
+
|
55
|
+
// Floating menu.
|
56
|
+
floatingMenu(),
|
57
|
+
|
58
|
+
// Line decorations.
|
59
|
+
decorations(options),
|
60
|
+
|
61
|
+
// Default markdown decorations.
|
62
|
+
decorateMarkdown({ listPaddingLeft: 8 }),
|
63
|
+
|
64
|
+
// Researve space for menu.
|
65
|
+
EditorView.contentAttributes.of({ class: 'is-full !mr-[3rem]' }),
|
66
|
+
];
|
67
|
+
|
68
|
+
/**
|
69
|
+
* Line decorations (for border and selection).
|
70
|
+
*/
|
71
|
+
const decorations = (options: OutlinerProps) => [
|
72
|
+
ViewPlugin.fromClass(
|
73
|
+
class {
|
74
|
+
decorations: DecorationSet = Decoration.none;
|
75
|
+
constructor(view: EditorView) {
|
76
|
+
this.updateDecorations(view.state, view);
|
77
|
+
}
|
78
|
+
|
79
|
+
update(update: ViewUpdate) {
|
80
|
+
const selectionChanged = !selectionEquals(
|
81
|
+
update.state.facet(selectionFacet),
|
82
|
+
update.startState.facet(selectionFacet),
|
83
|
+
);
|
84
|
+
|
85
|
+
if (
|
86
|
+
update.focusChanged ||
|
87
|
+
update.docChanged ||
|
88
|
+
update.viewportChanged ||
|
89
|
+
update.selectionSet ||
|
90
|
+
selectionChanged
|
91
|
+
) {
|
92
|
+
this.updateDecorations(update.state, update.view);
|
93
|
+
}
|
94
|
+
}
|
95
|
+
|
96
|
+
private updateDecorations(state: EditorState, { viewport: { from, to }, hasFocus }: EditorView) {
|
97
|
+
const selection = state.facet(selectionFacet);
|
98
|
+
const tree = state.facet(treeFacet);
|
99
|
+
const current = tree.find(state.selection.ranges[state.selection.mainIndex]?.from);
|
100
|
+
const doc = state.doc;
|
101
|
+
|
102
|
+
const decorations: Range<Decoration>[] = [];
|
103
|
+
for (let lineNum = doc.lineAt(from).number; lineNum <= doc.lineAt(to).number; lineNum++) {
|
104
|
+
const line = doc.line(lineNum);
|
105
|
+
const item = tree.find(line.from);
|
106
|
+
if (item) {
|
107
|
+
const lineFrom = doc.lineAt(item.contentRange.from);
|
108
|
+
const lineTo = doc.lineAt(item.contentRange.to);
|
109
|
+
const isSelected = selection.includes(item.index) || item === current;
|
110
|
+
decorations.push(
|
111
|
+
Decoration.line({
|
112
|
+
class: mx(
|
113
|
+
'cm-list-item',
|
114
|
+
lineFrom.number === line.number && 'cm-list-item-start',
|
115
|
+
lineTo.number === line.number && 'cm-list-item-end',
|
116
|
+
isSelected && (hasFocus ? 'cm-list-item-focused' : 'cm-list-item-selected'),
|
117
|
+
),
|
118
|
+
}).range(line.from, line.from),
|
119
|
+
);
|
120
|
+
}
|
121
|
+
}
|
122
|
+
|
123
|
+
this.decorations = Decoration.set(decorations);
|
124
|
+
}
|
125
|
+
},
|
126
|
+
{
|
127
|
+
decorations: (v) => v.decorations,
|
128
|
+
},
|
129
|
+
),
|
130
|
+
|
131
|
+
// Theme.
|
132
|
+
EditorView.theme(
|
133
|
+
Object.assign({
|
134
|
+
'.cm-list-item': {
|
135
|
+
borderLeftWidth: '1px',
|
136
|
+
borderRightWidth: '1px',
|
137
|
+
paddingLeft: '32px',
|
138
|
+
borderColor: 'transparent',
|
139
|
+
},
|
140
|
+
'.cm-list-item.cm-codeblock-start': {
|
141
|
+
borderRadius: '0',
|
142
|
+
},
|
143
|
+
|
144
|
+
'.cm-list-item-start': {
|
145
|
+
borderTopWidth: '1px',
|
146
|
+
borderTopLeftRadius: '4px',
|
147
|
+
borderTopRightRadius: '4px',
|
148
|
+
paddingTop: '4px',
|
149
|
+
marginTop: '2px',
|
150
|
+
},
|
151
|
+
|
152
|
+
'.cm-list-item-end': {
|
153
|
+
borderBottomWidth: '1px',
|
154
|
+
borderBottomLeftRadius: '4px',
|
155
|
+
borderBottomRightRadius: '4px',
|
156
|
+
paddingBottom: '4px',
|
157
|
+
marginBottom: '2px',
|
158
|
+
},
|
159
|
+
|
160
|
+
'.cm-list-item-selected': {
|
161
|
+
borderColor: options.showSelected ? 'var(--dx-separator)' : undefined,
|
162
|
+
},
|
163
|
+
'.cm-list-item-focused': {
|
164
|
+
borderColor: 'var(--dx-accentFocusIndicator)',
|
165
|
+
},
|
166
|
+
}),
|
167
|
+
),
|
168
|
+
];
|
@@ -0,0 +1,50 @@
|
|
1
|
+
//
|
2
|
+
// Copyright 2025 DXOS.org
|
3
|
+
//
|
4
|
+
|
5
|
+
import { Compartment, type EditorState, Facet, type SelectionRange } from '@codemirror/state';
|
6
|
+
import { type EditorView, type Command } from '@codemirror/view';
|
7
|
+
|
8
|
+
import { treeFacet } from './tree';
|
9
|
+
|
10
|
+
export type Selection = number[];
|
11
|
+
|
12
|
+
export const getSelection = (state: EditorState): SelectionRange => state.selection.main;
|
13
|
+
|
14
|
+
export const selectionEquals = (a: number[], b: number[]) => a.length === b.length && a.every((i) => b.includes(i));
|
15
|
+
|
16
|
+
export const selectionFacet = Facet.define<Selection, Selection>({
|
17
|
+
combine: (values) => values[0],
|
18
|
+
});
|
19
|
+
|
20
|
+
export const selectionCompartment = new Compartment();
|
21
|
+
|
22
|
+
export const selectNone: Command = (view: EditorView) => {
|
23
|
+
view.dispatch({
|
24
|
+
effects: selectionCompartment.reconfigure(selectionFacet.of([])),
|
25
|
+
});
|
26
|
+
|
27
|
+
return true;
|
28
|
+
};
|
29
|
+
|
30
|
+
export const selectAll: Command = (view: EditorView) => {
|
31
|
+
const tree = view.state.facet(treeFacet);
|
32
|
+
const selection = view.state.facet(selectionFacet);
|
33
|
+
const items: Selection = [];
|
34
|
+
tree.traverse((item) => items.push(item.index));
|
35
|
+
view.dispatch({
|
36
|
+
effects: selectionCompartment.reconfigure(selectionFacet.of(selectionEquals(selection, items) ? [] : items)),
|
37
|
+
});
|
38
|
+
|
39
|
+
return true;
|
40
|
+
};
|
41
|
+
|
42
|
+
// TODO(burdon): Implement.
|
43
|
+
export const selectUp: Command = (view: EditorView) => {
|
44
|
+
return true;
|
45
|
+
};
|
46
|
+
|
47
|
+
// TODO(burdon): Implement.
|
48
|
+
export const selectDown: Command = (view: EditorView) => {
|
49
|
+
return true;
|
50
|
+
};
|
@@ -0,0 +1,164 @@
|
|
1
|
+
//
|
2
|
+
// Copyright 2025 DXOS.org
|
3
|
+
//
|
4
|
+
|
5
|
+
import { markdown, markdownLanguage } from '@codemirror/lang-markdown';
|
6
|
+
import { EditorState } from '@codemirror/state';
|
7
|
+
import { describe, test } from 'vitest';
|
8
|
+
|
9
|
+
import { outlinerTree, treeFacet, listItemToString, type Item } from './tree';
|
10
|
+
import { str } from '../../testing';
|
11
|
+
import { type Range } from '../../types';
|
12
|
+
|
13
|
+
const lines = [
|
14
|
+
'- [ ] 1',
|
15
|
+
'- [ ] 2',
|
16
|
+
' - [ ] 2.1',
|
17
|
+
' - [ ] 2.2',
|
18
|
+
' - 2.2.1',
|
19
|
+
' - 2.2.2',
|
20
|
+
' - 2.2.3',
|
21
|
+
' - [ ] 2.3',
|
22
|
+
'- [ ] 3',
|
23
|
+
];
|
24
|
+
|
25
|
+
const getPos = (line: number) => {
|
26
|
+
return lines.slice(0, line).reduce((acc, line) => acc + line.length + 1, 0);
|
27
|
+
};
|
28
|
+
|
29
|
+
const extensions = [markdown({ base: markdownLanguage }), outlinerTree()];
|
30
|
+
|
31
|
+
describe('tree (boundary conditions)', () => {
|
32
|
+
test('empty', ({ expect }) => {
|
33
|
+
const state = EditorState.create({ doc: str(''), extensions });
|
34
|
+
const tree = state.facet(treeFacet);
|
35
|
+
expect(tree).to.exist;
|
36
|
+
});
|
37
|
+
|
38
|
+
test('content range', ({ expect }) => {
|
39
|
+
const state = EditorState.create({ doc: '- [ ] A', extensions });
|
40
|
+
const tree = state.facet(treeFacet);
|
41
|
+
console.log(JSON.stringify(tree, null, 2));
|
42
|
+
expect(tree.toJSON()).to.deep.eq({
|
43
|
+
type: 'root',
|
44
|
+
index: -1,
|
45
|
+
level: -1,
|
46
|
+
lineRange: { from: 0, to: -1 },
|
47
|
+
contentRange: { from: 0, to: -1 },
|
48
|
+
children: [
|
49
|
+
{
|
50
|
+
type: 'task',
|
51
|
+
index: 0,
|
52
|
+
level: 0,
|
53
|
+
lineRange: { from: 0, to: 7 },
|
54
|
+
contentRange: { from: 6, to: 7 },
|
55
|
+
children: [],
|
56
|
+
},
|
57
|
+
],
|
58
|
+
});
|
59
|
+
|
60
|
+
const item = tree.find(0);
|
61
|
+
expect(item?.contentRange).to.include({ from: 6, to: state.doc.length });
|
62
|
+
});
|
63
|
+
|
64
|
+
test('empty continuation', ({ expect }) => {
|
65
|
+
const state = EditorState.create({ doc: str('- [ ] A', ' '), extensions });
|
66
|
+
const tree = state.facet(treeFacet);
|
67
|
+
tree.traverse((item, level) => {
|
68
|
+
console.log(listItemToString(item, level));
|
69
|
+
});
|
70
|
+
});
|
71
|
+
});
|
72
|
+
|
73
|
+
describe('tree (advanced)', () => {
|
74
|
+
const state = EditorState.create({ doc: str(...lines), extensions });
|
75
|
+
|
76
|
+
test('traverse', ({ expect }) => {
|
77
|
+
const tree = state.facet(treeFacet);
|
78
|
+
let count = 0;
|
79
|
+
tree.traverse((item, level) => {
|
80
|
+
console.log(listItemToString(item, level));
|
81
|
+
count++;
|
82
|
+
});
|
83
|
+
expect(count).toBe(9);
|
84
|
+
});
|
85
|
+
|
86
|
+
test('continguous', ({ expect }) => {
|
87
|
+
const tree = state.facet(treeFacet);
|
88
|
+
const ranges: Range[] = [];
|
89
|
+
tree.traverse((item) => {
|
90
|
+
ranges.push(item.lineRange);
|
91
|
+
console.log(listItemToString(item));
|
92
|
+
});
|
93
|
+
|
94
|
+
// Check no gaps between ranges.
|
95
|
+
expect(ranges[0].from).toBe(0);
|
96
|
+
expect(ranges[ranges.length - 1].to).toBe(state.doc.length);
|
97
|
+
for (let i = 0; i < ranges.length - 1; i++) {
|
98
|
+
const current = ranges[i];
|
99
|
+
const next = ranges[i + 1];
|
100
|
+
expect(current.to + 1).toBe(next.from);
|
101
|
+
}
|
102
|
+
});
|
103
|
+
|
104
|
+
test('find', ({ expect }) => {
|
105
|
+
const tree = state.facet(treeFacet);
|
106
|
+
|
107
|
+
expect(tree.find(0)).to.include({ type: 'task' });
|
108
|
+
expect(tree.find(state.doc.length)).to.include({ type: 'task' });
|
109
|
+
|
110
|
+
expect(tree.find(getPos(1))).to.include({ type: 'task' });
|
111
|
+
expect(tree.find(getPos(1))).toBe(tree.find(getPos(1) + 4));
|
112
|
+
expect(tree.find(getPos(5))).to.include({ type: 'bullet' });
|
113
|
+
});
|
114
|
+
|
115
|
+
test('siblings', ({ expect }) => {
|
116
|
+
const tree = state.facet(treeFacet);
|
117
|
+
const items: Item[] = [];
|
118
|
+
tree.traverse((item) => {
|
119
|
+
items.push(item);
|
120
|
+
});
|
121
|
+
|
122
|
+
expect(items[0].nextSibling).toBe(items[1]);
|
123
|
+
expect(items[1].prevSibling).toBe(items[0]);
|
124
|
+
|
125
|
+
expect(items[1].nextSibling).toBe(items[8]);
|
126
|
+
expect(items[8].prevSibling).toBe(items[1]);
|
127
|
+
});
|
128
|
+
|
129
|
+
test('next/previous', ({ expect }) => {
|
130
|
+
const tree = state.facet(treeFacet);
|
131
|
+
const items: Item[] = [];
|
132
|
+
tree.traverse((item) => {
|
133
|
+
items.push(item);
|
134
|
+
});
|
135
|
+
|
136
|
+
expect(tree.prev(items[0])).not.to.exist;
|
137
|
+
expect(tree.next(items[items.length - 1])).not.to.exist;
|
138
|
+
|
139
|
+
for (let i = 0; i < items.length - 1; i++) {
|
140
|
+
const current = items[i];
|
141
|
+
const next = items[i + 1];
|
142
|
+
expect(tree.next(current)?.index).toEqual(next.index);
|
143
|
+
expect(tree.prev(next)?.index).toEqual(current.index);
|
144
|
+
}
|
145
|
+
});
|
146
|
+
|
147
|
+
test('lastDescendant', ({ expect }) => {
|
148
|
+
const tree = state.facet(treeFacet);
|
149
|
+
{
|
150
|
+
const item = tree.find(getPos(0))!;
|
151
|
+
expect(tree.lastDescendant(item).index).toBe(item.index);
|
152
|
+
}
|
153
|
+
{
|
154
|
+
const item = tree.find(getPos(1))!;
|
155
|
+
const last = tree.find(getPos(7))!;
|
156
|
+
expect(tree.lastDescendant(item).index).toBe(last.index);
|
157
|
+
}
|
158
|
+
{
|
159
|
+
const item = tree.find(getPos(3))!;
|
160
|
+
const last = tree.find(getPos(6))!;
|
161
|
+
expect(tree.lastDescendant(item).index).toBe(last.index);
|
162
|
+
}
|
163
|
+
});
|
164
|
+
});
|