@dxos/react-ui-editor 0.3.11-next.0fb359e → 0.3.11-next.e28df4f

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 (201) hide show
  1. package/dist/lib/browser/index.mjs +1854 -1391
  2. package/dist/lib/browser/index.mjs.map +4 -4
  3. package/dist/lib/browser/meta.json +1 -1
  4. package/dist/types/src/components/TextEditor/MarkdownEditor.stories.d.ts +16 -14
  5. package/dist/types/src/components/TextEditor/MarkdownEditor.stories.d.ts.map +1 -1
  6. package/dist/types/src/components/TextEditor/TextEditor.d.ts +20 -17
  7. package/dist/types/src/components/TextEditor/TextEditor.d.ts.map +1 -1
  8. package/dist/types/src/components/TextEditor/TextEditor.stories.d.ts +6 -6
  9. package/dist/types/src/components/TextEditor/TextEditor.stories.d.ts.map +1 -1
  10. package/dist/types/src/components/TextEditor/automerge.stories.d.ts.map +1 -1
  11. package/dist/types/src/components/TextEditor/comments.stories.d.ts +26 -0
  12. package/dist/types/src/components/TextEditor/comments.stories.d.ts.map +1 -0
  13. package/dist/types/src/components/TextEditor/hooks.stories.d.ts +23 -0
  14. package/dist/types/src/components/TextEditor/hooks.stories.d.ts.map +1 -0
  15. package/dist/types/src/components/Toolbar/Toolbar.d.ts +25 -0
  16. package/dist/types/src/components/Toolbar/Toolbar.d.ts.map +1 -0
  17. package/dist/types/src/components/Toolbar/Toolbar.stories.d.ts +23 -0
  18. package/dist/types/src/components/Toolbar/Toolbar.stories.d.ts.map +1 -0
  19. package/dist/types/src/components/Toolbar/index.d.ts +2 -0
  20. package/dist/types/src/components/Toolbar/index.d.ts.map +1 -0
  21. package/dist/types/src/components/index.d.ts +1 -0
  22. package/dist/types/src/components/index.d.ts.map +1 -1
  23. package/dist/types/src/extensions/autocomplete.d.ts.map +1 -1
  24. package/dist/types/src/extensions/automerge/automerge.d.ts.map +1 -1
  25. package/dist/types/src/extensions/automerge/automerge.spec.d.ts +2 -0
  26. package/dist/types/src/extensions/automerge/automerge.spec.d.ts.map +1 -0
  27. package/dist/types/src/extensions/automerge/automerge.test.d.ts +2 -0
  28. package/dist/types/src/extensions/automerge/automerge.test.d.ts.map +1 -0
  29. package/dist/types/src/extensions/automerge/cursor.d.ts +1 -1
  30. package/dist/types/src/extensions/automerge/cursor.d.ts.map +1 -1
  31. package/dist/types/src/extensions/automerge/defs.d.ts +9 -11
  32. package/dist/types/src/extensions/automerge/defs.d.ts.map +1 -1
  33. package/dist/types/src/extensions/automerge/semaphore.d.ts +8 -12
  34. package/dist/types/src/extensions/automerge/semaphore.d.ts.map +1 -1
  35. package/dist/types/src/extensions/automerge/update-automerge.d.ts +2 -2
  36. package/dist/types/src/extensions/automerge/update-automerge.d.ts.map +1 -1
  37. package/dist/types/src/extensions/awareness.d.ts +2 -2
  38. package/dist/types/src/extensions/awareness.d.ts.map +1 -1
  39. package/dist/types/src/extensions/basic.d.ts +4 -5
  40. package/dist/types/src/extensions/basic.d.ts.map +1 -1
  41. package/dist/types/src/extensions/comments.d.ts +37 -20
  42. package/dist/types/src/extensions/comments.d.ts.map +1 -1
  43. package/dist/types/src/{util → extensions}/cursor.d.ts +10 -2
  44. package/dist/types/src/extensions/cursor.d.ts.map +1 -0
  45. package/dist/types/src/extensions/index.d.ts +2 -8
  46. package/dist/types/src/extensions/index.d.ts.map +1 -1
  47. package/dist/types/src/extensions/markdown/bundle.d.ts +3 -6
  48. package/dist/types/src/extensions/markdown/bundle.d.ts.map +1 -1
  49. package/dist/types/src/extensions/markdown/code.d.ts.map +1 -0
  50. package/dist/types/src/extensions/markdown/formatting.d.ts +13 -0
  51. package/dist/types/src/extensions/markdown/formatting.d.ts.map +1 -0
  52. package/dist/types/src/extensions/markdown/heading.d.ts +6 -0
  53. package/dist/types/src/extensions/markdown/heading.d.ts.map +1 -0
  54. package/dist/types/src/extensions/markdown/highlight.d.ts.map +1 -1
  55. package/dist/types/src/extensions/markdown/hr.d.ts.map +1 -0
  56. package/dist/types/src/extensions/markdown/image.d.ts.map +1 -0
  57. package/dist/types/src/extensions/markdown/index.d.ts +8 -0
  58. package/dist/types/src/extensions/markdown/index.d.ts.map +1 -1
  59. package/dist/types/src/extensions/markdown/link.d.ts.map +1 -0
  60. package/dist/types/src/extensions/markdown/table.d.ts.map +1 -0
  61. package/dist/types/src/extensions/markdown/tasklist.d.ts.map +1 -0
  62. package/dist/types/src/extensions/mention.d.ts.map +1 -1
  63. package/dist/types/src/extensions/outliner.d.ts +4 -0
  64. package/dist/types/src/extensions/outliner.d.ts.map +1 -0
  65. package/dist/types/src/hooks/awareness-provider.d.ts +7 -4
  66. package/dist/types/src/hooks/awareness-provider.d.ts.map +1 -1
  67. package/dist/types/src/hooks/defs.d.ts +25 -0
  68. package/dist/types/src/hooks/defs.d.ts.map +1 -0
  69. package/dist/types/src/hooks/index.d.ts +4 -2
  70. package/dist/types/src/hooks/index.d.ts.map +1 -1
  71. package/dist/types/src/hooks/useActionHandler.d.ts +4 -0
  72. package/dist/types/src/hooks/useActionHandler.d.ts.map +1 -0
  73. package/dist/types/src/hooks/useEditorView.d.ts +17 -0
  74. package/dist/types/src/hooks/useEditorView.d.ts.map +1 -0
  75. package/dist/types/src/hooks/useTextEditor.d.ts +39 -0
  76. package/dist/types/src/hooks/useTextEditor.d.ts.map +1 -0
  77. package/dist/types/src/hooks/useTextModel.d.ts +2 -32
  78. package/dist/types/src/hooks/useTextModel.d.ts.map +1 -1
  79. package/dist/types/src/index.d.ts +3 -0
  80. package/dist/types/src/index.d.ts.map +1 -1
  81. package/dist/types/src/styles/markdown.d.ts +2 -2
  82. package/dist/types/src/styles/markdown.d.ts.map +1 -1
  83. package/dist/types/src/styles/tokens.d.ts +1 -0
  84. package/dist/types/src/styles/tokens.d.ts.map +1 -1
  85. package/dist/types/src/themes/default.d.ts.map +1 -1
  86. package/dist/types/src/util.d.ts +7 -0
  87. package/dist/types/src/util.d.ts.map +1 -0
  88. package/package.json +48 -37
  89. package/src/components/TextEditor/MarkdownEditor.stories.tsx +84 -91
  90. package/src/components/TextEditor/TextEditor.stories.tsx +12 -46
  91. package/src/components/TextEditor/TextEditor.tsx +136 -95
  92. package/src/components/TextEditor/automerge.stories.tsx +1 -2
  93. package/src/components/TextEditor/comments.stories.tsx +357 -0
  94. package/src/components/TextEditor/hooks.stories.tsx +106 -0
  95. package/src/components/Toolbar/Toolbar.stories.tsx +90 -0
  96. package/src/components/Toolbar/Toolbar.tsx +166 -0
  97. package/src/components/Toolbar/index.ts +5 -0
  98. package/src/components/index.ts +1 -0
  99. package/src/extensions/autocomplete.ts +23 -19
  100. package/src/extensions/automerge/automerge.spec.tsx +76 -0
  101. package/src/extensions/automerge/automerge.test.ts +13 -0
  102. package/src/extensions/automerge/automerge.ts +55 -46
  103. package/src/extensions/automerge/cursor.ts +5 -4
  104. package/src/extensions/automerge/defs.ts +13 -34
  105. package/src/extensions/automerge/semaphore.ts +33 -32
  106. package/src/extensions/automerge/update-automerge.ts +14 -8
  107. package/src/extensions/automerge/update-codemirror.ts +5 -5
  108. package/src/extensions/awareness.ts +43 -49
  109. package/src/extensions/basic.ts +17 -10
  110. package/src/extensions/comments.ts +323 -268
  111. package/src/extensions/cursor.ts +50 -0
  112. package/src/extensions/index.ts +2 -8
  113. package/src/extensions/markdown/bundle.ts +23 -28
  114. package/src/extensions/markdown/code.ts +150 -0
  115. package/src/extensions/markdown/formatting.ts +188 -0
  116. package/src/extensions/markdown/heading.ts +59 -0
  117. package/src/extensions/markdown/highlight.ts +11 -26
  118. package/src/extensions/markdown/hr.ts +71 -0
  119. package/src/extensions/markdown/image.ts +87 -0
  120. package/src/extensions/markdown/index.ts +8 -0
  121. package/src/extensions/{link.ts → markdown/link.ts} +87 -86
  122. package/src/extensions/{table.ts → markdown/table.ts} +10 -4
  123. package/src/extensions/markdown/tasklist.ts +110 -0
  124. package/src/extensions/mention.ts +9 -2
  125. package/src/extensions/outliner.ts +12 -0
  126. package/src/hooks/awareness-provider.ts +24 -20
  127. package/src/hooks/defs.ts +42 -0
  128. package/src/hooks/index.ts +5 -2
  129. package/src/hooks/useActionHandler.ts +62 -0
  130. package/src/hooks/useEditorView.ts +29 -0
  131. package/src/hooks/useTextEditor.ts +153 -0
  132. package/src/hooks/useTextModel.ts +50 -59
  133. package/src/index.ts +3 -0
  134. package/src/styles/markdown.ts +9 -12
  135. package/src/styles/tokens.ts +3 -0
  136. package/src/themes/default.ts +53 -80
  137. package/src/util.ts +41 -0
  138. package/dist/types/src/components/TextEditor/codemirror.stories.d.ts +0 -12
  139. package/dist/types/src/components/TextEditor/codemirror.stories.d.ts.map +0 -1
  140. package/dist/types/src/components/TextEditor/yjs.stories.d.ts +0 -12
  141. package/dist/types/src/components/TextEditor/yjs.stories.d.ts.map +0 -1
  142. package/dist/types/src/extensions/code.d.ts.map +0 -1
  143. package/dist/types/src/extensions/hr.d.ts.map +0 -1
  144. package/dist/types/src/extensions/image.d.ts.map +0 -1
  145. package/dist/types/src/extensions/link.d.ts.map +0 -1
  146. package/dist/types/src/extensions/mermaid.d.ts +0 -3
  147. package/dist/types/src/extensions/mermaid.d.ts.map +0 -1
  148. package/dist/types/src/extensions/table.d.ts.map +0 -1
  149. package/dist/types/src/extensions/tasklist.d.ts.map +0 -1
  150. package/dist/types/src/extensions/yjs/cursor.d.ts +0 -4
  151. package/dist/types/src/extensions/yjs/cursor.d.ts.map +0 -1
  152. package/dist/types/src/extensions/yjs/index.d.ts +0 -2
  153. package/dist/types/src/extensions/yjs/index.d.ts.map +0 -1
  154. package/dist/types/src/extensions/yjs/yjs.d.ts +0 -4
  155. package/dist/types/src/extensions/yjs/yjs.d.ts.map +0 -1
  156. package/dist/types/src/extensions/yjs/yjs.test.d.ts +0 -2
  157. package/dist/types/src/extensions/yjs/yjs.test.d.ts.map +0 -1
  158. package/dist/types/src/hooks/automerge.d.ts +0 -3
  159. package/dist/types/src/hooks/automerge.d.ts.map +0 -1
  160. package/dist/types/src/hooks/yjs.d.ts +0 -27
  161. package/dist/types/src/hooks/yjs.d.ts.map +0 -1
  162. package/dist/types/src/testing/index.d.ts +0 -3
  163. package/dist/types/src/testing/index.d.ts.map +0 -1
  164. package/dist/types/src/testing/proto/gen/schema.d.ts +0 -15
  165. package/dist/types/src/testing/proto/gen/schema.d.ts.map +0 -1
  166. package/dist/types/src/testing/proto/index.d.ts +0 -2
  167. package/dist/types/src/testing/proto/index.d.ts.map +0 -1
  168. package/dist/types/src/testing/replicator.d.ts +0 -45
  169. package/dist/types/src/testing/replicator.d.ts.map +0 -1
  170. package/dist/types/src/util/cursor.d.ts.map +0 -1
  171. package/dist/types/src/util/index.d.ts +0 -3
  172. package/dist/types/src/util/index.d.ts.map +0 -1
  173. package/dist/types/src/util/util.d.ts +0 -5
  174. package/dist/types/src/util/util.d.ts.map +0 -1
  175. package/src/components/TextEditor/codemirror.stories.ts +0 -115
  176. package/src/components/TextEditor/yjs.stories.tsx +0 -56
  177. package/src/extensions/code.ts +0 -99
  178. package/src/extensions/hr.ts +0 -74
  179. package/src/extensions/image.ts +0 -69
  180. package/src/extensions/mermaid.ts +0 -11
  181. package/src/extensions/tasklist.ts +0 -121
  182. package/src/extensions/yjs/cursor.ts +0 -21
  183. package/src/extensions/yjs/index.ts +0 -5
  184. package/src/extensions/yjs/yjs.test.ts +0 -35
  185. package/src/extensions/yjs/yjs.ts +0 -16
  186. package/src/hooks/automerge.ts +0 -52
  187. package/src/hooks/yjs.ts +0 -178
  188. package/src/testing/index.ts +0 -6
  189. package/src/testing/proto/gen/schema.ts +0 -49
  190. package/src/testing/proto/index.ts +0 -5
  191. package/src/testing/proto/schema.proto +0 -15
  192. package/src/testing/replicator.ts +0 -184
  193. package/src/util/cursor.ts +0 -29
  194. package/src/util/index.ts +0 -6
  195. package/src/util/util.ts +0 -18
  196. /package/dist/types/src/extensions/{code.d.ts → markdown/code.d.ts} +0 -0
  197. /package/dist/types/src/extensions/{hr.d.ts → markdown/hr.d.ts} +0 -0
  198. /package/dist/types/src/extensions/{image.d.ts → markdown/image.d.ts} +0 -0
  199. /package/dist/types/src/extensions/{link.d.ts → markdown/link.d.ts} +0 -0
  200. /package/dist/types/src/extensions/{table.d.ts → markdown/table.d.ts} +0 -0
  201. /package/dist/types/src/extensions/{tasklist.d.ts → markdown/tasklist.d.ts} +0 -0
