@dxos/react-ui-editor 0.3.10 → 0.3.11-main.008b7c6

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 (211) hide show
  1. package/dist/lib/browser/index.mjs +1537 -847
  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 +14 -10
  5. package/dist/types/src/components/TextEditor/MarkdownEditor.stories.d.ts.map +1 -1
  6. package/dist/types/src/components/TextEditor/TextEditor.d.ts +3 -2
  7. package/dist/types/src/components/TextEditor/TextEditor.d.ts.map +1 -1
  8. package/dist/types/src/components/TextEditor/TextEditor.stories.d.ts +3 -0
  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 -2
  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/index.d.ts +0 -2
  14. package/dist/types/src/components/TextEditor/index.d.ts.map +1 -1
  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 -0
  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 +3 -0
  25. package/dist/types/src/extensions/automerge/index.d.ts.map +1 -0
  26. package/dist/types/src/{hooks/automerge/PatchSemaphore.d.ts → extensions/automerge/semaphore.d.ts} +5 -5
  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 +5 -0
  31. package/dist/types/src/extensions/automerge/update-codemirror.d.ts.map +1 -0
  32. package/dist/types/src/extensions/awareness.d.ts +46 -0
  33. package/dist/types/src/extensions/awareness.d.ts.map +1 -0
  34. package/dist/types/src/extensions/basic.d.ts.map +1 -0
  35. package/dist/types/src/extensions/blast.d.ts.map +1 -0
  36. package/dist/types/src/extensions/code.d.ts.map +1 -0
  37. package/dist/types/src/extensions/comments.d.ts +50 -0
  38. package/dist/types/src/extensions/comments.d.ts.map +1 -0
  39. package/dist/types/src/extensions/hr.d.ts +2 -0
  40. package/dist/types/src/extensions/hr.d.ts.map +1 -0
  41. package/dist/types/src/extensions/image.d.ts.map +1 -0
  42. package/dist/types/src/{components/TextEditor/extensions → extensions}/index.d.ts +6 -3
  43. package/dist/types/src/extensions/index.d.ts.map +1 -0
  44. package/dist/types/src/{components/TextEditor/extensions → extensions}/link.d.ts +1 -0
  45. package/dist/types/src/extensions/link.d.ts.map +1 -0
  46. package/dist/types/src/extensions/listener.d.ts +10 -0
  47. package/dist/types/src/extensions/listener.d.ts.map +1 -0
  48. package/dist/types/src/extensions/markdown/bundle.d.ts.map +1 -0
  49. package/dist/types/src/{components/TextEditor/extensions → extensions}/markdown/highlight.d.ts +0 -1
  50. package/dist/types/src/extensions/markdown/highlight.d.ts.map +1 -0
  51. package/dist/types/src/extensions/markdown/index.d.ts.map +1 -0
  52. package/dist/types/src/extensions/mention.d.ts.map +1 -0
  53. package/dist/types/src/extensions/mermaid.d.ts +3 -0
  54. package/dist/types/src/extensions/mermaid.d.ts.map +1 -0
  55. package/dist/types/src/extensions/table.d.ts.map +1 -0
  56. package/dist/types/src/extensions/tasklist.d.ts.map +1 -0
  57. package/dist/types/src/{components/TextEditor/extensions/demo.d.ts → extensions/typewriter.d.ts} +2 -3
  58. package/dist/types/src/extensions/typewriter.d.ts.map +1 -0
  59. package/dist/types/src/extensions/yjs/cursor.d.ts +4 -0
  60. package/dist/types/src/extensions/yjs/cursor.d.ts.map +1 -0
  61. package/dist/types/src/extensions/yjs/index.d.ts +2 -0
  62. package/dist/types/src/extensions/yjs/index.d.ts.map +1 -0
  63. package/dist/types/src/extensions/yjs/yjs.d.ts +4 -0
  64. package/dist/types/src/extensions/yjs/yjs.d.ts.map +1 -0
  65. package/dist/types/src/extensions/yjs/yjs.test.d.ts +2 -0
  66. package/dist/types/src/extensions/yjs/yjs.test.d.ts.map +1 -0
  67. package/dist/types/src/hooks/automerge.d.ts +3 -0
  68. package/dist/types/src/hooks/automerge.d.ts.map +1 -0
  69. package/dist/types/src/hooks/awareness-provider.d.ts +28 -0
  70. package/dist/types/src/hooks/awareness-provider.d.ts.map +1 -0
  71. package/dist/types/src/hooks/index.d.ts +2 -0
  72. package/dist/types/src/hooks/index.d.ts.map +1 -1
  73. package/dist/types/src/hooks/useTextModel.d.ts +20 -11
  74. package/dist/types/src/hooks/useTextModel.d.ts.map +1 -1
  75. package/dist/types/src/hooks/{yjs/space-provider.d.ts → yjs.d.ts} +8 -8
  76. package/dist/types/src/hooks/yjs.d.ts.map +1 -0
  77. package/dist/types/src/index.d.ts +3 -0
  78. package/dist/types/src/index.d.ts.map +1 -1
  79. package/dist/types/src/styles/cursors.d.ts.map +1 -0
  80. package/dist/types/src/styles/index.d.ts +1 -0
  81. package/dist/types/src/styles/index.d.ts.map +1 -1
  82. package/dist/types/src/testing/replicator.d.ts +2 -2
  83. package/dist/types/src/testing/replicator.d.ts.map +1 -1
  84. package/dist/types/src/{components/TextEditor/themes → themes}/default.d.ts +5 -13
  85. package/dist/types/src/themes/default.d.ts.map +1 -0
  86. package/dist/types/src/themes/index.d.ts.map +1 -0
  87. package/dist/types/src/util/cursor.d.ts +17 -0
  88. package/dist/types/src/util/cursor.d.ts.map +1 -0
  89. package/dist/types/src/util/index.d.ts +3 -0
  90. package/dist/types/src/util/index.d.ts.map +1 -0
  91. package/dist/types/src/util/util.d.ts +5 -0
  92. package/dist/types/src/util/util.d.ts.map +1 -0
  93. package/package.json +25 -20
  94. package/src/components/TextEditor/MarkdownEditor.stories.tsx +130 -71
  95. package/src/components/TextEditor/TextEditor.stories.tsx +15 -2
  96. package/src/components/TextEditor/TextEditor.tsx +30 -26
  97. package/src/{hooks/automerge → components/TextEditor}/automerge.stories.tsx +67 -8
  98. package/src/components/TextEditor/codemirror.stories.ts +3 -2
  99. package/src/components/TextEditor/index.ts +0 -3
  100. package/src/{hooks/yjs → components/TextEditor}/yjs.stories.tsx +1 -1
  101. package/src/extensions/automerge/automerge.ts +94 -0
  102. package/src/extensions/automerge/cursor.ts +51 -0
  103. package/src/extensions/automerge/defs.ts +62 -0
  104. package/src/extensions/automerge/index.ts +6 -0
  105. package/src/{hooks/automerge/PatchSemaphore.ts → extensions/automerge/semaphore.ts} +18 -15
  106. package/src/{hooks/automerge/translation/codemirror-to-automerge.ts → extensions/automerge/update-automerge.ts} +6 -7
  107. package/src/{hooks/automerge/translation/automerge-to-codemirror.ts → extensions/automerge/update-codemirror.ts} +14 -11
  108. package/src/extensions/awareness.ts +316 -0
  109. package/src/{components/TextEditor/extensions → extensions}/code.ts +13 -13
  110. package/src/extensions/comments.ts +421 -0
  111. package/src/extensions/hr.ts +74 -0
  112. package/src/{components/TextEditor/extensions → extensions}/image.ts +3 -1
  113. package/src/{components/TextEditor/extensions → extensions}/index.ts +6 -3
  114. package/src/{components/TextEditor/extensions → extensions}/link.ts +66 -10
  115. package/src/extensions/listener.ts +35 -0
  116. package/src/{components/TextEditor/extensions → extensions}/markdown/highlight.ts +6 -9
  117. package/src/extensions/mermaid.ts +11 -0
  118. package/src/{components/TextEditor/extensions → extensions}/table.ts +14 -1
  119. package/src/{components/TextEditor/extensions/demo.ts → extensions/typewriter.ts} +2 -3
  120. package/src/extensions/yjs/cursor.ts +21 -0
  121. package/src/extensions/yjs/index.ts +5 -0
  122. package/src/extensions/yjs/yjs.test.ts +35 -0
  123. package/src/extensions/yjs/yjs.ts +16 -0
  124. package/src/hooks/automerge.ts +52 -0
  125. package/src/hooks/awareness-provider.ts +120 -0
  126. package/src/hooks/index.ts +2 -0
  127. package/src/hooks/useTextModel.ts +43 -79
  128. package/src/hooks/{yjs/space-provider.ts → yjs.ts} +71 -19
  129. package/src/index.ts +3 -0
  130. package/src/styles/index.ts +1 -0
  131. package/src/testing/replicator.ts +10 -16
  132. package/src/{components/TextEditor/themes → themes}/default.ts +61 -38
  133. package/src/util/cursor.ts +29 -0
  134. package/src/util/index.ts +6 -0
  135. package/src/util/util.ts +18 -0
  136. package/dist/types/src/components/TextEditor/extensions/autocomplete.d.ts.map +0 -1
  137. package/dist/types/src/components/TextEditor/extensions/basic.d.ts.map +0 -1
  138. package/dist/types/src/components/TextEditor/extensions/blast.d.ts.map +0 -1
  139. package/dist/types/src/components/TextEditor/extensions/code.d.ts.map +0 -1
  140. package/dist/types/src/components/TextEditor/extensions/comments.d.ts +0 -18
  141. package/dist/types/src/components/TextEditor/extensions/comments.d.ts.map +0 -1
  142. package/dist/types/src/components/TextEditor/extensions/demo.d.ts.map +0 -1
  143. package/dist/types/src/components/TextEditor/extensions/experimental.d.ts +0 -10
  144. package/dist/types/src/components/TextEditor/extensions/experimental.d.ts.map +0 -1
  145. package/dist/types/src/components/TextEditor/extensions/highlight.d.ts +0 -24
  146. package/dist/types/src/components/TextEditor/extensions/highlight.d.ts.map +0 -1
  147. package/dist/types/src/components/TextEditor/extensions/image.d.ts.map +0 -1
  148. package/dist/types/src/components/TextEditor/extensions/index.d.ts.map +0 -1
  149. package/dist/types/src/components/TextEditor/extensions/link.d.ts.map +0 -1
  150. package/dist/types/src/components/TextEditor/extensions/listener.d.ts +0 -9
  151. package/dist/types/src/components/TextEditor/extensions/listener.d.ts.map +0 -1
  152. package/dist/types/src/components/TextEditor/extensions/markdown/bundle.d.ts.map +0 -1
  153. package/dist/types/src/components/TextEditor/extensions/markdown/highlight.d.ts.map +0 -1
  154. package/dist/types/src/components/TextEditor/extensions/markdown/index.d.ts.map +0 -1
  155. package/dist/types/src/components/TextEditor/extensions/mention.d.ts.map +0 -1
  156. package/dist/types/src/components/TextEditor/extensions/table.d.ts.map +0 -1
  157. package/dist/types/src/components/TextEditor/extensions/tasklist.d.ts.map +0 -1
  158. package/dist/types/src/components/TextEditor/extensions/tooltip.d.ts +0 -12
  159. package/dist/types/src/components/TextEditor/extensions/tooltip.d.ts.map +0 -1
  160. package/dist/types/src/components/TextEditor/themes/default.d.ts.map +0 -1
  161. package/dist/types/src/components/TextEditor/themes/index.d.ts.map +0 -1
  162. package/dist/types/src/hooks/automerge/PatchSemaphore.d.ts.map +0 -1
  163. package/dist/types/src/hooks/automerge/automerge.stories.d.ts.map +0 -1
  164. package/dist/types/src/hooks/automerge/handle.d.ts +0 -9
  165. package/dist/types/src/hooks/automerge/handle.d.ts.map +0 -1
  166. package/dist/types/src/hooks/automerge/index.d.ts +0 -3
  167. package/dist/types/src/hooks/automerge/index.d.ts.map +0 -1
  168. package/dist/types/src/hooks/automerge/plugin.d.ts +0 -26
  169. package/dist/types/src/hooks/automerge/plugin.d.ts.map +0 -1
  170. package/dist/types/src/hooks/automerge/translation/automerge-to-codemirror.d.ts +0 -5
  171. package/dist/types/src/hooks/automerge/translation/automerge-to-codemirror.d.ts.map +0 -1
  172. package/dist/types/src/hooks/automerge/translation/codemirror-to-automerge.d.ts +0 -6
  173. package/dist/types/src/hooks/automerge/translation/codemirror-to-automerge.d.ts.map +0 -1
  174. package/dist/types/src/hooks/automerge/translation/index.d.ts +0 -3
  175. package/dist/types/src/hooks/automerge/translation/index.d.ts.map +0 -1
  176. package/dist/types/src/hooks/yjs/cursors.d.ts.map +0 -1
  177. package/dist/types/src/hooks/yjs/index.d.ts +0 -3
  178. package/dist/types/src/hooks/yjs/index.d.ts.map +0 -1
  179. package/dist/types/src/hooks/yjs/space-provider.d.ts.map +0 -1
  180. package/dist/types/src/hooks/yjs/yjs.stories.d.ts.map +0 -1
  181. package/src/components/TextEditor/extensions/comments.ts +0 -173
  182. package/src/components/TextEditor/extensions/experimental.tsx +0 -114
  183. package/src/components/TextEditor/extensions/highlight.ts +0 -128
  184. package/src/components/TextEditor/extensions/listener.ts +0 -23
  185. package/src/components/TextEditor/extensions/tooltip.tsx +0 -61
  186. package/src/hooks/automerge/handle.ts +0 -14
  187. package/src/hooks/automerge/index.ts +0 -6
  188. package/src/hooks/automerge/plugin.ts +0 -134
  189. package/src/hooks/automerge/translation/index.ts +0 -6
  190. package/src/hooks/yjs/index.ts +0 -6
  191. /package/dist/types/src/{components/TextEditor/extensions → extensions}/autocomplete.d.ts +0 -0
  192. /package/dist/types/src/{components/TextEditor/extensions → extensions}/basic.d.ts +0 -0
  193. /package/dist/types/src/{components/TextEditor/extensions → extensions}/blast.d.ts +0 -0
  194. /package/dist/types/src/{components/TextEditor/extensions → extensions}/code.d.ts +0 -0
  195. /package/dist/types/src/{components/TextEditor/extensions → extensions}/image.d.ts +0 -0
  196. /package/dist/types/src/{components/TextEditor/extensions → extensions}/markdown/bundle.d.ts +0 -0
  197. /package/dist/types/src/{components/TextEditor/extensions → extensions}/markdown/index.d.ts +0 -0
  198. /package/dist/types/src/{components/TextEditor/extensions → extensions}/mention.d.ts +0 -0
  199. /package/dist/types/src/{components/TextEditor/extensions → extensions}/table.d.ts +0 -0
  200. /package/dist/types/src/{components/TextEditor/extensions → extensions}/tasklist.d.ts +0 -0
  201. /package/dist/types/src/{hooks/yjs → styles}/cursors.d.ts +0 -0
  202. /package/dist/types/src/{components/TextEditor/themes → themes}/index.d.ts +0 -0
  203. /package/src/{components/TextEditor/extensions → extensions}/autocomplete.ts +0 -0
  204. /package/src/{components/TextEditor/extensions → extensions}/basic.ts +0 -0
  205. /package/src/{components/TextEditor/extensions → extensions}/blast.ts +0 -0
  206. /package/src/{components/TextEditor/extensions → extensions}/markdown/bundle.ts +0 -0
  207. /package/src/{components/TextEditor/extensions → extensions}/markdown/index.ts +0 -0
  208. /package/src/{components/TextEditor/extensions → extensions}/mention.ts +0 -0
  209. /package/src/{components/TextEditor/extensions → extensions}/tasklist.ts +0 -0
  210. /package/src/{hooks/yjs → styles}/cursors.ts +0 -0
  211. /package/src/{components/TextEditor/themes → themes}/index.ts +0 -0
