@dxos/react-ui-editor 0.8.2-main.fbd8ed0 → 0.8.2-staging.4d6ad0f

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.
Files changed (180) hide show
  1. package/dist/lib/browser/index.mjs +1731 -926
  2. package/dist/lib/browser/index.mjs.map +4 -4
  3. package/dist/lib/browser/meta.json +1 -1
  4. package/dist/lib/browser/testing/index.mjs +3 -64
  5. package/dist/lib/browser/testing/index.mjs.map +4 -4
  6. package/dist/lib/node/index.cjs +1912 -1111
  7. package/dist/lib/node/index.cjs.map +4 -4
  8. package/dist/lib/node/meta.json +1 -1
  9. package/dist/lib/node/testing/index.cjs +3 -75
  10. package/dist/lib/node/testing/index.cjs.map +4 -4
  11. package/dist/lib/node-esm/index.mjs +1731 -926
  12. package/dist/lib/node-esm/index.mjs.map +4 -4
  13. package/dist/lib/node-esm/meta.json +1 -1
  14. package/dist/lib/node-esm/testing/index.mjs +3 -64
  15. package/dist/lib/node-esm/testing/index.mjs.map +4 -4
  16. package/dist/types/src/components/EditorToolbar/EditorToolbar.d.ts.map +1 -1
  17. package/dist/types/src/components/EditorToolbar/index.d.ts +1 -1
  18. package/dist/types/src/components/EditorToolbar/index.d.ts.map +1 -1
  19. package/dist/types/src/components/EditorToolbar/util.d.ts +4 -6
  20. package/dist/types/src/components/EditorToolbar/util.d.ts.map +1 -1
  21. package/dist/types/src/components/Popover/RefDropdownMenu.d.ts +21 -0
  22. package/dist/types/src/components/Popover/RefDropdownMenu.d.ts.map +1 -0
  23. package/dist/types/src/{testing → components/Popover}/RefPopover.d.ts +1 -1
  24. package/dist/types/src/components/Popover/RefPopover.d.ts.map +1 -0
  25. package/dist/types/src/components/Popover/index.d.ts +3 -0
  26. package/dist/types/src/components/Popover/index.d.ts.map +1 -0
  27. package/dist/types/src/components/index.d.ts +1 -0
  28. package/dist/types/src/components/index.d.ts.map +1 -1
  29. package/dist/types/src/defaults.d.ts +2 -5
  30. package/dist/types/src/defaults.d.ts.map +1 -1
  31. package/dist/types/src/extensions/annotations.d.ts +4 -1
  32. package/dist/types/src/extensions/annotations.d.ts.map +1 -1
  33. package/dist/types/src/extensions/autocomplete.d.ts +1 -2
  34. package/dist/types/src/extensions/autocomplete.d.ts.map +1 -1
  35. package/dist/types/src/extensions/automerge/automerge.stories.d.ts.map +1 -1
  36. package/dist/types/src/extensions/automerge/sync.d.ts.map +1 -1
  37. package/dist/types/src/extensions/awareness/awareness-provider.d.ts.map +1 -1
  38. package/dist/types/src/extensions/awareness/awareness.d.ts.map +1 -1
  39. package/dist/types/src/extensions/command/command.d.ts +1 -2
  40. package/dist/types/src/extensions/command/command.d.ts.map +1 -1
  41. package/dist/types/src/extensions/command/hint.d.ts +14 -2
  42. package/dist/types/src/extensions/command/hint.d.ts.map +1 -1
  43. package/dist/types/src/extensions/command/index.d.ts +2 -0
  44. package/dist/types/src/extensions/command/index.d.ts.map +1 -1
  45. package/dist/types/src/extensions/command/menu.d.ts +7 -8
  46. package/dist/types/src/extensions/command/menu.d.ts.map +1 -1
  47. package/dist/types/src/extensions/command/state.d.ts +1 -1
  48. package/dist/types/src/extensions/command/state.d.ts.map +1 -1
  49. package/dist/types/src/extensions/command/typeahead.d.ts +17 -0
  50. package/dist/types/src/extensions/command/typeahead.d.ts.map +1 -0
  51. package/dist/types/src/extensions/comments.d.ts +2 -12
  52. package/dist/types/src/extensions/comments.d.ts.map +1 -1
  53. package/dist/types/src/extensions/factories.d.ts +4 -0
  54. package/dist/types/src/extensions/factories.d.ts.map +1 -1
  55. package/dist/types/src/extensions/index.d.ts +2 -0
  56. package/dist/types/src/extensions/index.d.ts.map +1 -1
  57. package/dist/types/src/extensions/json.d.ts +7 -0
  58. package/dist/types/src/extensions/json.d.ts.map +1 -0
  59. package/dist/types/src/extensions/markdown/{editorAction.d.ts → action.d.ts} +1 -1
  60. package/dist/types/src/extensions/markdown/action.d.ts.map +1 -0
  61. package/dist/types/src/extensions/markdown/bundle.d.ts +2 -1
  62. package/dist/types/src/extensions/markdown/bundle.d.ts.map +1 -1
  63. package/dist/types/src/extensions/markdown/index.d.ts +1 -2
  64. package/dist/types/src/extensions/markdown/index.d.ts.map +1 -1
  65. package/dist/types/src/extensions/markdown/styles.d.ts.map +1 -1
  66. package/dist/types/src/extensions/outliner/commands.d.ts +9 -0
  67. package/dist/types/src/extensions/outliner/commands.d.ts.map +1 -0
  68. package/dist/types/src/extensions/outliner/editor.d.ts +5 -0
  69. package/dist/types/src/extensions/outliner/editor.d.ts.map +1 -0
  70. package/dist/types/src/extensions/outliner/editor.test.d.ts +2 -0
  71. package/dist/types/src/extensions/outliner/editor.test.d.ts.map +1 -0
  72. package/dist/types/src/extensions/outliner/index.d.ts +3 -0
  73. package/dist/types/src/extensions/outliner/index.d.ts.map +1 -0
  74. package/dist/types/src/extensions/outliner/outliner.d.ts +10 -0
  75. package/dist/types/src/extensions/outliner/outliner.d.ts.map +1 -0
  76. package/dist/types/src/extensions/outliner/outliner.test.d.ts +2 -0
  77. package/dist/types/src/extensions/outliner/outliner.test.d.ts.map +1 -0
  78. package/dist/types/src/extensions/outliner/selection.d.ts +12 -0
  79. package/dist/types/src/extensions/outliner/selection.d.ts.map +1 -0
  80. package/dist/types/src/extensions/outliner/tree.d.ts +79 -0
  81. package/dist/types/src/extensions/outliner/tree.d.ts.map +1 -0
  82. package/dist/types/src/extensions/outliner/tree.test.d.ts +2 -0
  83. package/dist/types/src/extensions/outliner/tree.test.d.ts.map +1 -0
  84. package/dist/types/src/stories/Command.stories.d.ts +7 -0
  85. package/dist/types/src/stories/Command.stories.d.ts.map +1 -0
  86. package/dist/types/src/stories/{TextEditorComments.stories.d.ts → Comments.stories.d.ts} +3 -3
  87. package/dist/types/src/stories/Comments.stories.d.ts.map +1 -0
  88. package/dist/types/src/stories/EditorToolbar.stories.d.ts +12 -0
  89. package/dist/types/src/stories/EditorToolbar.stories.d.ts.map +1 -0
  90. package/dist/types/src/stories/{TextEditorSpecial.stories.d.ts → Experimental.stories.d.ts} +3 -6
  91. package/dist/types/src/stories/Experimental.stories.d.ts.map +1 -0
  92. package/dist/types/src/stories/Markdown.stories.d.ts +46 -0
  93. package/dist/types/src/stories/Markdown.stories.d.ts.map +1 -0
  94. package/dist/types/src/stories/Outliner.stories.d.ts +26 -0
  95. package/dist/types/src/stories/Outliner.stories.d.ts.map +1 -0
  96. package/dist/types/src/stories/Preview.stories.d.ts +10 -0
  97. package/dist/types/src/stories/Preview.stories.d.ts.map +1 -0
  98. package/dist/types/src/stories/{TextEditorBasic.stories.d.ts → TextEditor.stories.d.ts} +9 -39
  99. package/dist/types/src/stories/TextEditor.stories.d.ts.map +1 -0
  100. package/dist/types/src/stories/{story-utils.d.ts → util.d.ts} +6 -6
  101. package/dist/types/src/stories/util.d.ts.map +1 -0
  102. package/dist/types/src/styles/theme.d.ts.map +1 -1
  103. package/dist/types/src/styles/tokens.d.ts.map +1 -1
  104. package/dist/types/src/testing/index.d.ts +1 -1
  105. package/dist/types/src/testing/index.d.ts.map +1 -1
  106. package/dist/types/src/testing/util.d.ts +2 -0
  107. package/dist/types/src/testing/util.d.ts.map +1 -0
  108. package/package.json +40 -34
  109. package/src/components/EditorToolbar/EditorToolbar.tsx +81 -57
  110. package/src/components/EditorToolbar/index.ts +7 -1
  111. package/src/components/EditorToolbar/util.ts +3 -4
  112. package/src/components/Popover/RefDropdownMenu.tsx +77 -0
  113. package/src/{testing → components/Popover}/RefPopover.tsx +5 -4
  114. package/src/components/Popover/index.ts +6 -0
  115. package/src/components/index.ts +1 -0
  116. package/src/defaults.ts +10 -13
  117. package/src/extensions/annotations.ts +41 -64
  118. package/src/extensions/autocomplete.ts +5 -6
  119. package/src/extensions/automerge/automerge.stories.tsx +2 -7
  120. package/src/extensions/automerge/automerge.test.tsx +3 -2
  121. package/src/extensions/automerge/sync.ts +3 -3
  122. package/src/extensions/awareness/awareness-provider.ts +4 -4
  123. package/src/extensions/awareness/awareness.ts +7 -7
  124. package/src/extensions/blast.ts +9 -9
  125. package/src/extensions/command/command.ts +1 -3
  126. package/src/extensions/command/hint.ts +7 -7
  127. package/src/extensions/command/index.ts +2 -0
  128. package/src/extensions/command/menu.ts +43 -49
  129. package/src/extensions/command/typeahead.ts +116 -0
  130. package/src/extensions/comments.ts +4 -69
  131. package/src/extensions/factories.ts +13 -0
  132. package/src/extensions/index.ts +2 -0
  133. package/src/extensions/json.ts +56 -0
  134. package/src/extensions/markdown/bundle.ts +13 -9
  135. package/src/extensions/markdown/decorate.ts +7 -7
  136. package/src/extensions/markdown/image.ts +2 -2
  137. package/src/extensions/markdown/index.ts +1 -2
  138. package/src/extensions/markdown/styles.ts +2 -1
  139. package/src/extensions/markdown/table.ts +3 -3
  140. package/src/extensions/outliner/commands.ts +242 -0
  141. package/src/extensions/outliner/editor.test.ts +33 -0
  142. package/src/extensions/outliner/editor.ts +180 -0
  143. package/src/extensions/outliner/index.ts +6 -0
  144. package/src/extensions/outliner/outliner.test.ts +99 -0
  145. package/src/extensions/outliner/outliner.ts +162 -0
  146. package/src/extensions/outliner/selection.ts +50 -0
  147. package/src/extensions/outliner/tree.test.ts +164 -0
  148. package/src/extensions/outliner/tree.ts +315 -0
  149. package/src/extensions/preview/preview.ts +5 -5
  150. package/src/stories/Command.stories.tsx +97 -0
  151. package/src/stories/{TextEditorComments.stories.tsx → Comments.stories.tsx} +13 -14
  152. package/src/{components/EditorToolbar → stories}/EditorToolbar.stories.tsx +26 -20
  153. package/src/stories/{TextEditorSpecial.stories.tsx → Experimental.stories.tsx} +9 -30
  154. package/src/stories/Markdown.stories.tsx +121 -0
  155. package/src/stories/Outliner.stories.tsx +108 -0
  156. package/src/stories/{TextEditorPreview.stories.tsx → Preview.stories.tsx} +46 -136
  157. package/src/stories/TextEditor.stories.tsx +256 -0
  158. package/src/stories/{story-utils.tsx → util.tsx} +21 -22
  159. package/src/styles/theme.ts +12 -5
  160. package/src/styles/tokens.ts +1 -2
  161. package/src/testing/index.ts +1 -1
  162. package/src/testing/util.ts +5 -0
  163. package/dist/types/src/components/EditorToolbar/EditorToolbar.stories.d.ts +0 -53
  164. package/dist/types/src/components/EditorToolbar/EditorToolbar.stories.d.ts.map +0 -1
  165. package/dist/types/src/components/EditorToolbar/comment.d.ts +0 -18
  166. package/dist/types/src/components/EditorToolbar/comment.d.ts.map +0 -1
  167. package/dist/types/src/extensions/markdown/editorAction.d.ts.map +0 -1
  168. package/dist/types/src/extensions/markdown/outliner.d.ts +0 -12
  169. package/dist/types/src/extensions/markdown/outliner.d.ts.map +0 -1
  170. package/dist/types/src/stories/TextEditorBasic.stories.d.ts.map +0 -1
  171. package/dist/types/src/stories/TextEditorComments.stories.d.ts.map +0 -1
  172. package/dist/types/src/stories/TextEditorPreview.stories.d.ts +0 -13
  173. package/dist/types/src/stories/TextEditorPreview.stories.d.ts.map +0 -1
  174. package/dist/types/src/stories/TextEditorSpecial.stories.d.ts.map +0 -1
  175. package/dist/types/src/stories/story-utils.d.ts.map +0 -1
  176. package/dist/types/src/testing/RefPopover.d.ts.map +0 -1
  177. package/src/components/EditorToolbar/comment.ts +0 -30
  178. package/src/extensions/markdown/outliner.ts +0 -235
  179. package/src/stories/TextEditorBasic.stories.tsx +0 -333
  180. /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