@@ -0,0 +1,71 @@
1
+ //
2
+ // Copyright 2023 DXOS.org
3
+ //
4
+
5
+ import { syntaxTree } from '@codemirror/language';
6
+ import { RangeSetBuilder } from '@codemirror/state';
7
+ import { Decoration, type DecorationSet, EditorView, ViewPlugin, type ViewUpdate, WidgetType } from '@codemirror/view';
8
+
9
+ import { getToken } from '../../styles';
10
+
11
+ // TODO(burdon): Reconcile with theme.
12
+ const styles = EditorView.baseTheme({
13
+ '& .cm-hr': {
14
+ // Note that block-level decorations should not have vertical margins,
15
+ borderTop: `1px solid ${getToken('extend.colors.neutral.200')}`,
16
+ },
17
+ });
18
+
19
+ class HorizontalRuleWidget extends WidgetType {
20
+ override toDOM() {
21
+ const el = document.createElement('div');
22
+ el.className = 'cm-hr';
23
+ return el;
24
+ }
25
+ }
26
+
27
+ const decoration = Decoration.replace({ widget: new HorizontalRuleWidget() });
28
+
29
+ const buildDecorations = (view: EditorView): DecorationSet => {
30
+ const builder = new RangeSetBuilder<Decoration>();
31
+ const { state } = view;
32
+ const cursor = state.selection.main.head;
33
+
34
+ for (const { from, to } of view.visibleRanges) {
35
+ syntaxTree(state).iterate({
36
+ enter: (node) => {
37
+ if (node.name === 'HorizontalRule') {
38
+ // Check if cursor is inside text.
39
+ if (cursor <= node.from || cursor >= node.to) {
40
+ builder.add(node.from, node.to, decoration);
41
+ }
42
+ }
43
+ },
44
+ from,
45
+ to,
46
+ });
47
+ }
48
+
49
+ return builder.finish();
50
+ };
51
+
52
+ export const hr = () => {
53
+ return [
54
+ ViewPlugin.fromClass(
55
+ class {
56
+ decorations: DecorationSet;
57
+ constructor(view: EditorView) {
58
+ this.decorations = buildDecorations(view);
59
+ }
60
+
61
+ update(update: ViewUpdate) {
62
+ this.decorations = buildDecorations(update.view);
63
+ }
64
+ },
65
+ {
66
+ decorations: (value) => value.decorations,
67
+ },
68
+ ),
69
+ styles,
70
+ ];
71
+ };
@@ -0,0 +1,87 @@
1
+ //
2
+ // Copyright 2023 DXOS.org
3
+ //
4
+
5
+ import { syntaxTree } from '@codemirror/language';
6
+ import { type EditorState, type Extension, StateField, type Transaction, type Range } from '@codemirror/state';
7
+ import { Decoration, type DecorationSet, EditorView, WidgetType } from '@codemirror/view';
8
+
9
+ export type ImageOptions = {};
10
+
11
+ export const image = (options: ImageOptions = {}): Extension => {
12
+ return StateField.define<DecorationSet>({
13
+ create: (state) => {
14
+ return Decoration.set(buildDecorations(0, state.doc.length, state));
15
+ },
16
+ update: (value: DecorationSet, tr: Transaction) => {
17
+ if (!tr.docChanged && !tr.selection) {
18
+ return value;
19
+ }
20
+ // Find range of changes and cursor changes.
21
+ const cursor = tr.state.selection.main.head;
22
+ const oldCursor = tr.changes.mapPos(tr.startState.selection.main.head);
23
+ let from = Math.min(cursor, oldCursor);
24
+ let to = Math.max(cursor, oldCursor);
25
+ tr.changes.iterChangedRanges((fromA, toA, fromB, toB) => {
26
+ from = Math.min(from, fromB);
27
+ to = Math.max(to, toB);
28
+ });
29
+ // Expand to cover lines
30
+ from = tr.state.doc.lineAt(from).from;
31
+ to = tr.state.doc.lineAt(to).to;
32
+ return value.map(tr.changes).update({
33
+ filterFrom: from,
34
+ filterTo: to,
35
+ filter: () => false,
36
+ add: buildDecorations(from, to, tr.state),
37
+ });
38
+ },
39
+ provide: (field) => EditorView.decorations.from(field),
40
+ });
41
+ };
42
+
43
+ const buildDecorations = (from: number, to: number, state: EditorState) => {
44
+ const decorations: Range<Decoration>[] = [];
45
+ const cursor = state.selection.main.head;
46
+
47
+ syntaxTree(state).iterate({
48
+ enter: (node) => {
49
+ if (node.name === 'Image') {
50
+ const urlNode = node.node.getChild('URL');
51
+ if (urlNode) {
52
+ const hide = state.readOnly || cursor < node.from || cursor > node.to;
53
+ const url = state.sliceDoc(urlNode.from, urlNode.to);
54
+ decorations.push(
55
+ Decoration.replace({
56
+ block: true, // Prevent cursor from entering.
57
+ widget: new ImageWidget(url),
58
+ }).range(hide ? node.from : node.to, node.to),
59
+ );
60
+ }
61
+ }
62
+ },
63
+ from,
64
+ to,
65
+ });
66
+
67
+ return decorations;
68
+ };
69
+
70
+ class ImageWidget extends WidgetType {
71
+ constructor(readonly _url: string) {
72
+ super();
73
+ }
74
+
75
+ override eq(other: this) {
76
+ return this._url === (other as any as ImageWidget)._url;
77
+ }
78
+
79
+ override toDOM(view: EditorView) {
80
+ const img = document.createElement('img');
81
+ img.setAttribute('src', this._url);
82
+ img.setAttribute('class', 'cm-image');
83
+ // Images are hidden until successfully loaded to avoid flickering effects.
84
+ img.onload = () => img.classList.add('cm-loaded-image');
85
+ return img;
86
+ }
87
+ }
@@ -3,4 +3,12 @@
3
3
  //
