@dxos/react-ui-editor 0.3.11-main.9bacd9f → 0.3.11-main.9c729f3

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 (195) hide show
  1. package/dist/lib/browser/index.mjs +1460 -986
  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/TextEditor.d.ts.map +1 -1
  5. package/dist/types/src/components/TextEditor/TextEditor.stories.d.ts +3 -0
  6. package/dist/types/src/components/TextEditor/TextEditor.stories.d.ts.map +1 -1
  7. package/dist/types/src/{hooks/automerge → components/TextEditor}/automerge.stories.d.ts +6 -1
  8. package/dist/types/src/components/TextEditor/automerge.stories.d.ts.map +1 -0
  9. package/dist/types/src/components/TextEditor/codemirror.stories.d.ts.map +1 -1
  10. package/dist/types/src/components/TextEditor/index.d.ts +0 -2
  11. package/dist/types/src/components/TextEditor/index.d.ts.map +1 -1
  12. package/dist/types/src/{hooks/yjs → components/TextEditor}/yjs.stories.d.ts +1 -1
  13. package/dist/types/src/components/TextEditor/yjs.stories.d.ts.map +1 -0
  14. package/dist/types/src/extensions/autocomplete.d.ts.map +1 -0
  15. package/dist/types/src/extensions/automerge/automerge.d.ts +9 -0
  16. package/dist/types/src/extensions/automerge/automerge.d.ts.map +1 -0
  17. package/dist/types/src/extensions/automerge/cursor.d.ts +5 -0
  18. package/dist/types/src/extensions/automerge/cursor.d.ts.map +1 -0
  19. package/dist/types/src/extensions/automerge/defs.d.ts +26 -0
  20. package/dist/types/src/extensions/automerge/defs.d.ts.map +1 -0
  21. package/dist/types/src/extensions/automerge/index.d.ts +3 -0
  22. package/dist/types/src/extensions/automerge/index.d.ts.map +1 -0
  23. package/dist/types/src/{hooks → extensions}/automerge/semaphore.d.ts +4 -4
  24. package/dist/types/src/extensions/automerge/semaphore.d.ts.map +1 -0
  25. package/dist/types/src/extensions/automerge/update-automerge.d.ts +5 -0
  26. package/dist/types/src/extensions/automerge/update-automerge.d.ts.map +1 -0
  27. package/dist/types/src/extensions/automerge/update-codemirror.d.ts.map +1 -0
  28. package/dist/types/src/extensions/awareness.d.ts +46 -0
  29. package/dist/types/src/extensions/awareness.d.ts.map +1 -0
  30. package/dist/types/src/extensions/basic.d.ts.map +1 -0
  31. package/dist/types/src/extensions/blast.d.ts.map +1 -0
  32. package/dist/types/src/extensions/code.d.ts.map +1 -0
  33. package/dist/types/src/{components/TextEditor/extensions → extensions}/comments.d.ts +4 -2
  34. package/dist/types/src/extensions/comments.d.ts.map +1 -0
  35. package/dist/types/src/extensions/hr.d.ts.map +1 -0
  36. package/dist/types/src/extensions/image.d.ts.map +1 -0
  37. package/dist/types/src/{components/TextEditor/extensions → extensions}/index.d.ts +3 -0
  38. package/dist/types/src/extensions/index.d.ts.map +1 -0
  39. package/dist/types/src/extensions/link.d.ts.map +1 -0
  40. package/dist/types/src/extensions/listener.d.ts.map +1 -0
  41. package/dist/types/src/extensions/markdown/bundle.d.ts.map +1 -0
  42. package/dist/types/src/extensions/markdown/highlight.d.ts.map +1 -0
  43. package/dist/types/src/extensions/markdown/index.d.ts.map +1 -0
  44. package/dist/types/src/extensions/mention.d.ts.map +1 -0
  45. package/dist/types/src/extensions/mermaid.d.ts.map +1 -0
  46. package/dist/types/src/extensions/table.d.ts.map +1 -0
  47. package/dist/types/src/extensions/tasklist.d.ts.map +1 -0
  48. package/dist/types/src/extensions/typewriter.d.ts.map +1 -0
  49. package/dist/types/src/extensions/yjs/cursor.d.ts +4 -0
  50. package/dist/types/src/extensions/yjs/cursor.d.ts.map +1 -0
  51. package/dist/types/src/extensions/yjs/index.d.ts +2 -0
  52. package/dist/types/src/extensions/yjs/index.d.ts.map +1 -0
  53. package/dist/types/src/extensions/yjs/yjs.d.ts +4 -0
  54. package/dist/types/src/extensions/yjs/yjs.d.ts.map +1 -0
  55. package/dist/types/src/extensions/yjs/yjs.test.d.ts.map +1 -0
  56. package/dist/types/src/hooks/automerge.d.ts +3 -0
  57. package/dist/types/src/hooks/automerge.d.ts.map +1 -0
  58. package/dist/types/src/hooks/awareness-provider.d.ts +28 -0
  59. package/dist/types/src/hooks/awareness-provider.d.ts.map +1 -0
  60. package/dist/types/src/hooks/index.d.ts +2 -0
  61. package/dist/types/src/hooks/index.d.ts.map +1 -1
  62. package/dist/types/src/hooks/useTextModel.d.ts +9 -8
  63. package/dist/types/src/hooks/useTextModel.d.ts.map +1 -1
  64. package/dist/types/src/hooks/{yjs/space-provider.d.ts → yjs.d.ts} +8 -8
  65. package/dist/types/src/hooks/yjs.d.ts.map +1 -0
  66. package/dist/types/src/index.d.ts +2 -0
  67. package/dist/types/src/index.d.ts.map +1 -1
  68. package/dist/types/src/styles/cursors.d.ts.map +1 -0
  69. package/dist/types/src/styles/index.d.ts +1 -0
  70. package/dist/types/src/styles/index.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/{components/TextEditor/themes → themes}/default.d.ts +1 -1
  74. package/dist/types/src/themes/default.d.ts.map +1 -0
  75. package/dist/types/src/themes/index.d.ts.map +1 -0
  76. package/dist/types/src/util/cursor.d.ts +17 -0
  77. package/dist/types/src/util/cursor.d.ts.map +1 -0
  78. package/dist/types/src/util/index.d.ts +3 -0
  79. package/dist/types/src/util/index.d.ts.map +1 -0
  80. package/dist/types/src/util/util.d.ts.map +1 -0
  81. package/package.json +22 -21
  82. package/src/components/TextEditor/MarkdownEditor.stories.tsx +1 -1
  83. package/src/components/TextEditor/TextEditor.stories.tsx +14 -1
  84. package/src/components/TextEditor/TextEditor.tsx +3 -3
  85. package/src/{hooks/automerge → components/TextEditor}/automerge.stories.tsx +66 -6
  86. package/src/components/TextEditor/codemirror.stories.ts +3 -2
  87. package/src/components/TextEditor/index.ts +0 -3
  88. package/src/{hooks/yjs → components/TextEditor}/yjs.stories.tsx +1 -1
  89. package/src/extensions/automerge/automerge.ts +94 -0
  90. package/src/extensions/automerge/cursor.ts +51 -0
  91. package/src/extensions/automerge/defs.ts +62 -0
  92. package/src/extensions/automerge/index.ts +6 -0
  93. package/src/{hooks → extensions}/automerge/semaphore.ts +14 -12
  94. package/src/{hooks → extensions}/automerge/update-automerge.ts +5 -6
  95. package/src/{hooks → extensions}/automerge/update-codemirror.ts +13 -5
  96. package/src/extensions/awareness.ts +316 -0
  97. package/src/{components/TextEditor/extensions → extensions}/code.ts +1 -1
  98. package/src/{components/TextEditor/extensions → extensions}/comments.ts +51 -19
  99. package/src/{components/TextEditor/extensions → extensions}/hr.ts +1 -1
  100. package/src/{components/TextEditor/extensions → extensions}/index.ts +3 -0
  101. package/src/{components/TextEditor/extensions → extensions}/markdown/highlight.ts +1 -1
  102. package/src/{components/TextEditor/extensions → extensions}/typewriter.ts +1 -1
  103. package/src/extensions/yjs/cursor.ts +21 -0
  104. package/src/extensions/yjs/index.ts +5 -0
  105. package/src/{hooks → extensions}/yjs/yjs.test.ts +1 -25
  106. package/src/extensions/yjs/yjs.ts +16 -0
  107. package/src/hooks/automerge.ts +52 -0
  108. package/src/hooks/awareness-provider.ts +120 -0
  109. package/src/hooks/index.ts +2 -0
  110. package/src/hooks/useTextModel.ts +20 -93
  111. package/src/hooks/{yjs/space-provider.ts → yjs.ts} +71 -19
  112. package/src/index.ts +2 -0
  113. package/src/styles/index.ts +1 -0
  114. package/src/testing/replicator.ts +10 -15
  115. package/src/{components/TextEditor/themes → themes}/default.ts +1 -1
  116. package/src/util/cursor.ts +29 -0
  117. package/src/util/index.ts +6 -0
  118. package/dist/types/src/components/TextEditor/extensions/autocomplete.d.ts.map +0 -1
  119. package/dist/types/src/components/TextEditor/extensions/basic.d.ts.map +0 -1
  120. package/dist/types/src/components/TextEditor/extensions/blast.d.ts.map +0 -1
  121. package/dist/types/src/components/TextEditor/extensions/code.d.ts.map +0 -1
  122. package/dist/types/src/components/TextEditor/extensions/comments.d.ts.map +0 -1
  123. package/dist/types/src/components/TextEditor/extensions/hr.d.ts.map +0 -1
  124. package/dist/types/src/components/TextEditor/extensions/image.d.ts.map +0 -1
  125. package/dist/types/src/components/TextEditor/extensions/index.d.ts.map +0 -1
  126. package/dist/types/src/components/TextEditor/extensions/link.d.ts.map +0 -1
  127. package/dist/types/src/components/TextEditor/extensions/listener.d.ts.map +0 -1
  128. package/dist/types/src/components/TextEditor/extensions/markdown/bundle.d.ts.map +0 -1
  129. package/dist/types/src/components/TextEditor/extensions/markdown/highlight.d.ts.map +0 -1
  130. package/dist/types/src/components/TextEditor/extensions/markdown/index.d.ts.map +0 -1
  131. package/dist/types/src/components/TextEditor/extensions/mention.d.ts.map +0 -1
  132. package/dist/types/src/components/TextEditor/extensions/mermaid.d.ts.map +0 -1
  133. package/dist/types/src/components/TextEditor/extensions/table.d.ts.map +0 -1
  134. package/dist/types/src/components/TextEditor/extensions/tasklist.d.ts.map +0 -1
  135. package/dist/types/src/components/TextEditor/extensions/typewriter.d.ts.map +0 -1
  136. package/dist/types/src/components/TextEditor/extensions/util.d.ts.map +0 -1
  137. package/dist/types/src/components/TextEditor/themes/default.d.ts.map +0 -1
  138. package/dist/types/src/components/TextEditor/themes/index.d.ts.map +0 -1
  139. package/dist/types/src/hooks/automerge/automerge.stories.d.ts.map +0 -1
  140. package/dist/types/src/hooks/automerge/handle.d.ts +0 -9
  141. package/dist/types/src/hooks/automerge/handle.d.ts.map +0 -1
  142. package/dist/types/src/hooks/automerge/index.d.ts +0 -3
  143. package/dist/types/src/hooks/automerge/index.d.ts.map +0 -1
  144. package/dist/types/src/hooks/automerge/plugin.d.ts +0 -26
  145. package/dist/types/src/hooks/automerge/plugin.d.ts.map +0 -1
  146. package/dist/types/src/hooks/automerge/semaphore.d.ts.map +0 -1
  147. package/dist/types/src/hooks/automerge/update-automerge.d.ts +0 -6
  148. package/dist/types/src/hooks/automerge/update-automerge.d.ts.map +0 -1
  149. package/dist/types/src/hooks/automerge/update-codemirror.d.ts.map +0 -1
  150. package/dist/types/src/hooks/yjs/cursors.d.ts.map +0 -1
  151. package/dist/types/src/hooks/yjs/index.d.ts +0 -3
  152. package/dist/types/src/hooks/yjs/index.d.ts.map +0 -1
  153. package/dist/types/src/hooks/yjs/space-provider.d.ts.map +0 -1
  154. package/dist/types/src/hooks/yjs/yjs.stories.d.ts.map +0 -1
  155. package/dist/types/src/hooks/yjs/yjs.test.d.ts.map +0 -1
  156. package/src/hooks/automerge/handle.ts +0 -14
  157. package/src/hooks/automerge/index.ts +0 -6
  158. package/src/hooks/automerge/plugin.ts +0 -134
  159. package/src/hooks/yjs/index.ts +0 -6
  160. /package/dist/types/src/{components/TextEditor/extensions → extensions}/autocomplete.d.ts +0 -0
  161. /package/dist/types/src/{hooks → extensions}/automerge/update-codemirror.d.ts +0 -0
  162. /package/dist/types/src/{components/TextEditor/extensions → extensions}/basic.d.ts +0 -0
  163. /package/dist/types/src/{components/TextEditor/extensions → extensions}/blast.d.ts +0 -0
  164. /package/dist/types/src/{components/TextEditor/extensions → extensions}/code.d.ts +0 -0
  165. /package/dist/types/src/{components/TextEditor/extensions → extensions}/hr.d.ts +0 -0
  166. /package/dist/types/src/{components/TextEditor/extensions → extensions}/image.d.ts +0 -0
  167. /package/dist/types/src/{components/TextEditor/extensions → extensions}/link.d.ts +0 -0
  168. /package/dist/types/src/{components/TextEditor/extensions → extensions}/listener.d.ts +0 -0
  169. /package/dist/types/src/{components/TextEditor/extensions → extensions}/markdown/bundle.d.ts +0 -0
  170. /package/dist/types/src/{components/TextEditor/extensions → extensions}/markdown/highlight.d.ts +0 -0
  171. /package/dist/types/src/{components/TextEditor/extensions → extensions}/markdown/index.d.ts +0 -0
  172. /package/dist/types/src/{components/TextEditor/extensions → extensions}/mention.d.ts +0 -0
  173. /package/dist/types/src/{components/TextEditor/extensions → extensions}/mermaid.d.ts +0 -0
  174. /package/dist/types/src/{components/TextEditor/extensions → extensions}/table.d.ts +0 -0
  175. /package/dist/types/src/{components/TextEditor/extensions → extensions}/tasklist.d.ts +0 -0
  176. /package/dist/types/src/{components/TextEditor/extensions → extensions}/typewriter.d.ts +0 -0
  177. /package/dist/types/src/{hooks → extensions}/yjs/yjs.test.d.ts +0 -0
  178. /package/dist/types/src/{hooks/yjs → styles}/cursors.d.ts +0 -0
  179. /package/dist/types/src/{components/TextEditor/themes → themes}/index.d.ts +0 -0
  180. /package/dist/types/src/{components/TextEditor/extensions → util}/util.d.ts +0 -0
  181. /package/src/{components/TextEditor/extensions → extensions}/autocomplete.ts +0 -0
  182. /package/src/{components/TextEditor/extensions → extensions}/basic.ts +0 -0
  183. /package/src/{components/TextEditor/extensions → extensions}/blast.ts +0 -0
  184. /package/src/{components/TextEditor/extensions → extensions}/image.ts +0 -0
  185. /package/src/{components/TextEditor/extensions → extensions}/link.ts +0 -0
  186. /package/src/{components/TextEditor/extensions → extensions}/listener.ts +0 -0
  187. /package/src/{components/TextEditor/extensions → extensions}/markdown/bundle.ts +0 -0
  188. /package/src/{components/TextEditor/extensions → extensions}/markdown/index.ts +0 -0
  189. /package/src/{components/TextEditor/extensions → extensions}/mention.ts +0 -0
  190. /package/src/{components/TextEditor/extensions → extensions}/mermaid.ts +0 -0
  191. /package/src/{components/TextEditor/extensions → extensions}/table.ts +0 -0
  192. /package/src/{components/TextEditor/extensions → extensions}/tasklist.ts +0 -0
  193. /package/src/{hooks/yjs → styles}/cursors.ts +0 -0
  194. /package/src/{components/TextEditor/themes → themes}/index.ts +0 -0
  195. /package/src/{components/TextEditor/extensions → util}/util.ts +0 -0