@@ -8,21 +8,18 @@ import { tags, styleTags, Tag } from '@lezer/highlight';
8
8
  import { type MarkdownConfig, Table } from '@lezer/markdown';
9
9
  import get from 'lodash.get';
10
10
 
11
- import { mx } from '@dxos/react-ui-theme';
12
-
13
11
  import {
14
12
  blockquote,
15
13
  bold,
16
14
  code,
17
15
  codeMark,
18
16
  heading,
19
- horizontalRule,
20
17
  inlineUrl,
21
18
  italic,
22
19
  mark,
23
20
  strikethrough,
24
21
  tokens,
25
- } from '../../../../styles';
22
+ } from '../../styles';
26
23
 
27
24
  /**
28
25
  * Custom tags defined and processed by the GFM lezer extension.
@@ -35,7 +32,7 @@ export const markdownTags = {
35
32
  CodeText: Tag.define(),
36
33
  EmphasisMark: Tag.define(),
37
34
  HeaderMark: Tag.define(),
38
- HorizontalRule: Tag.define(),
35
+ // HorizontalRule: Tag.define(),
39
36
  InlineCode: Tag.define(),
40
37
  LinkLabel: Tag.define(),
41
38
  LinkReference: Tag.define(),
@@ -182,10 +179,10 @@ export const markdownHighlightStyle = (readonly?: boolean) => {
182
179
  },
183
180
 
184
181
  // TODO(burdon): Replace with extension.
185
- {
186
- tag: [markdownTags.HorizontalRule],
187
- class: mx(horizontalRule, readonly && '-indent-[100rem]'),
188
- },
182
+ // {
183
+ // tag: [markdownTags.HorizontalRule],
184
+ // class: mx(horizontalRule, readonly && '-indent-[100rem]'),
185
+ // },
189
186
 
190
187
  // TODO(burdon): Only if being edited.
191
188
  {
@@ -0,0 +1,11 @@
1
+ //
2
+ // Copyright 2024 DXOS.org
3
+ //
4
+
5
+ import { type Extension } from '@codemirror/state';
6
+
7
+ // TODO(burdon): How will this interact with the code extension?
8
+ // TODO(burdon): Move to mermaid-plugin (which should contribute this extension).
9
+ export const mermaid = (): Extension => {
10
+ return [];
11
+ };
@@ -9,6 +9,9 @@ import { Decoration, EditorView, WidgetType } from '@codemirror/view';
9
9
  // TODO(burdon): Snippet to create basic table.
10
10
  // https://codemirror.net/docs/ref/#autocomplete.snippet
11
11
 
12
+ // TODO(burdon): Advanced formatting (left/right/center).
13
+ // TODO(burdon): Editor to auto balance columns.
14
+
12
15
  export type TableOptions = {};
13
16
 
14
17
  /**
@@ -73,7 +76,17 @@ const update = (state: EditorState, options: TableOptions) => {
73
76
 
74
77
  tables.forEach((table) => {
75
78
  const hide = state.readOnly || cursor < table.from || cursor > table.to;
76
- hide && builder.add(table.from, table.to, Decoration.replace({ widget: new TableWidget(table) }));
79
+ hide &&
80
+ builder.add(
81
+ table.from,
82
+ table.to,
83
+ Decoration.replace({
84
+ block: true,
85
+ widget: new TableWidget(table),
86
+ }),
87
+ );
88
+
89
+ // Add class for styling.
77
90
  builder.add(table.from, table.to, Decoration.mark({ class: 'cm-table' }));
78
91
  });
79
92
 
@@ -15,12 +15,11 @@ export type DemoOptions = {
15
15
  const defaultItems = ['hello world!', 'this is a test.', 'this is [DXOS](https://dxos.org)'];
16
16
 
17
17
  /**
18
- * Demo script.
19
18
  * Configurable plugin that let's user cycle through pre-configured input script.
20
19
  */