4
4
 
5
5
  export * from './bundle';
6
+ export * from './code';
7
+ export * from './formatting';
8
+ export * from './heading';
6
9
  export * from './highlight';
10
+ export * from './hr';
11
+ export * from './image';
12
+ export * from './link';
13
+ export * from './table';
14
+ export * from './tasklist';
@@ -4,24 +4,25 @@
4
4
  //
5
5
 
6
6
  import { syntaxTree } from '@codemirror/language';
7
+ import { type Extension, RangeSetBuilder } from '@codemirror/state';
7
8
  import {
8
- type EditorState,
9
- type Extension,
10
- type RangeSet,
11
- RangeSetBuilder,
12
- StateField,
13
- type Transaction,
14
- } from '@codemirror/state';
15
- import { Decoration, EditorView, WidgetType, hoverTooltip } from '@codemirror/view';
9
+ hoverTooltip,
10
+ Decoration,
11
+ type DecorationSet,
12
+ type EditorView,
13
+ ViewPlugin,
14
+ type ViewUpdate,
15
+ WidgetType,
16
+ } from '@codemirror/view';
17
+ import { type SyntaxNode } from '@lezer/common';
16
18
 
17
19
  import { tooltipContent } from '@dxos/react-ui-theme';
18
20
 
19
- const markdownLinkRegexp = /\[([^\]]+)]\(([^)]+)\)/;
20
-
21
21
  // Adapted from:
