@dxos/react-ui-editor 0.3.11-main.870164f → 0.3.11-main.8a9dd1e

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 (146) hide show
  1. package/dist/lib/browser/index.mjs +1707 -1320
  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 +9 -5
  5. package/dist/types/src/components/TextEditor/MarkdownEditor.stories.d.ts.map +1 -1
  6. package/dist/types/src/components/TextEditor/TextEditor.d.ts +12 -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/{hooks/automerge → components/TextEditor}/automerge.stories.d.ts +6 -1
  11. package/dist/types/src/components/TextEditor/automerge.stories.d.ts.map +1 -0
  12. package/dist/types/src/components/TextEditor/codemirror.stories.d.ts.map +1 -1
  13. package/dist/types/src/components/TextEditor/comments.stories.d.ts +26 -0
  14. package/dist/types/src/components/TextEditor/comments.stories.d.ts.map +1 -0
  15. package/dist/types/src/{hooks/yjs → components/TextEditor}/yjs.stories.d.ts +1 -1
  16. package/dist/types/src/components/TextEditor/yjs.stories.d.ts.map +1 -0
  17. package/dist/types/src/extensions/autocomplete.d.ts.map +1 -1
  18. package/dist/types/src/extensions/automerge/automerge.d.ts +9 -0
  19. package/dist/types/src/extensions/automerge/automerge.d.ts.map +1 -0
  20. package/dist/types/src/extensions/automerge/cursor.d.ts +5 -0
  21. package/dist/types/src/extensions/automerge/cursor.d.ts.map +1 -0
  22. package/dist/types/src/extensions/automerge/defs.d.ts +26 -0
  23. package/dist/types/src/extensions/automerge/defs.d.ts.map +1 -0
  24. package/dist/types/src/extensions/automerge/index.d.ts +4 -0
  25. package/dist/types/src/extensions/automerge/index.d.ts.map +1 -0
  26. package/dist/types/src/{hooks → extensions}/automerge/semaphore.d.ts +4 -4
  27. package/dist/types/src/extensions/automerge/semaphore.d.ts.map +1 -0
  28. package/dist/types/src/extensions/automerge/update-automerge.d.ts +5 -0
  29. package/dist/types/src/extensions/automerge/update-automerge.d.ts.map +1 -0
  30. package/dist/types/src/extensions/automerge/update-codemirror.d.ts.map +1 -0
  31. package/dist/types/src/extensions/awareness.d.ts +46 -0
  32. package/dist/types/src/extensions/awareness.d.ts.map +1 -0
  33. package/dist/types/src/extensions/code.d.ts.map +1 -1
  34. package/dist/types/src/extensions/comments.d.ts +6 -2
  35. package/dist/types/src/extensions/comments.d.ts.map +1 -1
  36. package/dist/types/src/extensions/cursor.d.ts +25 -0
  37. package/dist/types/src/extensions/cursor.d.ts.map +1 -0
  38. package/dist/types/src/extensions/hr.d.ts.map +1 -1
  39. package/dist/types/src/extensions/image.d.ts.map +1 -1
  40. package/dist/types/src/extensions/index.d.ts +4 -1
  41. package/dist/types/src/extensions/index.d.ts.map +1 -1
  42. package/dist/types/src/extensions/link.d.ts.map +1 -1
  43. package/dist/types/src/extensions/markdown/highlight.d.ts.map +1 -1
  44. package/dist/types/src/extensions/tasklist.d.ts +5 -1
  45. package/dist/types/src/extensions/tasklist.d.ts.map +1 -1
  46. package/dist/types/src/extensions/yjs/cursor.d.ts +4 -0
  47. package/dist/types/src/extensions/yjs/cursor.d.ts.map +1 -0
  48. package/dist/types/src/extensions/yjs/index.d.ts +2 -0
  49. package/dist/types/src/extensions/yjs/index.d.ts.map +1 -0
  50. package/dist/types/src/extensions/yjs/yjs.d.ts +4 -0
  51. package/dist/types/src/extensions/yjs/yjs.d.ts.map +1 -0
  52. package/dist/types/src/extensions/yjs/yjs.test.d.ts.map +1 -0
  53. package/dist/types/src/hooks/automerge.d.ts +3 -0
  54. package/dist/types/src/hooks/automerge.d.ts.map +1 -0
  55. package/dist/types/src/hooks/awareness-provider.d.ts +28 -0
  56. package/dist/types/src/hooks/awareness-provider.d.ts.map +1 -0
  57. package/dist/types/src/hooks/index.d.ts +2 -0
  58. package/dist/types/src/hooks/index.d.ts.map +1 -1
  59. package/dist/types/src/hooks/useTextModel.d.ts +4 -8
  60. package/dist/types/src/hooks/useTextModel.d.ts.map +1 -1
  61. package/dist/types/src/hooks/{yjs/space-provider.d.ts → yjs.d.ts} +8 -8
  62. package/dist/types/src/hooks/yjs.d.ts.map +1 -0
  63. package/dist/types/src/index.d.ts +3 -0
  64. package/dist/types/src/index.d.ts.map +1 -1
  65. package/dist/types/src/styles/cursors.d.ts.map +1 -0
  66. package/dist/types/src/styles/index.d.ts +1 -0
  67. package/dist/types/src/styles/index.d.ts.map +1 -1
  68. package/dist/types/src/styles/markdown.d.ts.map +1 -1
  69. package/dist/types/src/styles/tokens.d.ts +1 -0
  70. package/dist/types/src/styles/tokens.d.ts.map +1 -1
  71. package/dist/types/src/testing/replicator.d.ts +2 -2
  72. package/dist/types/src/testing/replicator.d.ts.map +1 -1
  73. package/dist/types/src/themes/default.d.ts.map +1 -1
  74. package/dist/types/src/util.d.ts.map +1 -0
  75. package/package.json +24 -22
  76. package/src/components/TextEditor/MarkdownEditor.stories.tsx +25 -23
  77. package/src/components/TextEditor/TextEditor.stories.tsx +12 -46
  78. package/src/components/TextEditor/TextEditor.tsx +69 -65
  79. package/src/{hooks/automerge → components/TextEditor}/automerge.stories.tsx +66 -6
  80. package/src/components/TextEditor/codemirror.stories.ts +3 -2
  81. package/src/components/TextEditor/comments.stories.tsx +329 -0
  82. package/src/{hooks/yjs → components/TextEditor}/yjs.stories.tsx +1 -1
  83. package/src/extensions/autocomplete.ts +18 -17
  84. package/src/{hooks/automerge/plugin.ts → extensions/automerge/automerge.ts} +20 -58
  85. package/src/extensions/automerge/cursor.ts +52 -0
  86. package/src/extensions/automerge/defs.ts +62 -0
  87. package/src/extensions/automerge/index.ts +7 -0
  88. package/src/{hooks → extensions}/automerge/semaphore.ts +14 -12
  89. package/src/{hooks → extensions}/automerge/update-automerge.ts +5 -6
  90. package/src/{hooks → extensions}/automerge/update-codemirror.ts +13 -5
  91. package/src/extensions/awareness.ts +309 -0
  92. package/src/extensions/code.ts +14 -7
  93. package/src/extensions/comments.ts +95 -110
  94. package/src/extensions/cursor.ts +46 -0
  95. package/src/extensions/hr.ts +4 -5
  96. package/src/extensions/image.ts +40 -22
  97. package/src/extensions/index.ts +4 -1
  98. package/src/extensions/link.ts +86 -88
  99. package/src/extensions/markdown/highlight.ts +3 -12
  100. package/src/extensions/table.ts +10 -4
  101. package/src/extensions/tasklist.ts +53 -79
  102. package/src/extensions/yjs/cursor.ts +22 -0
  103. package/src/extensions/yjs/index.ts +5 -0
  104. package/src/{hooks → extensions}/yjs/yjs.test.ts +2 -26
  105. package/src/extensions/yjs/yjs.ts +16 -0
  106. package/src/hooks/automerge.ts +52 -0
  107. package/src/hooks/awareness-provider.ts +120 -0
  108. package/src/hooks/index.ts +2 -0
  109. package/src/hooks/useTextModel.ts +20 -97
  110. package/src/hooks/{yjs/space-provider.ts → yjs.ts} +49 -19
  111. package/src/index.ts +3 -0
  112. package/src/styles/index.ts +1 -0
  113. package/src/styles/markdown.ts +0 -2
  114. package/src/styles/tokens.ts +3 -0
  115. package/src/testing/replicator.ts +10 -15
  116. package/src/themes/default.ts +51 -78
  117. package/dist/types/src/extensions/mermaid.d.ts +0 -3
  118. package/dist/types/src/extensions/mermaid.d.ts.map +0 -1
  119. package/dist/types/src/extensions/util.d.ts.map +0 -1
  120. package/dist/types/src/hooks/automerge/automerge.stories.d.ts.map +0 -1
  121. package/dist/types/src/hooks/automerge/handle.d.ts +0 -9
  122. package/dist/types/src/hooks/automerge/handle.d.ts.map +0 -1
  123. package/dist/types/src/hooks/automerge/index.d.ts +0 -3
  124. package/dist/types/src/hooks/automerge/index.d.ts.map +0 -1
  125. package/dist/types/src/hooks/automerge/plugin.d.ts +0 -26
  126. package/dist/types/src/hooks/automerge/plugin.d.ts.map +0 -1
  127. package/dist/types/src/hooks/automerge/semaphore.d.ts.map +0 -1
  128. package/dist/types/src/hooks/automerge/update-automerge.d.ts +0 -6
  129. package/dist/types/src/hooks/automerge/update-automerge.d.ts.map +0 -1
  130. package/dist/types/src/hooks/automerge/update-codemirror.d.ts.map +0 -1
  131. package/dist/types/src/hooks/yjs/cursors.d.ts.map +0 -1
  132. package/dist/types/src/hooks/yjs/index.d.ts +0 -3
  133. package/dist/types/src/hooks/yjs/index.d.ts.map +0 -1
  134. package/dist/types/src/hooks/yjs/space-provider.d.ts.map +0 -1
  135. package/dist/types/src/hooks/yjs/yjs.stories.d.ts.map +0 -1
  136. package/dist/types/src/hooks/yjs/yjs.test.d.ts.map +0 -1
  137. package/src/extensions/mermaid.ts +0 -11
  138. package/src/hooks/automerge/handle.ts +0 -14
  139. package/src/hooks/automerge/index.ts +0 -6
  140. package/src/hooks/yjs/index.ts +0 -6
  141. /package/dist/types/src/{hooks → extensions}/automerge/update-codemirror.d.ts +0 -0
  142. /package/dist/types/src/{hooks → extensions}/yjs/yjs.test.d.ts +0 -0
  143. /package/dist/types/src/{hooks/yjs → styles}/cursors.d.ts +0 -0
  144. /package/dist/types/src/{extensions/util.d.ts → util.d.ts} +0 -0
  145. /package/src/{hooks/yjs → styles}/cursors.ts +0 -0
  146. /package/src/{extensions/util.ts → util.ts} +0 -0