21
- export const demo = ({ delay = 75, items = defaultItems }: DemoOptions = {}): Extension => {
20
+ export const typewriter = ({ delay = 75, items = defaultItems }: DemoOptions = {}): Extension => {
22
21
  let t: any;
23
- let idx = 0;
22
+ let idx = 0; // TODO(burdon): Make global.
24
23
 
25
24
  return [
26
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';
@@ -0,0 +1,35 @@
1
+ //
2
+ // Copyright 2024 DXOS.org
3
+ //
4
+
5
+ import { expect } from 'chai';
6
+ import * as Y from 'yjs';
7
+
8
+ import { TextObject } from '@dxos/echo-schema';
9
+ import { describe, test } from '@dxos/test';
10
+ import { type YText } from '@dxos/text-model';
11
+
12
+ describe('YJS', () => {
13
+ // https://docs.yjs.dev/api/shared-types/y.text
14
+ // https://github.com/yjs/yjs?tab=readme-ov-file#relative-positions
15
+ test('absolute position', () => {
16
+ const obj = new TextObject('hello world');
17
+ const index = obj.text.indexOf('world');
18
+ const relPos = Y.encodeRelativePosition(Y.createRelativePositionFromTypeIndex(obj.content as YText, index));
19
+
20
+ {
21
+ const cursor = Y.createAbsolutePositionFromRelativePosition(Y.decodeRelativePosition(relPos), obj.doc!);
22
+ expect(cursor!.index).to.equal(index);
23
+ expect(obj.text.substring(cursor!.index)).to.equal('world');
24
+ }
25
+
26
+ (obj.content as YText).insert(index, 'cruel ');
27
+ expect(obj.text).to.equal('hello cruel world');
28
+
29
+ {
30
+ const cursor = Y.createAbsolutePositionFromRelativePosition(Y.decodeRelativePosition(relPos), obj.doc!);
31
+ expect(cursor!.index).not.to.equal(index);
32
+ expect(obj.text.substring(cursor!.index)).to.equal('world');
33
+ }
34
+ });
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,45 +2,53 @@
2
2
  // Copyright 2023 DXOS.org
3
3
  //
4
4
 
5
- import { type Extension } 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';
7
+ import type * as YP from 'y-protocols/awareness';
10
8
 
11
- import { invariant } from '@dxos/invariant';
12
- import {
13
- type DocAccessor,
14
- type Space,
15
- type TextObject,
16
- type AutomergeTextCompat,
17
- getRawDoc,
18
- isActualAutomergeObject,
19
- } from '@dxos/react-client/echo';
9
+ import { isAutomergeObject, type DocAccessor, type Space, type TextObject } from '@dxos/react-client/echo';
20
10
  import { type Identity } from '@dxos/react-client/halo';
21
11
  import type { YText, YXmlFragment } from '@dxos/text-model';
22
12
 
23
- import { automergePlugin } from './automerge';
24
- import { SpaceAwarenessProvider } from './yjs';
13
+ import { createAutomergeModel } from './automerge';
14
+ import { createYjsModel } from './yjs';
25
15
 
26
- // TODO(burdon): Move.
27
- type Awareness = awarenessProtocol.Awareness;
16
+ // TODO(burdon): Factor out defs.
28
17
 
29
- export type DocumentRange = {
30
- id: string;
18
+ /**
19
+ * State field makes the model available to other extensions.
20
+ */
21
+ export const modelState = StateField.define<EditorModel | undefined>({
22
+ create: () => undefined,
23
+ update: (model) => model,
24
+ });
25
+
26
+ export type Range = {
31
27
  from: number;
32
28
  to: number;
33
29
  };
34
30
 
31
+ export type CommentRange = {
32
+ id: string;
33
+ cursor: string;
34
+ };
35
+
35
36
  // TODO(wittjosiah): Factor out to common package? @dxos/react-client?
36
37
  export type EditorModel = {
37
38
  id: string;
38
- // TODO(burdon): Remove.
39
- content: string | YText | YXmlFragment | DocAccessor;
40
39
  text: () => string;
41
- ranges: DocumentRange[];
42
40
  extension?: Extension;
43
- awareness?: Awareness;
41
+
42
+ // TODO(burdon): Remove.
43
+ content: string | YText | YXmlFragment | DocAccessor;
44
+ /**
45
+ * @deprecated Use CursorConverter.
46
+ */
47
+ getCursorFromRange?: (value: Range) => string;
48
+ getRangeFromCursor?: (cursor: string) => Range | undefined;
49
+ // TODO(burdon): Move into extension.
50
+ awareness?: YP.Awareness;
51
+ // TODO(burdon): Remove.
44
52
  peer?: {
45
53
  id: string;
46
54
  name?: string;
@@ -48,7 +56,7 @@ export type EditorModel = {
48
56
  };
49
57
 
50
58
  // TODO(burdon): Remove space/identity dependency. Define interface for the framework re content and presence.
51
- export type UseTextModelOptions = {
59
+ export type UseTextModelProps = {
52
60
  identity?: Identity | null;
53
61
  space?: Space;
54
62
  text?: TextObject;
@@ -56,64 +64,20 @@ export type UseTextModelOptions = {
56
64
 
57
65
  // TODO(burdon): Remove YJS/Automerge deps (from UI component -- create abstraction; incl. all ECHO/Space deps).
58
66
  // TODO(wittjosiah): Factor out to common package? @dxos/react-client?
59
- export const useTextModel = ({ identity, space, text }: UseTextModelOptions): EditorModel | undefined => {
60
- const [model, setModel] = useState<EditorModel | undefined>(() => createModel({ identity, space, text }));
61
- useEffect(() => {
62
- setModel(createModel({ identity, space, text }));
63
- }, [identity, space, text]);
67
+ export const useTextModel = (props: UseTextModelProps): EditorModel | undefined => {
68
+ const { identity, space, text } = props;
69
+ const [model, setModel] = useState<EditorModel | undefined>(() => createModel(props));
70
+ useEffect(() => setModel(createModel(props)), [identity, space, text]);
64
71
  return model;
65
72
  };
66
73
 
67
- const createModel = (options: UseTextModelOptions) => {
68
- const { text } = options;
69
- if (isActualAutomergeObject(text)) {
70
- return createAutomergeModel(options);
74
+ const createModel = (props: UseTextModelProps) => {
75
+ const { text } = props;
76
+ if (isAutomergeObject(text)) {
77
+ return createAutomergeModel(props);
78
+ } else if (text?.doc) {
79
+ return createYjsModel(props);
71
80
  } else {
72
- if (!text?.doc) {
73
- return undefined;
74
- }
75
-
76
- return createYjsModel(options);
81
+ return undefined;
77
82
  }
78
83
  };
79
-
80
- const createYjsModel = ({ identity, space, text }: UseTextModelOptions): EditorModel => {
81
- invariant(text?.doc && text?.content);
82
- const provider = space
83
- ? new SpaceAwarenessProvider({ space, doc: text.doc, channel: `yjs.awareness.${text.id}` })
84
- : undefined;
85
-
86
- return {
87
- id: text.doc.guid,
88
- content: text.content,
89
- text: () => text.content!.toString(),
90
- ranges: [],
91
- extension: yCollab(text.content as YText, provider?.awareness),
92
- awareness: provider?.awareness,
93
- peer: identity
94
- ? {
95
- id: identity.identityKey.toHex(),
96
- name: identity.profile?.displayName,
97
- }
98
- : undefined,
99
- };
100
- };
101
-
102
- const createAutomergeModel = ({ identity, text }: UseTextModelOptions): EditorModel => {
103
- const obj = text as any as AutomergeTextCompat;
104
- const doc = getRawDoc(obj, [obj.field]);
105
-
106
- return {
107
- id: obj.id,
108
- content: doc,
109
- text: () => get(doc.handle.docSync(), doc.path),
110
- ranges: [],
111
- extension: automergePlugin(doc.handle, doc.path),
112
- peer: identity
113
- ? {
114
- id: identity.identityKey.toHex(),
115
- name: identity.profile?.displayName,
116
- }
117
- : undefined,
118
- };
119
- };
@@ -5,14 +5,62 @@
5
5
  import * as decoding from 'lib0/decoding';
6
6
  import * as encoding from 'lib0/encoding';
7
7
  import { Observable } from 'lib0/observable';
8
- import * as awarenessProtocol from 'y-protocols/awareness';
8
+ import * as YP from 'y-protocols/awareness';
9
+ import * as Y from 'yjs';
9
10
  import { type Doc } from 'yjs';
10
11
 
12
+ import { invariant } from '@dxos/invariant';
11
13
  import { log } from '@dxos/log';
12
14
  import { type Space } from '@dxos/react-client/echo';
13
15
  import { type GossipMessage } from '@dxos/react-client/mesh';
14
-
15
- type Awareness = awarenessProtocol.Awareness;
16
+ import type { YText } from '@dxos/text-model';
17
+ import { arrayToString, stringToArray } from '@dxos/util';
18
+
19
+ import { type EditorModel, modelState, type Range, type UseTextModelProps } from './useTextModel';
20
+ import { yjs } from '../extensions';
21
+
22
+ export const createYjsModel = ({ identity, space, text }: UseTextModelProps): EditorModel => {
23
+ invariant(text?.doc && text?.content);
24
+ const provider = space
25
+ ? new YAwarenessProvider({ space, doc: text.doc, channel: `yjs.awareness.${text.id}` })
26
+ : undefined;
27
+
28
+ const model: EditorModel = {
29
+ id: text.doc.guid,
30
+ content: text.content,
31
+ text: () => text.content!.toString(),
32
+ // https://github.com/yjs/yjs?tab=readme-ov-file#relative-positions
33
+ // TODO(dmaretskyi): Refactor as cursor-converter.
34
+ getCursorFromRange: (range: Range) => {
35
+ const from = Y.encodeRelativePosition(Y.createRelativePositionFromTypeIndex(text.content as YText, range.from));
36
+ const to = Y.encodeRelativePosition(Y.createRelativePositionFromTypeIndex(text.content as YText, range.to, -1));
37
+ return [arrayToString(from), arrayToString(to)].join(':');
38
+ },
39
+ getRangeFromCursor: (cursor: string) => {
40
+ invariant(text.doc);
41
+ const parts = cursor.split(':');
42
+ const from = Y.createAbsolutePositionFromRelativePosition(
43
+ Y.decodeRelativePosition(stringToArray(parts[0])),
44
+ text.doc,
45
+ );
46
+ const to = Y.createAbsolutePositionFromRelativePosition(
47
+ Y.decodeRelativePosition(stringToArray(parts[1])),
48
+ text.doc,
49
+ );
50
+ return from && to ? { from: from.index, to: to.index } : undefined;
51
+ },
52
+ extension: [modelState.init(() => model), yjs(text.content as YText, provider?.awareness)],
53
+ awareness: provider?.awareness,
54
+ peer: identity
55
+ ? {
56
+ id: identity.identityKey.toHex(),
57
+ name: identity.profile?.displayName,
58
+ }
59
+ : undefined,
60
+ };
61
+
62
+ return model;
63
+ };
16
64
 
17
65
  // Based on https://github.com/yjs/y-webrtc/blob/88baab2/src/y-webrtc.js.
18
66
 
@@ -22,16 +70,26 @@ const messageQueryAwareness = 3;
22
70
  /**
23
71
  * Yjs awareness provider on top of a DXOS space.
24
72
  */
25
- export class SpaceAwarenessProvider extends Observable<any> {
73
+ export class YAwarenessProvider extends Observable<any> {
26
74
  private readonly _space: Space;
27
- private readonly _awareness: Awareness;
75
+ private readonly _awareness: YP.Awareness;
28
76
  private readonly _clientId: number;
29
77
  private readonly _channel: string;
30
78
 
31
- constructor({ space, doc, channel, awareness }: { space: Space; doc: Doc; channel: string; awareness?: Awareness }) {
79
+ constructor({
80
+ space,
81
+ doc,
82
+ channel,
83
+ awareness,
84
+ }: {
85
+ space: Space;
86
+ doc: Doc;
87
+ channel: string;
88
+ awareness?: YP.Awareness;
89
+ }) {
32
90
  super();
33
91
  this._space = space;
34
- this._awareness = awareness ?? new awarenessProtocol.Awareness(doc);
92
+ this._awareness = awareness ?? new YP.Awareness(doc);
35
93
  this._channel = channel;
36
94
  this._clientId = doc.clientID;
37
95
 
@@ -52,14 +110,11 @@ export class SpaceAwarenessProvider extends Observable<any> {
52
110
  // Post local awareness state.
53
111
  const encoderAwarenessState = encoding.createEncoder();
54
112
  encoding.writeVarUint(encoderAwarenessState, messageAwareness);
55
- encoding.writeVarUint8Array(
56
- encoderAwarenessState,
57
- awarenessProtocol.encodeAwarenessUpdate(this.awareness, [this._clientId]),
58
- );
113
+ encoding.writeVarUint8Array(encoderAwarenessState, YP.encodeAwarenessUpdate(this.awareness, [this._clientId]));
59
114
  void this._space.postMessage(this._channel, encoding.toUint8Array(encoderAwarenessState));
60
115
  }
61
116
 
62
- get awareness(): Awareness {
117
+ get awareness(): YP.Awareness {
63
118
  return this._awareness;
64
119
  }
65
120
 
@@ -68,10 +123,7 @@ export class SpaceAwarenessProvider extends Observable<any> {
68
123
  const changedClients = added.concat(updated).concat(removed);
69
124
  const encoderAwareness = encoding.createEncoder();
70
125
  encoding.writeVarUint(encoderAwareness, messageAwareness);
71
- encoding.writeVarUint8Array(
72
- encoderAwareness,
73
- awarenessProtocol.encodeAwarenessUpdate(this._awareness, changedClients),
74
- );
126
+ encoding.writeVarUint8Array(encoderAwareness, YP.encodeAwarenessUpdate(this._awareness, changedClients));
75
127
  void this._space.postMessage(this._channel, encoding.toUint8Array(encoderAwareness));
76
128
  }
77
129
 
@@ -92,7 +144,7 @@ export class SpaceAwarenessProvider extends Observable<any> {
92
144
  let sendReply = false;
93
145
  switch (messageType) {
94
146
  case messageAwareness: {
95
- awarenessProtocol.applyAwarenessUpdate(this._awareness, decoding.readVarUint8Array(decoder), this);
147
+ YP.applyAwarenessUpdate(this._awareness, decoding.readVarUint8Array(decoder), this);
96
148
  break;
97
149
  }
98
150
 
@@ -100,7 +152,7 @@ export class SpaceAwarenessProvider extends Observable<any> {
100
152
  encoding.writeVarUint(encoder, messageAwareness);
101
153
  encoding.writeVarUint8Array(
102
154
  encoder,
103
- awarenessProtocol.encodeAwarenessUpdate(this._awareness, Array.from(this._awareness.getStates().keys())),
155
+ YP.encodeAwarenessUpdate(this._awareness, Array.from(this._awareness.getStates().keys())),
104
156
  );
105
157
  sendReply = true;
106
158
  break;
@@ -121,6 +173,6 @@ export class SpaceAwarenessProvider extends Observable<any> {
121
173
  }
122
174
 
123
175
  private _handleBeforeUnload() {
124
- awarenessProtocol.removeAwarenessStates(this._awareness, [this._clientId], 'window unload');
176
+ YP.removeAwarenessStates(this._awareness, [this._clientId], 'window unload');
125
177
  }
126
178
  }