22
22
  // https://codemirror.net/try/?c=aW1wb3J0IHsgYmFzaWNTZXR1cCwgRWRpdG9yVmlldyB9IGZyb20gImNvZGVtaXJyb3IiCmltcG9ydCB7IG1hcmtkb3duIH0gZnJvbSAiQGNvZGVtaXJyb3IvbGFuZy1tYXJrZG93biIKaW1wb3J0IHsgc3ludGF4VHJlZSB9IGZyb20gIkBjb2RlbWlycm9yL2xhbmd1YWdlIjsKaW1wb3J0IHsgRWRpdG9yU3RhdGUsIFJhbmdlU2V0QnVpbGRlciwgU3RhdGVGaWVsZCB9IGZyb20gIkBjb2RlbWlycm9yL3N0YXRlIjsKaW1wb3J0IHsgRGVjb3JhdGlvbiwgV2lkZ2V0VHlwZSB9IGZyb20gIkBjb2RlbWlycm9yL3ZpZXciOwoKY2xhc3MgTGluayBleHRlbmRzIFdpZGdldFR5cGUgewogIGNvbnN0cnVjdG9yKHRleHQsIHVybCkgewogICAgc3VwZXIoKQogICAgdGhpcy50ZXh0ID0gdGV4dAogICAgdGhpcy51cmwgPSB1cmwKICB9CiAgZXEob3RoZXIpIHsgcmV0dXJuIHRoaXMudGV4dCA9PSBvdGhlci50ZXh0ICYmIHRoaXMudXJsID09IG90aGVyLnVybCB9CiAgdG9ET00oKSB7CiAgICBsZXQgbGluayA9IGRvY3VtZW50LmNyZWF0ZUVsZW1lbnQoImEiKQogICAgbGluay50ZXh0Q29udGVudCA9IHRoaXMudGV4dAogICAgbGluay5ocmVmID0gdGhpcy51cmwKICAgIHJldHVybiBsaW5rOwogIH0KfQoKbGV0IGRlY29yYXRpb25zRmllbGQgPSBTdGF0ZUZpZWxkLmRlZmluZSh7CiAgY3JlYXRlKCkgewogICAgcmV0dXJuIERlY29yYXRpb24ubm9uZTsKICB9LAogIHVwZGF0ZShfLCB0cikgewogICAgY29uc3QgYnVpbGRlciA9IG5ldyBSYW5nZVNldEJ1aWxkZXIoKTsKICAgIGxldCBjdXJzb3IgPSB0ci5zdGF0ZS5zZWxlY3Rpb24ubWFpbi5oZWFkOwogICAgc3ludGF4VHJlZSh0ci5zdGF0ZSkuaXRlcmF0ZSh7CiAgICAgIGVudGVyOiAobm9kZSkgPT4gewogICAgICAgIGlmICgoY3Vyc29yIDwgbm9kZS5mcm9tIHx8IGN1cnNvciA+IG5vZGUudG8pICYmIG5vZGUubmFtZSA9PSAiTGluayIpIHsKICAgICAgICAgIGxldCBtYXJrcyA9IG5vZGUubm9kZS5nZXRDaGlsZHJlbigiTGlua01hcmsiKTsKICAgICAgICAgIGxldCB0ZXh0ID0gbWFya3MubGVuZ3RoID49IDIgPyB0ci5zdGF0ZS5zbGljZURvYyhtYXJrc1swXS50bywgbWFya3NbMV0uZnJvbSkgOiAiIgoKICAgICAgICAgIGxldCB1cmxOb2RlID0gbm9kZS5ub2RlLmdldENoaWxkKCJVUkwiKTsKICAgICAgICAgIGxldCB1cmwgPSB1cmxOb2RlID8gdHIuc3RhdGUuc2xpY2VEb2ModXJsTm9kZS5mcm9tLCB1cmxOb2RlLnRvKSA6ICIiCiAgICAgICAgICBidWlsZGVyLmFkZChub2RlLmZyb20sIG5vZGUudG8sIERlY29yYXRpb24ucmVwbGFjZSh7CiAgICAgICAgICAgIHdpZGdldDogbmV3IExpbmsodGV4dCwgdXJsKSwKICAgICAgICAgIH0pLAogICAgICAgICAgICAgICAgICAgICApOwogICAgICAgICAgcmV0dXJuIGZhbHNlOwogICAgICAgIH0KICAgICAgICByZXR1cm4gdHJ1ZTsKICAgICAgfSwKICAgIH0pOwogICAgcmV0dXJuIGJ1aWxkZXIuZmluaXNoKCk7CiAgfSwKICBwcm92aWRlOiAoZikgPT4gRWRpdG9yVmlldy5kZWNvcmF0aW9ucy5mcm9tKGYpLAp9KTsKCmxldCB2aWV3ID0gbmV3IEVkaXRvclZpZXcoewogIGRvYzogIiMgSGVsbG8gV29ybGRcblxuW1Rlc3RdKGh0dHBzOi8vZXhhbXBsZS5jb20pXG5cblxuIiwKICBleHRlbnNpb25zOiBbZGVjb3JhdGlvbnNGaWVsZCwgbWFya2Rvd24oKV0sCiAgcGFyZW50OiBkb2N1bWVudC5ib2R5LAp9KTsKCnZpZXcuZm9jdXMoKTs=