@@ -16,6 +16,7 @@ import {
16
16
  WidgetType,
17
17
  type Rect,
18
18
  } from '@codemirror/view';
19
+ import get from 'lodash.get';
19
20
  import sortBy from 'lodash.sortby';
20
21
 
21
22
  import { debounce } from '@dxos/async';
@@ -23,10 +24,10 @@ import { invariant } from '@dxos/invariant';
23
24
  import { log } from '@dxos/log';
24
25
  import { nonNullable } from '@dxos/util';
25
26
 
26
- import { callbackWrapper } from './util';
27
- import { type CommentRange, type EditorModel, modelState, type Range } from '../../../hooks';
27
+ import { type CommentRange, type EditorModel, type Range } from '../hooks';
28
+ import { tokens } from '../styles';
29
+ import { callbackWrapper, CursorConverter } from '../util';
28
30
 
29
- // TODO(burdon): Handle scroll to.
30
31
  // TODO(burdon): Handle delete, cut, copy, and paste (separately) text that includes comment range.
31
32
  // TODO(burdon): Consider breaking into separate plugin (since not standalone)? Like mermaid?
32
33
 
@@ -42,10 +43,10 @@ const styles = EditorView.baseTheme({
42
43
  backgroundColor: 'orange',
43
44
  },
44
45
  '& .cm-comment': {
45
- backgroundColor: 'yellow',
46
+ backgroundColor: get(tokens, 'extend.colors.yellow.50'),
46
47
  },
47
48
  '& .cm-comment-active': {
48
- backgroundColor: 'orange',
49
+ backgroundColor: get(tokens, 'extend.colors.yellow.100'),
49
50
  },
50
51
  });