@@ -4,20 +4,20 @@
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
+ Decoration,
10
+ type EditorView,
11
+ WidgetType,
12
+ hoverTooltip,
13
+ ViewPlugin,
14
+ type ViewUpdate,
15
+ type DecorationSet,
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
@@ -34,11 +34,24 @@ export type LinkOptions = {
34
34
  */
35
35
  export const link = (options: LinkOptions = {}): Extension => {
36
36
  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
- }),
37
+ ViewPlugin.fromClass(
38
+ class {
39
+ decorations: DecorationSet;
40
+
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: (v) => v.decorations,
53
+ },
54
+ ),
42
55
  ];
43
56
 
44
57
  if (options.onHover) {
@@ -46,41 +59,25 @@ 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);
69
-
70
- if (!match) {
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
+ }
68
+ const url = link && link.getChild('URL');
69
+ if (!url || !link) {
71
70
  return null;
72
71
  }
73
-
74
- const [, , url] = match ?? [];
75
-
72
+ const urlText = view.state.sliceDoc(url.from, url.to);
76
73
  return {
77
- pos: idx + from,
78
- end: idx + from + match[0].length,
74
+ pos: link.from,
75
+ end: link.to,
79
76
  above: true,
80
77
  create: () => {
81
78
  const el = document.createElement('div');
82
79
  el.className = tooltipContent({}, 'pli-2 plb-1');
83
- options.onHover!(el, url);
80
+ options.onHover!(el, urlText);
84
81
  return { dom: el, offset: { x: 0, y: 4 } };
85
82
  },
86
83
  };