+ // Flaky
33
+ describe.skip('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,162 @@
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
+ /**
32
+ * Outliner extension.
33
+ * - Stores outline as a standard markdown document with task and list markers.
34
+ * - Supports continuation lines and rich formatting (with Shift+Enter).
35
+ * - Constrains editor to outline structure.
36
+ * - Supports smart cut-and-paste.
37
+ */
38
+ export const outliner = (): Extension => [
39
+ // Commands.
40
+ Prec.highest(commands()),
41
+
42
+ // Selection.
43
+ selectionCompartment.of(selectionFacet.of([])),
44
+
45
+ // State.
46
+ outlinerTree(),
47
+
48
+ // Filter and possibly modify changes.
49
+ editor(),
50
+
51
+ // Floating menu.
52
+ floatingMenu(),
53
+
54
+ // Line decorations.
55
+ decorations(),
56
+
57
+ // Default markdown decorations.
58
+ decorateMarkdown({ listPaddingLeft: 8 }),
59
+
60
+ // Researve space for menu.
61
+ EditorView.contentAttributes.of({ class: 'is-full !mr-[3rem]' }),
62
+ ];
63
+
64
+ /**
65
+ * Line decorations (for border and selection).
66
+ */
67
+ const decorations = () => [
68
+ ViewPlugin.fromClass(
69
+ class {
70
+ decorations: DecorationSet = Decoration.none;
71
+ constructor(view: EditorView) {
72
+ this.updateDecorations(view.state, view);
73
+ }
74
+
75
+ update(update: ViewUpdate) {
76
+ const selectionChanged = !selectionEquals(
77
+ update.state.facet(selectionFacet),
78
+ update.startState.facet(selectionFacet),
79
+ );
80
+
81
+ if (
82
+ update.focusChanged ||
83
+ update.docChanged ||
84
+ update.viewportChanged ||
85
+ update.selectionSet ||
86
+ selectionChanged
87
+ ) {
88
+ this.updateDecorations(update.state, update.view);
89
+ }
90
+ }
91
+
92
+ private updateDecorations(state: EditorState, { viewport: { from, to }, hasFocus }: EditorView) {
93
+ const selection = state.facet(selectionFacet);
94
+ const tree = state.facet(treeFacet);
95
+ const current = tree.find(state.selection.ranges[state.selection.mainIndex]?.from);
96
+ const doc = state.doc;
97
+
98
+ const decorations: Range<Decoration>[] = [];
99
+ for (let lineNum = doc.lineAt(from).number; lineNum <= doc.lineAt(to).number; lineNum++) {
100
+ const line = doc.line(lineNum);
101
+ const item = tree.find(line.from);
102
+ if (item) {
103
+ const lineFrom = doc.lineAt(item.contentRange.from);
104
+ const lineTo = doc.lineAt(item.contentRange.to);
105
+ const isSelected = selection.includes(item.index) || item === current;
106
+ decorations.push(
107
+ Decoration.line({
108
+ class: mx(
109
+ 'cm-list-item',
110
+ lineFrom.number === line.number && 'cm-list-item-start',
111
+ lineTo.number === line.number && 'cm-list-item-end',
112
+ isSelected && (hasFocus ? 'cm-list-item-focused' : 'cm-list-item-selected'),
113
+ ),
114
+ }).range(line.from, line.from),
115
+ );
116
+ }
117
+ }
118
+
119
+ this.decorations = Decoration.set(decorations);
120
+ }
121
+ },
122
+ {
123
+ decorations: (v) => v.decorations,
124
+ },
125
+ ),
126
+
127
+ // Theme.
128
+ EditorView.theme({
129
+ '.cm-list-item': {
130
+ borderLeftWidth: '1px',
131
+ borderRightWidth: '1px',
132
+ paddingLeft: '32px',
133
+ borderColor: 'transparent',
134
+ },
135
+ '.cm-list-item.cm-codeblock-start': {
136
+ borderRadius: '0',
137
+ },
138
+
139
+ '.cm-list-item-start': {
140
+ borderTopWidth: '1px',
141
+ borderTopLeftRadius: '4px',
142
+ borderTopRightRadius: '4px',
143
+ paddingTop: '4px',
144
+ marginTop: '8px',
145
+ },
146
+
147
+ '.cm-list-item-end': {
148
+ borderBottomWidth: '1px',
149
+ borderBottomLeftRadius: '4px',
150
+ borderBottomRightRadius: '4px',
151
+ paddingBottom: '4px',
152
+ marginBottom: '8px',
153
+ },
154
+
155
+ '.cm-list-item-selected': {
156
+ borderColor: 'var(--dx-separator)',
157
+ },
158
+ '.cm-list-item-focused': {
159
+ borderColor: 'var(--dx-accentFocusIndicator)',
160
+ },
161
+ }),
162
+ ];
@@ -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
+ });