51
52
 
@@ -71,9 +72,22 @@ export type CommentsState = CommentSelected & {
71
72
  ranges: ExtendedCommentRange[];
72
73
  };
73
74
 
75
+ export const setFocus = (view: EditorView, thread: string) => {
76
+ const range = view.state.field(commentsStateField).ranges.find((range) => range.id === thread);
77
+ if (!range) {
78
+ return;
79
+ }
80
+
81
+ view.dispatch({
82
+ effects: setSelection.of({ active: thread }),
83
+ selection: { anchor: range.from },
84
+ scrollIntoView: true,
85
+ });
86
+ };
87
+
74
88
  export const setCommentRange = StateEffect.define<{ model: EditorModel; comments: CommentRange[] }>();
75
89
 
76
- const setSelection = StateEffect.define<CommentSelected>();
90
+ export const setSelection = StateEffect.define<CommentSelected>();
77
91
 
78
92
  const setCommentState = StateEffect.define<CommentsState>();
79
93
 
@@ -85,6 +99,8 @@ const setCommentState = StateEffect.define<CommentsState>();
85
99
  const commentsStateField = StateField.define<CommentsState>({
86
100
  create: () => ({ ranges: [] }),
87
101
  update: (value, tr) => {
102
+ const cursorConverter = tr.state.facet(CursorConverter);
103
+
88
104
  for (const effect of tr.effects) {
89
105
  // Update selection.
90
106
  if (effect.is(setSelection)) {
@@ -93,11 +109,13 @@ const commentsStateField = StateField.define<CommentsState>({
93
109
 
94
110
  // Update range from store.
95
111
  if (effect.is(setCommentRange)) {
96
- const { model, comments } = effect.value;
97
- const ranges: ExtendedCommentRange[] = comments.map((comment) => {
98
- const range = model.getRangeFromCursor!(comment.cursor)!;
99
- return { ...comment, ...range };
100
- });
112
+ const { comments } = effect.value;
113
+ const ranges: ExtendedCommentRange[] = comments
114
+ .map((comment) => {
115
+ const range = getRangeFromCursor(cursorConverter, comment.cursor);
116
+ return range && { ...comment, ...range };
117
+ })
118
+ .filter(nonNullable);
101
119
 
102
120
  return { ...value, ranges };
103
121
  }
@@ -202,14 +220,15 @@ export const comments = (options: CommentsOptions = {}): Extension => {
202
220
  * Create comment thread action.
203
221
  */
204
222
  const createCommentThread: Command = (view) => {
223
+ const cursorConverter = view.state.facet(CursorConverter);
224
+
205
225
  invariant(options.onCreate);
206
226
  const { head, from, to } = view.state.selection.main;
207
227
  if (from === to) {
208
228
  return false;
209
229
  }
210
230
 
211
- const model = view.state.field(modelState);
212
- const relPos = model?.getCursorFromRange?.({ from, to });
231
+ const relPos = getCursorFromRange(cursorConverter, { from, to });
213
232
  if (relPos) {
214
233
  // Create thread via callback.
215
234
  const id = callbackWrapper(options.onCreate)(relPos);
@@ -322,27 +341,27 @@ export const comments = (options: CommentsOptions = {}): Extension => {
322
341
  // Monitor cursor movement and text updates.
323
342
  // TODO(burdon): Is there a better (finer grained) way to do this?
324
343
  //
325
- EditorView.updateListener.of(({ view, state, startState, changes }) => {
344
+ EditorView.updateListener.of(({ view, state, changes }) => {
345
+ const cursorConverter = view.state.facet(CursorConverter);
346
+
326
347
  //
327
348
  // Test if need to recompute indexed range if document changes before the end of the range.
328
349
  //
329
350
  {
330
351
  let mod = false;
331
- const model = state.field(modelState);
332
352
  const { active, ranges } = state.field(commentsStateField);
333
353
  changes.iterChanges((from, to, from2, to2) => {
334
- invariant(model);
335
354
  ranges.forEach((range) => {
336
355
  if (from2 === to2) {
337
- const newRange = model.getRangeFromCursor!(range.cursor);
338
- if (newRange.to - newRange.from === 0) {
356
+ const newRange = getRangeFromCursor(cursorConverter, range.cursor);
357
+ if (!newRange || newRange.to - newRange.from === 0) {
339
358
  // TODO(burdon): Delete range if empty.
340
359
  log.info('deleted comment', { thread: range.id });
341
360
  }
342
361
  }
343
362
 
344
363
  if (from <= range.to) {
345
- const newRange = model.getRangeFromCursor!(range.cursor);
364
+ const newRange = getRangeFromCursor(cursorConverter, range.cursor);
346
365
  Object.assign(range, newRange);
347
366
  mod = true;
348
367
  }
@@ -387,3 +406,16 @@ export const comments = (options: CommentsOptions = {}): Extension => {
387
406
  }),
388
407
  ];
389
408
  };
409
+
410
+ const getCursorFromRange = (cursorConverter: CursorConverter, range: Range) => {
411
+ const from = cursorConverter.toCursor(range.from);
412
+ const to = cursorConverter.toCursor(range.to, -1);
413
+ return [from, to].join(':');
414
+ };
415
+
416
+ const getRangeFromCursor = (cursorConverter: CursorConverter, cursor: string) => {
417
+ const parts = cursor.split(':');
418
+ const from = cursorConverter.fromCursor(parts[0]);
419
+ const to = cursorConverter.fromCursor(parts[1]);
420
+ return from && to ? { from, to } : undefined;
421
+ };
@@ -13,7 +13,7 @@ import {
13
13
  } from '@codemirror/view';
14
14
  import get from 'lodash.get';
15
15
 
16
- import { tokens } from '../../../styles';
16
+ import { tokens } from '../styles';
17
17
 
18
18
  // TODO(burdon): Reconcile with theme.
19
19
  const styles = EditorView.baseTheme({
@@ -3,6 +3,8 @@
3
3
  //
4
4
 
5
5
  export * from './autocomplete';
6
+ export * from './automerge';
7
+ export * from './awareness';
6
8
  export * from './basic';
7
9
  export * from './blast';
8
10
  export * from './code';
@@ -17,3 +19,4 @@ export * from './mermaid';
17
19
  export * from './table';
18
20
  export * from './tasklist';
19
21
  export * from './typewriter';
22
+ export * from './yjs';
@@ -19,7 +19,7 @@ import {
19
19
  mark,
20
20
  strikethrough,
21
21
  tokens,
22
- } from '../../../../styles';
22
+ } from '../../styles';
23
23
 
24
24
  /**
25
25
  * Custom tags defined and processed by the GFM lezer extension.
@@ -19,7 +19,7 @@ const defaultItems = ['hello world!', 'this is a test.', 'this is [DXOS](https:/
19
19
  */
20
20
  export const typewriter = ({ delay = 75, items = defaultItems }: DemoOptions = {}): Extension => {
21
21
  let t: any;
22
- let idx = 0;
22
+ let idx = 0; // TODO(burdon): Make global.
23
23
 
24
24
  return [
25
25
  keymap.of([
@@ -0,0 +1,21 @@
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 '../../util';
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
+ fromCursor: (cursor) => {
16
+ return (
17
+ Y.createAbsolutePositionFromRelativePosition(Y.decodeRelativePosition(stringToArray(cursor)), text.doc!)?.index ??
18
+ 0
19
+ );
20
+ },
21
+ });
@@ -0,0 +1,5 @@
1
+ //
2
+ // Copyright 2023 DXOS.org
3
+ //
4
+
5
+ export * from './yjs';
@@ -2,13 +2,10 @@
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
 
@@ -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 { CursorConverter } from '../../util';
12
+
13
+ export const yjs = (content: YText, awareness?: YP.Awareness) => [
14
+ CursorConverter.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
+ };
@@ -0,0 +1,120 @@
1
+ //
2
+ // Copyright 2024 DXOS.org
3
+ //
4
+
5
+ import { DeferredTask, Event, sleep } from '@dxos/async';
6
+ import { Context } from '@dxos/context';
7
+ import { invariant } from '@dxos/invariant';
8
+ import { log } from '@dxos/log';
9
+ import { type Space } from '@dxos/react-client/echo';
10
+ import { type GossipMessage } from '@dxos/react-client/mesh';
11
+
12
+ import { type AwarenessInfo, type AwarenessPosition, type AwarenessProvider, type AwarenessState } from '../extensions';
13
+
14
+ type ProtocolMessage =
15
+ | {
16
+ kind: 'query';
17
+ }
18
+ | {
19
+ kind: 'post';
20
+ state: AwarenessState;
21
+ };
22
+
23
+ const DEBOUNCE_INTERVAL = 100; // ms
24
+
25
+ export type AwarenessProviderParams = {
26
+ space: Space;
27
+ channel: string;
28
+ peerId: string;
29
+ info: AwarenessInfo;
30
+ };
31
+
32
+ export class SpaceAwarenessProvider implements AwarenessProvider {
33
+ public readonly remoteStateChange = new Event<void>();
34
+
35
+ private readonly _remoteStates = new Map<string, AwarenessState>();
36
+
37
+ private readonly _space: Space;
38
+ private readonly _channel: string;
39
+ private readonly _peerId: string;
40
+ private readonly _info: AwarenessInfo;
41
+
42
+ private _localState?: AwarenessState;
43
+ private _postTask?: DeferredTask;
44
+ private _ctx?: Context;
45
+
46
+ constructor(params: AwarenessProviderParams) {
47
+ this._space = params.space;
48
+ this._channel = params.channel;
49
+ this._peerId = params.peerId;
50
+ this._info = params.info;
51
+ }
52
+
53
+ open() {
54
+ this._ctx = new Context();
55
+ this._postTask = new DeferredTask(this._ctx, async () => {
56
+ if (this._localState) {
57
+ await this._space.postMessage(this._channel, {
58
+ kind: 'post',
59
+ state: this._localState,
60
+ } satisfies ProtocolMessage);
61
+
62
+ await sleep(DEBOUNCE_INTERVAL);
63
+ }
64
+ });
65
+
66
+ const unsubscribe = this._space.listen(this._channel, (message: GossipMessage) => {
67
+ switch (message.payload.kind) {
68
+ case 'query': {
69
+ this._handleQueryMessage();
70
+ break;
71
+ }
72
+ case 'post': {
73
+ this._handlePostMessage(message.payload);
74
+ break;
75
+ }
76
+ }
77
+ });
78
+ this._ctx.onDispose(unsubscribe);
79
+
80
+ void this._space
81
+ .postMessage(this._channel, {
82
+ kind: 'query',
83
+ } satisfies ProtocolMessage)
84
+ .catch((err) => {
85
+ log.debug('failed to query awareness', { err });
86
+ });
87
+ }
88
+
89
+ close() {
90
+ void this._ctx?.dispose();
91
+ this._ctx = undefined;
92
+ this._postTask = undefined;
93
+ }
94
+
95
+ update(position: AwarenessPosition | undefined): void {
96
+ invariant(this._postTask);
97
+ this._localState = {
98
+ peerId: this._peerId,
99
+ position,
100
+ info: this._info,
101
+ };
102
+
103
+ this._postTask.schedule();
104
+ }
105
+
106
+ getRemoteStates(): AwarenessState[] {
107
+ return Array.from(this._remoteStates.values());
108
+ }
109
+
110
+ private _handleQueryMessage() {
111
+ invariant(this._postTask);
112
+ this._postTask.schedule();
113
+ }
114
+
115
+ private _handlePostMessage(message: ProtocolMessage) {
116
+ invariant(message.kind === 'post');
117
+ this._remoteStates.set(message.state.peerId, message.state);
118
+ this.remoteStateChange.emit();
119
+ }
120
+ }
@@ -2,5 +2,7 @@
2
2
  // Copyright 2023 DXOS.org
3
3
  //
4
4
 
5
+ export * from './automerge';
6
+ export * from './awareness-provider';
5
7
  export * from './useTextModel';
6
8
  export * from './yjs';
@@ -2,36 +2,22 @@
2
2
  // Copyright 2023 DXOS.org
3
3
  //
4
4
 
5
- import { type Extension, StateField } from '@codemirror/state';
6
- import get from 'lodash.get';
5
+ import { StateField, type Extension } from '@codemirror/state';
7
6
  import { useEffect, useState } from 'react';
8
- import { yCollab } from 'y-codemirror.next';
9
- import type * as awarenessProtocol from 'y-protocols/awareness';
10
- import * as Y from 'yjs';
7
+ import type * as YP from 'y-protocols/awareness';
11
8
 
12
- import { invariant } from '@dxos/invariant';
13
- import {
14
- type DocAccessor,
15
- type Space,
16
- type TextObject,
17
- type AutomergeTextCompat,
18
- getRawDoc,
19
- isActualAutomergeObject,
20
- } from '@dxos/react-client/echo';
9
+ import { isAutomergeObject, type DocAccessor, type Space, type TextObject } from '@dxos/react-client/echo';
21
10
  import { type Identity } from '@dxos/react-client/halo';
22
11
  import type { YText, YXmlFragment } from '@dxos/text-model';
23
- import { arrayToString, stringToArray } from '@dxos/util';
24
12
 
25
- import { automergePlugin } from './automerge';
26
- import { SpaceAwarenessProvider } from './yjs';
13
+ import { createAutomergeModel } from './automerge';
14
+ import { createYjsModel } from './yjs';
27
15
 
28
- // TODO(burdon): Move.
29
- type Awareness = awarenessProtocol.Awareness;
16
+ // TODO(burdon): Factor out defs.
30
17
 
31
18
  /**
32
19
  * State field makes the model available to other extensions.
33
20
  */
34
- // TODO(burdon): Use facet?
35
21
  export const modelState = StateField.define<EditorModel | undefined>({
36
22
  create: () => undefined,
37
23
  update: (model) => model,
@@ -44,20 +30,25 @@ export type Range = {
44
30
 
45
31
  export type CommentRange = {
46
32
  id: string;
47
- // TODO(burdon): Split into begin/end?
48
33
  cursor: string;
49
34
  };
50
35
 
51
36
  // TODO(wittjosiah): Factor out to common package? @dxos/react-client?
52
37
  export type EditorModel = {
53
38
  id: string;
39
+ text: () => string;
40
+ extension?: Extension;
41
+
54
42
  // TODO(burdon): Remove.
55
43
  content: string | YText | YXmlFragment | DocAccessor;
56
- text: () => string;
44
+ /**
45
+ * @deprecated Use CursorConverter.
46
+ */
57
47
  getCursorFromRange?: (value: Range) => string;
58
- getRangeFromCursor?: (cursor: string) => Range;
59
- extension?: Extension;
60
- awareness?: Awareness;
48
+ getRangeFromCursor?: (cursor: string) => Range | undefined;
49
+ // TODO(burdon): Move into extension.
50
+ awareness?: YP.Awareness;
51
+ // TODO(burdon): Remove.
61
52
  peer?: {
62
53
  id: string;
63
54
  name?: string;
@@ -82,75 +73,11 @@ export const useTextModel = (props: UseTextModelProps): EditorModel | undefined
82
73
 
83
74
  const createModel = (props: UseTextModelProps) => {
84
75
  const { text } = props;
85
- if (isActualAutomergeObject(text)) {
76
+ if (isAutomergeObject(text)) {
86
77
  return createAutomergeModel(props);
87
- } else {
88
- if (!text?.doc) {
89
- return undefined;
90
- }
91
-
78
+ } else if (text?.doc) {
92
79
  return createYjsModel(props);
80
+ } else {
81
+ return undefined;
93
82
  }
94
83
  };
95
-
96
- const createYjsModel = ({ identity, space, text }: UseTextModelProps): EditorModel => {
97
- invariant(text?.doc && text?.content);
98
- const provider = space
99
- ? new SpaceAwarenessProvider({ space, doc: text.doc, channel: `yjs.awareness.${text.id}` })
100
- : undefined;
101
-
102
- const model: EditorModel = {
103
- id: text.doc.guid,
104
- content: text.content,
105
- text: () => text.content!.toString(),
106
- // https://github.com/yjs/yjs?tab=readme-ov-file#relative-positions
107
- getCursorFromRange: (range: Range) => {
108
- const from = Y.encodeRelativePosition(Y.createRelativePositionFromTypeIndex(text.content as YText, range.from));
109
- const to = Y.encodeRelativePosition(Y.createRelativePositionFromTypeIndex(text.content as YText, range.to, -1));
110
- return [arrayToString(from), arrayToString(to)].join(':');
111
- },
112
- getRangeFromCursor: (cursor: string) => {
113
- const parts = cursor.split(':');
114
- const from = Y.createAbsolutePositionFromRelativePosition(
115
- Y.decodeRelativePosition(stringToArray(parts[0])),
116
- text.doc!,
117
- );
118
- const to = Y.createAbsolutePositionFromRelativePosition(
119
- Y.decodeRelativePosition(stringToArray(parts[1])),
120
- text.doc!,
121
- );
122
- return { from: from!.index, to: to!.index };
123
- },
124
- extension: [yCollab(text.content as YText, provider?.awareness), modelState.init(() => model)],
125
- awareness: provider?.awareness,
126
- peer: identity
127
- ? {
128
- id: identity.identityKey.toHex(),
129
- name: identity.profile?.displayName,
130
- }
131
- : undefined,
132
- };
133
-
134
- return model;
135
- };
136
-
137
- const createAutomergeModel = ({ identity, text }: UseTextModelProps): EditorModel => {
138
- const obj = text as any as AutomergeTextCompat;
139
- const doc = getRawDoc(obj, [obj.field]);
140
-
141
- const model: EditorModel = {
142
- id: obj.id,
143
- content: doc,
144
- text: () => get(doc.handle.docSync(), doc.path),
145
- // TODO(burdon): https://automerge.org/automerge/api-docs/js/functions/next.getCursor.html
146
- extension: [automergePlugin(doc.handle, doc.path), modelState.init(() => model)],
147
- peer: identity
148
- ? {
149
- id: identity.identityKey.toHex(),
150
- name: identity.profile?.displayName,
151
- }
152
- : undefined,
153
- };
154
-
155
- return model;
156
- };