@@ -92,19 +89,15 @@ export const link = (options: LinkOptions = {}): Extension => {
92
89
  };
93
90
 
94
91
  class LinkButton extends WidgetType {
95
- constructor(
96
- private readonly _pos: number,
97
- private readonly _url: string,
98
- private readonly _onAttach: LinkOptions['onRender'],
99
- ) {
92
+ constructor(private readonly _url: string, private readonly _onAttach: LinkOptions['onRender']) {
100
93
  super();
101
94
  }
102
95
 
103
- override eq(other: LinkButton) {
104
- return this._pos === other._pos && this._url === other._url;
96
+ override eq(other: this) {
97
+ return this._url === other._url;
105
98
  }
106
99
 
107
- toDOM(view: EditorView) {
100
+ override toDOM(view: EditorView) {
108
101
  const el = document.createElement('span');
109
102
  this._onAttach!(el, this._url);
110
103
  return el;
@@ -116,11 +109,11 @@ class LinkText extends WidgetType {
116
109
  super();
117
110
  }
118
111
 
119
- override eq(other: LinkText) {
112
+ override eq(other: this) {
120
113
  return this._url === other._url;
121
114
  }
122
115
 
123
- toDOM(view: EditorView) {
116
+ override toDOM(view: EditorView) {
124
117
  const link = document.createElement('a');
125
118
  link.setAttribute('class', 'cm-link');
126
119
  link.textContent = this._text;
@@ -147,46 +140,51 @@ class LinkText extends WidgetType {
147
140
  * possibly overlapping ranges in such a way that they can efficiently be mapped though document changes.
148
141
  * https://codemirror.net/docs/ref/#state
149
142
  */
150
- const update = (state: EditorState, options: LinkOptions) => {
151
- const builder = new RangeSetBuilder();
143
+ const buildDecorations = (view: EditorView, options: LinkOptions): DecorationSet => {
144
+ const builder = new RangeSetBuilder<Decoration>();
145
+ const { state } = view;
152
146
  const cursor = state.selection.main.head;
153
147
 
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
- }
148
+ for (const { from, to } of view.visibleRanges) {
149
+ syntaxTree(state).iterate({
150
+ enter: (node) => {
151
+ // Check if cursor is inside text.
152
+ if (node.name === 'Link') {
153
+ const marks = node.node.getChildren('LinkMark');
154
+ const text = marks.length >= 2 ? state.sliceDoc(marks[0].to, marks[1].from) : '';
155
+ const urlNode = node.node.getChild('URL');
156
+ const url = urlNode ? state.sliceDoc(urlNode.from, urlNode.to) : '';
157
+ if (!url) {
158
+ return false;
159
+ }
165
160
 
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
- }
161
+ if (!view.hasFocus || state.readOnly || cursor < node.from || cursor > node.to) {
162
+ builder.add(
163
+ node.from,
164
+ node.to,
165
+ Decoration.replace({
166
+ widget: new LinkText(text, options.link ? url : undefined),
167
+ }),
168
+ );
169
+ }
175
170
 
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
- }
171
+ if (options.onRender) {
172
+ builder.add(
173
+ node.to,
174
+ node.to,
175
+ Decoration.replace({
176
+ widget: new LinkButton(url, options.onRender),
177
+ }),
178
+ );
179
+ }
185
180
 
186
- return false;
187
- }
188
- },
189
- });
181
+ return false;
182
+ }
183
+ },
184
+ from,
185
+ to,
186
+ });
187
+ }
190
188
 
191
189
  return builder.finish();
192
190
  };
@@ -6,19 +6,18 @@ import { markdownLanguage } from '@codemirror/lang-markdown';
6
6
  import { HighlightStyle } from '@codemirror/language';
7
7
  import { tags, styleTags, Tag } from '@lezer/highlight';
8
8
  import { type MarkdownConfig, Table } from '@lezer/markdown';
9
- import get from 'lodash.get';
10
9
 
11
10
  import {
12
11
  blockquote,
13
12
  bold,
14
13
  code,
15
14
  codeMark,
15
+ getToken,
16
16
  heading,
17
17
  inlineUrl,
18
18
  italic,
19
19
  mark,
20
20
  strikethrough,
21
- tokens,
22
21
  } from '../../styles';
23
22
 
24
23
  /**
@@ -32,7 +31,6 @@ export const markdownTags = {
32
31
  CodeText: Tag.define(),
33
32
  EmphasisMark: Tag.define(),
34
33
  HeaderMark: Tag.define(),
35
- // HorizontalRule: Tag.define(),
36
34
  InlineCode: Tag.define(),
37
35
  LinkLabel: Tag.define(),
38
36
  LinkReference: Tag.define(),
@@ -178,13 +176,6 @@ export const markdownHighlightStyle = (readonly?: boolean) => {
178
176
  class: blockquote,
179
177
  },
180
178
 
181
- // TODO(burdon): Replace with extension.
182
- // {
183
- // tag: [markdownTags.HorizontalRule],
184
- // class: mx(horizontalRule, readonly && '-indent-[100rem]'),
185
- // },
186
-
187
- // TODO(burdon): Only if being edited.
188
179
  {
189
180
  tag: [markdownTags.TableCell],
190
181
  class: 'font-mono',
@@ -193,12 +184,12 @@ export const markdownHighlightStyle = (readonly?: boolean) => {
193
184
  // Main content, paragraphs, etc.
194
185
  // {
195
186
  // tag: [tags.content],
196
- // fontFamily: get(tokens, 'fontFamily.body', []).join(','),
187
+ // fontFamily: getToken('fontFamily.body', []).join(','),
197
188
  // },
198
189
  ],
199
190
  {
200
191
  scope: markdownLanguage,
201
- all: { fontFamily: get(tokens, 'fontFamily.body', []).join(',') },
192
+ all: { fontFamily: getToken('fontFamily.body', []).join(',') },
202
193
  },
203
194
  );
204
195
  };
@@ -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
  }
@@ -2,58 +2,39 @@
2
2
  // Copyright 2023 DXOS.org
3
3
  //
4
4
 
5
- import {
6
- EditorView,
7
- Decoration,
8
- WidgetType,
9
- MatchDecorator,
10
- ViewPlugin,
11
- type DecorationSet,
12
- type ViewUpdate,
13
- } from '@codemirror/view';
14
-
15
- // TODO(burdon): Reconcile with theme.
16
- const styles = EditorView.baseTheme({
17
- '& .cm-task-item': {
18
- paddingLeft: '4px',
19
- paddingRight: '8px',
20
- },
21
- });
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';
22
8
 
23
9
  class CheckboxWidget extends WidgetType {
24
- constructor(
25
- private readonly _pos: number,
26
- private _checked: boolean,
27
- private readonly _indent: number,
28
- private readonly _onCheck?: (check: boolean) => void,
29
- ) {
10
+ constructor(private _checked: boolean) {
30
11
  super();
31
12
  }
32
13
 
33
- override eq(other: CheckboxWidget) {
34
- return this._pos === other._pos && this._checked === other._checked && this._indent === other._indent;
14
+ override eq(other: this) {
15
+ return this._checked === other._checked;
35
16
  }
36
17
 
37
- toDOM(view: EditorView) {
38
- const wrap = document.createElement('span');
39
- wrap.className = 'cm-task-item';
40
- wrap.setAttribute('aria-hidden', 'true');
41
- wrap.style.setProperty('margin-left', this._indent * 24 + 'px');
42
-
43
- const input = wrap.appendChild(document.createElement('input'));
18
+ override toDOM(view: EditorView) {
19
+ const input = document.createElement('input');
44
20
  input.type = 'checkbox';
45
21
  input.checked = this._checked;
46
- if (!this._onCheck) {
22
+ if (view.state.readOnly) {
47
23
  input.setAttribute('disabled', 'true');
48
- }
49
- if (this._onCheck) {
50
- input.onchange = (event: Event) => {
51
- this._onCheck?.((event.target as any).checked);
52
- return true;
24
+ } else {
25
+ input.onmousedown = (event: Event) => {
26
+ const pos = view.posAtDOM(input);
27
+ const text = view.state.sliceDoc(pos, pos + 3);
28
+ if (text === (this._checked ? '[x]' : '[ ]')) {
29
+ view.dispatch({
30
+ changes: { from: pos + 1, to: pos + 2, insert: this._checked ? ' ' : 'x' },
31
+ });
32
+ event.preventDefault();
33
+ }
53
34
  };
54
35
  }
55
36
 
56
- return wrap;
37
+ return input;
57
38
  }
58
39
 
59
40
  override ignoreEvent() {
@@ -61,61 +42,54 @@ class CheckboxWidget extends WidgetType {
61
42
  }
62
43
  }
63
44
 
45
+ const checkedDeco = Decoration.replace({ widget: new CheckboxWidget(true) });
46
+ const uncheckedDeco = Decoration.replace({ widget: new CheckboxWidget(false) });
47
+
64
48
  export type TasklistOptions = {};
65
49
 
66
50
  export const tasklist = (options: TasklistOptions = {}) => {
67
- // TODO(burdon): Matcher isn't as precise as syntax tree.
68
- // Allows for indents to be greater than AST would allow.
69
- const taskMatcher = new MatchDecorator({
70
- regexp: /^(\s*)- \[([ xX])\]\s/g,
71
- decoration: (match, view, pos) => {
72
- const indent = Math.floor(match[1].length / 2);
73
- const checked = match[2] === 'x' || match[2] === 'X';
74
- return Decoration.replace({
75
- widget: new CheckboxWidget(
76
- pos,
77
- checked,
78
- indent,
79
- view.state.readOnly
80
- ? undefined
81
- : (checked) => {
82
- const idx = pos + match[0].indexOf('[') + 1;
83
- view.dispatch({
84
- changes: {
85
- from: idx,
86
- to: idx + 1,
87
- insert: checked ? 'x' : ' ',
88
- },
89
- // TODO(burdon): Restore cursor position? More useful to move to end of line (can indent).
90
- selection: {
91
- anchor: idx + 3,
92
- },
93
- });
94
- },
95
- ),
96
- });
97
- },
98
- });
99
-
100
- const tasks = ViewPlugin.fromClass(
51
+ return ViewPlugin.fromClass(
101
52
  class {
102
- tasks: DecorationSet;
53
+ decorations: DecorationSet;
54
+
103
55
  constructor(view: EditorView) {
104
- this.tasks = taskMatcher.createDeco(view);
56
+ this.decorations = buildDecorations(view);
105
57
  }
106
58
 
107
59
  update(update: ViewUpdate) {
108
- this.tasks = taskMatcher.updateDeco(update, this.tasks);
60
+ if (update.docChanged || update.viewportChanged || update.selectionSet) {
61
+ this.decorations = buildDecorations(update.view);
62
+ }
109
63
  }
110
64
  },
111
65
  {
112
- decorations: (value) => value.tasks,
66
+ decorations: (v) => v.decorations,
113
67
  provide: (plugin) =>
114
68
  EditorView.atomicRanges.of((view) => {
115
- return view.plugin(plugin)?.tasks || Decoration.none;
69
+ return view.plugin(plugin)?.decorations || Decoration.none;
116
70
  }),
117
71
  },
118
72
  );
73
+ };
74
+
75
+ const buildDecorations = (view: EditorView): DecorationSet => {
76
+ const builder = new RangeSetBuilder<Decoration>();
77
+ const { state } = view;
78
+ const cursor = state.selection.main.head;
79
+
80
+ for (const { from, to } of view.visibleRanges) {
81
+ syntaxTree(state).iterate({
82
+ enter: (node) => {
83
+ // Check if cursor is inside text.
84
+ if (node.name === 'TaskMarker' && (cursor < node.from || cursor > node.to)) {
85
+ const checked = state.doc.sliceString(node.from + 1, node.to - 1) === 'x';
86
+ builder.add(node.from, node.to, checked ? checkedDeco : uncheckedDeco);
87
+ }
88
+ },
89
+ from,
90
+ to,
91
+ });
92
+ }
119
93
 
120
- return [tasks, styles];
94
+ return builder.finish();
121
95
  };
@@ -0,0 +1,22 @@
1
+ //
2
+ // Copyright 2024 DXOS.org
3
+ //
4
+
5
+ import * as Y from 'yjs';
6
+
7
+ import { arrayToString, stringToArray } from '@dxos/util';
8
+
9
+ import { type CursorConverter } from '../cursor';
10
+
11
+ export const cursorConverter = (text: Y.Text): CursorConverter => ({
12
+ toCursor: (index, assoc) => {
13
+ return arrayToString(Y.encodeRelativePosition(Y.createRelativePositionFromTypeIndex(text, index, assoc)));
14
+ },
15
+
16
+ fromCursor: (cursor) => {
17
+ return (
18
+ Y.createAbsolutePositionFromRelativePosition(Y.decodeRelativePosition(stringToArray(cursor)), text.doc!)?.index ??
19
+ 0
20
+ );
21
+ },
22
+ });
@@ -0,0 +1,5 @@
1
+ //
2
+ // Copyright 2023 DXOS.org
3
+ //
4
+
5
+ export * from './yjs';
@@ -2,20 +2,17 @@
2
2
  // Copyright 2024 DXOS.org
3
3
  //
4
4
 
5
- import { effect } from '@preact/signals-react';
6
5
  import { expect } from 'chai';
7
6
  import * as Y from 'yjs';
8
7
 
9
- import { Trigger } from '@dxos/async';
10
- import { Expando, setGlobalAutomergePreference, TextObject } from '@dxos/echo-schema';
11
- import { registerSignalFactory } from '@dxos/echo-signals';
8
+ import { TextObject } from '@dxos/echo-schema';
12
9
  import { describe, test } from '@dxos/test';
13
10
  import { type YText } from '@dxos/text-model';
14
11
 
15
12
  describe('YJS', () => {
16
13
  // https://docs.yjs.dev/api/shared-types/y.text
17
14
  // https://github.com/yjs/yjs?tab=readme-ov-file#relative-positions
18
- test('absolute position', () => {
15
+ test.skip('absolute position', () => {
19
16
  const obj = new TextObject('hello world');
20
17
  const index = obj.text.indexOf('world');
21
18
  const relPos = Y.encodeRelativePosition(Y.createRelativePositionFromTypeIndex(obj.content as YText, index));
@@ -35,25 +32,4 @@ describe('YJS', () => {
35
32
  expect(obj.text.substring(cursor!.index)).to.equal('world');
36
33
  }
37
34
  });
38
-
39
- // TODO(burdon): Test deleting end character.
40
-
41
- test.skip('effect', async () => {
42
- setGlobalAutomergePreference(true);
43
- registerSignalFactory();
44
- const obj = new Expando({ comments: [] });
45
-
46
- const trigger = new Trigger();
47
- const unsubscribe = effect(() => {
48
- // TODO(burdon): BUG: doesn't work with YJS.
49
- console.log(obj.comments);
50
- if (obj.comments.length > 0) {
51
- trigger.wake();
52
- }
53
- });
54
-
55
- obj.comments.push({ message: 'hello' });
56
- await trigger.wait();
57
- unsubscribe();
58
- });
59
35
  });
@@ -0,0 +1,16 @@
1
+ //
2
+ // Copyright 2024 DXOS.org
3
+ //
4
+
5
+ import { yCollab } from 'y-codemirror.next';
6
+ import type * as YP from 'y-protocols/awareness';
7
+
8
+ import type { YText } from '@dxos/text-model';
9
+
10
+ import { cursorConverter } from './cursor';
11
+ import { Cursor } from '../cursor';
12
+
13
+ export const yjs = (content: YText, awareness?: YP.Awareness) => [
14
+ Cursor.converter.of(cursorConverter(content)),
15
+ yCollab(content, awareness),
16
+ ];
@@ -0,0 +1,52 @@
1
+ //
2
+ // Copyright 2024 DXOS.org
3
+ //
4
+
5
+ import get from 'lodash.get';
6
+
7
+ import { type AutomergeTextCompat, getRawDoc } from '@dxos/echo-schema';
8
+ import { isNotNullOrUndefined } from '@dxos/util';
9
+
10
+ import { SpaceAwarenessProvider } from './awareness-provider';
11
+ import { type EditorModel, modelState, type UseTextModelProps } from './useTextModel';
12
+ import { automerge, awareness, AwarenessProvider } from '../extensions';
13
+ import { cursorColor } from '../styles';
14
+
15
+ export const createAutomergeModel = ({ space, identity, text }: UseTextModelProps): EditorModel => {
16
+ const obj = text as any as AutomergeTextCompat;
17
+ const doc = getRawDoc(obj, [obj.field]);
18
+
19
+ const awarenessProvider =
20
+ space &&
21
+ new SpaceAwarenessProvider({
22
+ space,
23
+ channel: `automerge.awareness.${obj.id}`,
24
+ info: {
25
+ displayName: identity?.profile?.displayName ?? '',
26
+ color: cursorColor.color,
27
+ lightColor: cursorColor.light,
28
+ },
29
+ peerId: identity?.identityKey.toHex() ?? 'Anonymous',
30
+ });
31
+
32
+ const model: EditorModel = {
33
+ id: obj.id,
34
+ content: doc,
35
+ text: () => get(doc.handle.docSync(), doc.path),
36
+ // TODO(burdon): https://automerge.org/automerge/api-docs/js/functions/next.getCursor.html
37
+ extension: [
38
+ modelState.init(() => model),
39
+ automerge({ handle: doc.handle, path: doc.path }),
40
+ awarenessProvider && AwarenessProvider.of(awarenessProvider),
41
+ awareness(),
42
+ ].filter(isNotNullOrUndefined),
43
+ peer: identity
44
+ ? {
45
+ id: identity.identityKey.toHex(),
46
+ name: identity.profile?.displayName,
47
+ }
48
+ : undefined,
49
+ };
50
+
51
+ return model;
52
+ };