23
23
  // https://discuss.codemirror.net/t/link-widget-is-clicked-only-if-editor-is-out-of-focus/7433
24
24
 
25
+ // TODO(burdon): Callback to dynamically lookup title of linked pages.
25
26
  export type LinkOptions = {
26
27
  link?: boolean;
27
28
  onHover?: (el: Element, url: string) => void;
@@ -34,11 +35,23 @@ export type LinkOptions = {
34
35
  */
35
36
  export const link = (options: LinkOptions = {}): Extension => {
36
37
  const extensions: Extension[] = [
37
- StateField.define<RangeSet<any>>({
38
- create: (state) => update(state, options),
39
- update: (_: RangeSet<any>, tr: Transaction) => update(tr.state, options),
40
- provide: (field) => EditorView.decorations.from(field),
41
- }),
38
+ ViewPlugin.fromClass(
39
+ class {
40
+ decorations: DecorationSet;
41
+ constructor(view: EditorView) {
42
+ this.decorations = buildDecorations(view, options);
43
+ }
44
+
45
+ update(update: ViewUpdate) {
46
+ if (update.docChanged || update.viewportChanged || update.selectionSet) {
47
+ this.decorations = buildDecorations(update.view, options);
48
+ }
49
+ }
50
+ },
51
+ {
52
+ decorations: (value) => value.decorations,
53
+ },
54
+ ),
42
55
  ];
43
56
 
44
57
  if (options.onHover) {
@@ -46,41 +59,27 @@ export const link = (options: LinkOptions = {}): Extension => {
46
59
  // https://codemirror.net/examples/tooltip
47
60
  // https://codemirror.net/docs/ref/#view.hoverTooltip
48
61
  // https://github.com/codemirror/view/blob/main/src/tooltip.ts
49
- hoverTooltip((view, pos) => {
50
- const { from, text } = view.state.doc.lineAt(pos);
51
- const p = pos - from;
52
-
53
- let idx = 0;
54
- let match;
55
- do {
56
- match = text.substring(idx).match(markdownLinkRegexp);
57
- if (!match) {
58
- match = undefined;
59
- break;
60
- }
61
-
62
- idx = text.indexOf(match[0]);
63
- if (p >= idx && p < idx + match[0].length) {
64
- break;
65
- }
66
-
67
- idx += match[0].length;
68
- } while (true);
62
+ hoverTooltip((view, pos, side) => {
63
+ const syntax = syntaxTree(view.state).resolveInner(pos, side);
64
+ let link = null;
65
+ for (let i = 0, node: SyntaxNode | null = syntax; !link && node && i < 5; node = node.parent, i++) {
66
+ link = node.name === 'Link' ? node : null;
67
+ }
69
68
 
70
- if (!match) {
69
+ const url = link && link.getChild('URL');
70
+ if (!url || !link) {
71
71
  return null;
72
72
  }
73
73
 
74
- const [, , url] = match ?? [];
75
-
74
+ const urlText = view.state.sliceDoc(url.from, url.to);
76
75
  return {
77
- pos: idx + from,
78
- end: idx + from + match[0].length,
76
+ pos: link.from,
77
+ end: link.to,
79
78
  above: true,
80
79
  create: () => {
81
80
  const el = document.createElement('div');
82
81
  el.className = tooltipContent({}, 'pli-2 plb-1');
83
- options.onHover!(el, url);
82
+ options.onHover!(el, urlText);
84
83
  return { dom: el, offset: { x: 0, y: 4 } };
85
84
  },
86
85
  };
@@ -92,19 +91,15 @@ export const link = (options: LinkOptions = {}): Extension => {
92
91
  };
93
92
 
94
93
  class LinkButton extends WidgetType {
95
- constructor(
96
- private readonly _pos: number,
97
- private readonly _url: string,
98
- private readonly _onAttach: LinkOptions['onRender'],
99
- ) {
94
+ constructor(private readonly _url: string, private readonly _onAttach: LinkOptions['onRender']) {
100
95
  super();
101
96
  }
102
97
 
103
- override eq(other: LinkButton) {
104
- return this._pos === other._pos && this._url === other._url;
98
+ override eq(other: this) {
99
+ return this._url === other._url;
105
100
  }
106
101
 
107
- toDOM(view: EditorView) {
102
+ override toDOM(view: EditorView) {
108
103
  const el = document.createElement('span');
109
104
  this._onAttach!(el, this._url);
110
105
  return el;
@@ -116,11 +111,11 @@ class LinkText extends WidgetType {
116
111
  super();
117
112
  }
118
113
 
119
- override eq(other: LinkText) {
114
+ override eq(other: this) {
120
115
  return this._url === other._url;
121
116
  }
122
117
 
123
- toDOM(view: EditorView) {
118
+ override toDOM(view: EditorView) {
124
119
  const link = document.createElement('a');
125
120
  link.setAttribute('class', 'cm-link');
126
121
  link.textContent = this._text;
@@ -147,46 +142,52 @@ class LinkText extends WidgetType {
147
142
  * possibly overlapping ranges in such a way that they can efficiently be mapped though document changes.
148
143
  * https://codemirror.net/docs/ref/#state
149
144
  */
150
- const update = (state: EditorState, options: LinkOptions) => {
151
- const builder = new RangeSetBuilder();
145
+ const buildDecorations = (view: EditorView, options: LinkOptions): DecorationSet => {
146
+ const builder = new RangeSetBuilder<Decoration>();
147
+ const { state } = view;
152
148
  const cursor = state.selection.main.head;
153
149
 
154
- syntaxTree(state).iterate({
155
- enter: (node) => {
156
- // Check if cursor is inside text.
157
- if (node.name === 'Link') {
158
- const marks = node.node.getChildren('LinkMark');
159
- const text = marks.length >= 2 ? state.sliceDoc(marks[0].to, marks[1].from) : '';
160
- const urlNode = node.node.getChild('URL');
161
- const url = urlNode ? state.sliceDoc(urlNode.from, urlNode.to) : '';
162
- if (!url) {
163
- return false;
164
- }
150
+ for (const { from, to } of view.visibleRanges) {
151
+ syntaxTree(state).iterate({
152
+ enter: (node) => {
153
+ // Check if cursor is inside text.
154
+ if (node.name === 'Link') {
155
+ const marks = node.node.getChildren('LinkMark');
156
+ const text = marks.length >= 2 ? state.sliceDoc(marks[0].to, marks[1].from) : '';
157
+ const urlNode = node.node.getChild('URL');
158
+ const url = urlNode ? state.sliceDoc(urlNode.from, urlNode.to) : '';
159
+ if (!url) {
160
+ return false;
161
+ }
165
162
 
166
- if (state.readOnly || cursor < node.from || cursor > node.to) {
167
- builder.add(
168
- node.from,
169
- node.to,
170
- Decoration.replace({
171
- widget: new LinkText(text, options.link ? url : undefined),
172
- }),
173
- );
174
- }
163
+ // TODO(burdon): Don't expand if moving cursor up/down.
164
+ if (!view.hasFocus || state.readOnly || cursor < node.from || cursor > node.to) {
165
+ builder.add(
166
+ node.from,
167
+ node.to,
168
+ Decoration.replace({
169
+ widget: new LinkText(text, options.link ? url : undefined),
170
+ }),
171
+ );
172
+ }
175
173
 
176
- if (options.onRender) {
177
- builder.add(
178
- node.to,
179
- node.to,
180
- Decoration.replace({
181
- widget: new LinkButton(node.from, url, options.onRender),
182
- }),
183
- );
184
- }
174
+ if (options.onRender) {
175
+ builder.add(
176
+ node.to,
177
+ node.to,
178
+ Decoration.replace({
179
+ widget: new LinkButton(url, options.onRender),
180
+ }),
181
+ );
182
+ }
185
183
 
186
- return false;
187
- }
188
- },
189
- });
184
+ return false;
185
+ }
186
+ },
187
+ from,
188
+ to,
189
+ });
190
+ }
190
191
 
191
192
  return builder.finish();
192
193
  };
@@ -81,7 +81,6 @@ const update = (state: EditorState, options: TableOptions) => {
81
81
  table.from,
82
82
  table.to,
83
83
  Decoration.replace({
84
- block: true,
85
84
  widget: new TableWidget(table),
86
85
  }),
87
86
  );
@@ -98,11 +97,14 @@ class TableWidget extends WidgetType {
98
97
  super();
99
98
  }
100
99
 
101
- override eq(other: WidgetType) {
102
- return this._table.from === (other as any as TableWidget)?._table?.from;
100
+ override eq(other: this) {
101
+ return (
102
+ this._table.header?.join() === other._table.header?.join() &&
103
+ this._table.rows?.join() === other._table.rows?.join()
104
+ );
103
105
  }
104
106
 
105
- toDOM(view: EditorView) {
107
+ override toDOM(view: EditorView) {
106
108
  const table = document.createElement('table');
107
109
 
108
110
  {
@@ -129,4 +131,8 @@ class TableWidget extends WidgetType {
129
131
 
130
132
  return table;
131
133
  }
134
+
135
+ override ignoreEvent(e: Event) {
136
+ return !/^mouse/.test(e.type);
137
+ }
132
138
  }
@@ -0,0 +1,110 @@
1
+ //
2
+ // Copyright 2023 DXOS.org
3
+ //
4
+
5
+ import { syntaxTree } from '@codemirror/language';
6
+ import { RangeSetBuilder } from '@codemirror/state';
7
+ import { EditorView, Decoration, WidgetType, ViewPlugin, type DecorationSet, type ViewUpdate } from '@codemirror/view';
8
+
9
+ import { getToken } from '../../styles';
10
+
11
+ // TODO(burdon): Reconcile with theme.
12
+ const styles = EditorView.baseTheme({
13
+ '& .cm-task': {
14
+ color: getToken('extend.colors.blue.500'),
15
+ },
16
+ '& .cm-task-checkbox': {
17
+ marginLeft: '4px',
18
+ marginRight: '4px',
19
+ },
20
+ });
21
+
22
+ class CheckboxWidget extends WidgetType {
23
+ constructor(private _checked: boolean) {
24
+ super();
25
+ }
26
+
27
+ override eq(other: this) {
28
+ return this._checked === other._checked;
29
+ }
30
+
31
+ override toDOM(view: EditorView) {
32
+ const input = document.createElement('input');
33
+ input.className = 'cm-task-checkbox';
34
+ input.type = 'checkbox';
35
+ input.checked = this._checked;
36
+ if (view.state.readOnly) {
37
+ input.setAttribute('disabled', 'true');
38
+ } else {
39
+ input.onmousedown = (event: Event) => {
40
+ const pos = view.posAtDOM(input);
41
+ const text = view.state.sliceDoc(pos, pos + 3);
42
+ if (text === (this._checked ? '[x]' : '[ ]')) {
43
+ view.dispatch({
44
+ changes: { from: pos + 1, to: pos + 2, insert: this._checked ? ' ' : 'x' },
45
+ });
46
+ event.preventDefault();
47
+ }
48
+ };
49
+ }
50
+
51
+ return input;
52
+ }
53
+
54
+ override ignoreEvent() {
55
+ return false;
56
+ }
57
+ }
58
+
59
+ const checkedDecoration = Decoration.replace({ widget: new CheckboxWidget(true) });
60
+ const uncheckedDecoration = Decoration.replace({ widget: new CheckboxWidget(false) });
61
+
62
+ const buildDecorations = (view: EditorView): DecorationSet => {
63
+ const builder = new RangeSetBuilder<Decoration>();
64
+ const { state } = view;
65
+ const cursor = state.selection.main.head;
66
+
67
+ for (const { from, to } of view.visibleRanges) {
68
+ syntaxTree(state).iterate({
69
+ enter: (node) => {
70
+ if (node.name === 'TaskMarker') {
71
+ // Check if cursor is inside text.
72
+ if (cursor < node.from || cursor > node.to) {
73
+ const checked = state.doc.sliceString(node.from + 1, node.to - 1) === 'x';
74
+ builder.add(node.from - 2, node.from - 1, Decoration.mark({ class: 'cm-task' }));
75
+ builder.add(node.from, node.to, checked ? checkedDecoration : uncheckedDecoration);
76
+ }
77
+ }
78
+ },
79
+ from,
80
+ to,
81
+ });
82
+ }
83
+
84
+ return builder.finish();
85
+ };
86
+
87
+ export type TasklistOptions = {};
88
+
89
+ export const tasklist = (options: TasklistOptions = {}) => {
90
+ return [
91
+ ViewPlugin.fromClass(
92
+ class {
93
+ decorations: DecorationSet;
94
+ constructor(view: EditorView) {
95
+ this.decorations = buildDecorations(view);
96
+ }
97
+
98
+ update(update: ViewUpdate) {
99
+ if (update.docChanged || update.viewportChanged || update.selectionSet) {
100
+ this.decorations = buildDecorations(update.view);
101
+ }
102
+ }
103
+ },
104
+ {
105
+ decorations: (value) => value.decorations,
106
+ },
107
+ ),
108
+ styles,
109
+ ];
110
+ };
@@ -9,18 +9,25 @@ export type MentionOptions = {
9
9
  onSearch: (text: string) => string[];
10
10
  };
11
11
 
12
+ // TODO(burdon): Can only have a single autocompletion. Merge configuration with autocomplete.
12
13
  export const mention = ({ onSearch }: MentionOptions): Extension => {
13
14
  return autocompletion({
15
+ // TODO(burdon): Not working.
16
+ activateOnTyping: true,
17
+ // activateOnTypingDelay: 100,
18
+ // selectOnOpen: true,
14
19
  closeOnBlur: false, // For debugging.
20
+ // defaultKeymap: false,
21
+ icons: false,
15
22
  override: [
16
23
  (context: CompletionContext): CompletionResult | null => {
24
+ console.log(context);
25
+
17
26
  const match = context.matchBefore(/@(\w+)?/);
18
27
  if (!match || (match.from === match.to && !context.explicit)) {
19
28
  return null;
20
29
  }
21
30
 
22
- console.log(match);
23
-
24
31
  return {
25
32
  from: match.from,
26
33
  options: onSearch(match.text.slice(1).toLowerCase()).map((value) => ({ label: `@${value}` })),
@@ -0,0 +1,12 @@
1
+ //
2
+ // Copyright 2024 DXOS.org
3
+ //
4
+
5
+ import { type Extension } from '@codemirror/state';
6
+
7
+ export type OutlinerOptions = {};
8
+
9
+ // TODO(burdon): Experimental component: multiple hierarchical draggable editor sections.
10
+ export const outliner = (options: OutlinerOptions = {}): Extension => {
11
+ return [];
12
+ };
@@ -29,9 +29,10 @@ export type AwarenessProviderParams = {
29
29
  info: AwarenessInfo;
30
30
  };
31
31
 
32
+ /**
33
+ * Receives and broadcasts profile and cursor position.
34
+ */
32
35
  export class SpaceAwarenessProvider implements AwarenessProvider {
33
- public readonly remoteStateChange = new Event<void>();
34
-
35
36
  private readonly _remoteStates = new Map<string, AwarenessState>();
36
37
 
37
38
  private readonly _space: Space;
@@ -39,9 +40,11 @@ export class SpaceAwarenessProvider implements AwarenessProvider {
39
40
  private readonly _peerId: string;
40
41
  private readonly _info: AwarenessInfo;
41
42
 
42
- private _localState?: AwarenessState;
43
- private _postTask?: DeferredTask;
44
43
  private _ctx?: Context;
44
+ private _postTask?: DeferredTask;
45
+ private _localState?: AwarenessState;
46
+
47
+ public readonly remoteStateChange = new Event<void>();
45
48
 
46
49
  constructor(params: AwarenessProviderParams) {
47
50
  this._space = params.space;
@@ -63,19 +66,20 @@ export class SpaceAwarenessProvider implements AwarenessProvider {
63
66
  }
64
67
  });
65
68
 
66
- const unsubscribe = this._space.listen(this._channel, (message: GossipMessage) => {
67
- switch (message.payload.kind) {
68
- case 'query': {
69
- this._handleQueryMessage();
70
- break;
69
+ this._ctx.onDispose(
70
+ this._space.listen(this._channel, (message: GossipMessage) => {
71
+ switch (message.payload.kind) {
72
+ case 'query': {
73
+ this._handleQueryMessage();
74
+ break;
75
+ }
76
+ case 'post': {
77
+ this._handlePostMessage(message.payload);
78
+ break;
79
+ }
71
80
  }
72
- case 'post': {
73
- this._handlePostMessage(message.payload);
74
- break;
75
- }
76
- }
77
- });
78
- this._ctx.onDispose(unsubscribe);
81
+ }),
82
+ );
79
83
 
80
84
  void this._space
81
85
  .postMessage(this._channel, {
@@ -92,6 +96,10 @@ export class SpaceAwarenessProvider implements AwarenessProvider {
92
96
  this._postTask = undefined;
93
97
  }
94
98
 
99
+ getRemoteStates(): AwarenessState[] {
100
+ return Array.from(this._remoteStates.values());
101
+ }
102
+
95
103
  update(position: AwarenessPosition | undefined): void {
96
104
  invariant(this._postTask);
97
105
  this._localState = {
@@ -103,10 +111,6 @@ export class SpaceAwarenessProvider implements AwarenessProvider {
103
111
  this._postTask.schedule();
104
112
  }
105
113
 
106
- getRemoteStates(): AwarenessState[] {
107
- return Array.from(this._remoteStates.values());
108
- }
109
-
110
114
  private _handleQueryMessage() {
111
115
  invariant(this._postTask);
112
116
  this._postTask.schedule();