@dxos/react-ui-editor 0.3.10 → 0.3.11-main.3f845e1

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 (82) hide show
  1. package/dist/lib/browser/index.mjs +1928 -1717
  2. package/dist/lib/browser/index.mjs.map +4 -4
  3. package/dist/lib/browser/meta.json +1 -1
  4. package/dist/types/src/components/TextEditor/MarkdownEditor.stories.d.ts +16 -9
  5. package/dist/types/src/components/TextEditor/MarkdownEditor.stories.d.ts.map +1 -1
  6. package/dist/types/src/components/TextEditor/TextEditor.d.ts +2 -1
  7. package/dist/types/src/components/TextEditor/TextEditor.d.ts.map +1 -1
  8. package/dist/types/src/components/TextEditor/extensions/code.d.ts.map +1 -1
  9. package/dist/types/src/components/TextEditor/extensions/comments.d.ts +38 -8
  10. package/dist/types/src/components/TextEditor/extensions/comments.d.ts.map +1 -1
  11. package/dist/types/src/components/TextEditor/extensions/hr.d.ts +2 -0
  12. package/dist/types/src/components/TextEditor/extensions/hr.d.ts.map +1 -0
  13. package/dist/types/src/components/TextEditor/extensions/index.d.ts +3 -2
  14. package/dist/types/src/components/TextEditor/extensions/index.d.ts.map +1 -1
  15. package/dist/types/src/components/TextEditor/extensions/markdown/highlight.d.ts +0 -1
  16. package/dist/types/src/components/TextEditor/extensions/markdown/highlight.d.ts.map +1 -1
  17. package/dist/types/src/components/TextEditor/extensions/mermaid.d.ts +3 -0
  18. package/dist/types/src/components/TextEditor/extensions/mermaid.d.ts.map +1 -0
  19. package/dist/types/src/components/TextEditor/extensions/table.d.ts.map +1 -1
  20. package/dist/types/src/components/TextEditor/extensions/tooltip.d.ts.map +1 -1
  21. package/dist/types/src/components/TextEditor/extensions/{demo.d.ts → typewriter.d.ts} +2 -3
  22. package/dist/types/src/components/TextEditor/extensions/typewriter.d.ts.map +1 -0
  23. package/dist/types/src/components/TextEditor/extensions/util.d.ts +5 -0
  24. package/dist/types/src/components/TextEditor/extensions/util.d.ts.map +1 -0
  25. package/dist/types/src/components/TextEditor/themes/default.d.ts +5 -13
  26. package/dist/types/src/components/TextEditor/themes/default.d.ts.map +1 -1
  27. package/dist/types/src/hooks/automerge/automerge.stories.d.ts +2 -3
  28. package/dist/types/src/hooks/automerge/automerge.stories.d.ts.map +1 -1
  29. package/dist/types/src/hooks/automerge/plugin.d.ts.map +1 -1
  30. package/dist/types/src/hooks/automerge/{PatchSemaphore.d.ts → semaphore.d.ts} +1 -1
  31. package/dist/types/src/hooks/automerge/semaphore.d.ts.map +1 -0
  32. package/dist/types/src/hooks/automerge/update-automerge.d.ts +6 -0
  33. package/dist/types/src/hooks/automerge/update-automerge.d.ts.map +1 -0
  34. package/dist/types/src/hooks/automerge/update-codemirror.d.ts +5 -0
  35. package/dist/types/src/hooks/automerge/update-codemirror.d.ts.map +1 -0
  36. package/dist/types/src/hooks/useTextModel.d.ts +14 -6
  37. package/dist/types/src/hooks/useTextModel.d.ts.map +1 -1
  38. package/dist/types/src/hooks/yjs/yjs.test.d.ts +2 -0
  39. package/dist/types/src/hooks/yjs/yjs.test.d.ts.map +1 -0
  40. package/dist/types/src/index.d.ts +1 -0
  41. package/dist/types/src/index.d.ts.map +1 -1
  42. package/dist/types/src/testing/replicator.d.ts.map +1 -1
  43. package/package.json +24 -20
  44. package/src/components/TextEditor/MarkdownEditor.stories.tsx +128 -67
  45. package/src/components/TextEditor/TextEditor.stories.tsx +1 -1
  46. package/src/components/TextEditor/TextEditor.tsx +26 -22
  47. package/src/components/TextEditor/extensions/code.ts +12 -12
  48. package/src/components/TextEditor/extensions/comments.ts +307 -92
  49. package/src/components/TextEditor/extensions/hr.ts +74 -0
  50. package/src/components/TextEditor/extensions/image.ts +3 -1
  51. package/src/components/TextEditor/extensions/index.ts +3 -2
  52. package/src/components/TextEditor/extensions/markdown/highlight.ts +5 -8
  53. package/src/components/TextEditor/extensions/mermaid.ts +11 -0
  54. package/src/components/TextEditor/extensions/table.ts +14 -1
  55. package/src/components/TextEditor/extensions/{tooltip.tsx → tooltip.ts} +1 -1
  56. package/src/components/TextEditor/extensions/{demo.ts → typewriter.ts} +1 -2
  57. package/src/components/TextEditor/extensions/util.ts +18 -0
  58. package/src/components/TextEditor/themes/default.ts +61 -38
  59. package/src/hooks/automerge/automerge.stories.tsx +2 -3
  60. package/src/hooks/automerge/plugin.ts +1 -1
  61. package/src/hooks/automerge/{PatchSemaphore.ts → semaphore.ts} +4 -3
  62. package/src/hooks/automerge/{translation/codemirror-to-automerge.ts → update-automerge.ts} +3 -3
  63. package/src/hooks/automerge/{translation/automerge-to-codemirror.ts → update-codemirror.ts} +2 -7
  64. package/src/hooks/useTextModel.ts +59 -22
  65. package/src/hooks/yjs/yjs.test.ts +59 -0
  66. package/src/index.ts +1 -0
  67. package/src/testing/replicator.ts +0 -1
  68. package/dist/types/src/components/TextEditor/extensions/demo.d.ts.map +0 -1
  69. package/dist/types/src/components/TextEditor/extensions/experimental.d.ts +0 -10
  70. package/dist/types/src/components/TextEditor/extensions/experimental.d.ts.map +0 -1
  71. package/dist/types/src/components/TextEditor/extensions/highlight.d.ts +0 -24
  72. package/dist/types/src/components/TextEditor/extensions/highlight.d.ts.map +0 -1
  73. package/dist/types/src/hooks/automerge/PatchSemaphore.d.ts.map +0 -1
  74. package/dist/types/src/hooks/automerge/translation/automerge-to-codemirror.d.ts +0 -5
  75. package/dist/types/src/hooks/automerge/translation/automerge-to-codemirror.d.ts.map +0 -1
  76. package/dist/types/src/hooks/automerge/translation/codemirror-to-automerge.d.ts +0 -6
  77. package/dist/types/src/hooks/automerge/translation/codemirror-to-automerge.d.ts.map +0 -1
  78. package/dist/types/src/hooks/automerge/translation/index.d.ts +0 -3
  79. package/dist/types/src/hooks/automerge/translation/index.d.ts.map +0 -1
  80. package/src/components/TextEditor/extensions/experimental.tsx +0 -114
  81. package/src/components/TextEditor/extensions/highlight.ts +0 -128
  82. package/src/hooks/automerge/translation/index.ts +0 -6
@@ -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
 
@@ -55,7 +55,7 @@ export const tooltip = ({ onHover, regexp = markdownLinkRegexp }: TooltipOptions
55
55
  const el = document.createElement('div');
56
56
  el.className = tooltipContent({}, 'pli-2 plb-1');
57
57
  onHover(el, url);
58
- return { dom: el, offset: { x: 0, y: 12 } };
58
+ return { dom: el, offset: { x: 0, y: 4 } };
59
59
  },
60
60
  };
61
61
  });
@@ -15,10 +15,9 @@ 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
22
  let idx = 0;
24
23
 
@@ -0,0 +1,18 @@
1
+ //
2
+ // Copyright 2024 DXOS.org
3
+ //
4
+
5
+ import { log } from '@dxos/log';
6
+
7
+ /**
8
+ * CodeMirror callbacks swallow errors so wrap handlers.
9
+ */
10
+ // TODO(burdon): Figure out how to systematize this.
11
+ export const callbackWrapper = <T extends Function>(fn: T): T =>
12
+ ((...args: any[]) => {
13
+ try {
14
+ return fn(...args);
15
+ } catch (error) {
16
+ log.catch(error);
17
+ }
18
+ }) as unknown as T;
@@ -3,9 +3,11 @@
3
3
  //
4
4
 
5
5
  import get from 'lodash.get';
6
- import { type StyleSpec } from 'style-mod';
7
6
 
8
- import { tokens } from '../../../styles';
7
+ import { type ThemeStyles, tokens } from '../../../styles';
8
+
9
+ // TODO(burdon): Can we erase All of the default CM styles?
10
+ // TODO(burdon): Theme styles seem to be added multiple times?
9
11
 
10
12
  /**
11
13
  * Minimal styles.
@@ -18,19 +20,31 @@ import { tokens } from '../../../styles';
18
20
  * NOTE: Use one of '&', '&light', and '&dark' prefix to scope instance.
19
21
  * NOTE: `light` and `dark` selectors are preprocessed by CodeMirror and can only be in the base theme.
20
22
  */
21
- export const defaultTheme: {
22
- [selector: string]: StyleSpec;
23
- } = {
23
+ export const defaultTheme: ThemeStyles = {
24
24
  //
25
25
  // Main layout:
26
- // <div class=".cm-editor .cm-focused">
27
- // <div class=".cm-scroller">
28
- // <div class=".cm-content" role="textbox" contenteditable="true">
29
- // <div class=".cm-line" />
26
+ // https://codemirror.net/examples/styling
27
+ //
28
+ // <div class="cm-editor [cm-focused] [generated classes]">
29
+ // <div class="cm-scroller">
30
+ // <div class="cm-gutters">
31
+ // <div class="cm-gutter [...]">
32
+ // <div class="cm-gutterElement">...</div>
33
+ // </div>
34
+ // </div>
35
+ // <div class="cm-content" role="textbox" contenteditable="true">
36
+ // <div class="cm-line"></div>
37
+ // </div>
38
+ // <div class="cm-selectionLayer">
39
+ // <div class="cm-selectionBackground"></div>
40
+ // </div>
41
+ // <div class="cm-cursorLayer">
42
+ // <div class="cm-cursor"></div>
30
43
  // </div>
31
44
  // </div>
32
45
  // </div>
33
46
  //
47
+
34
48
  '&': {},
35
49
  '&.cm-focused': {
36
50
  outline: 'none',
@@ -39,9 +53,10 @@ export const defaultTheme: {
39
53
  overflow: 'visible',
40
54
  fontFamily: get(tokens, 'fontFamily.mono', []).join(','),
41
55
  },
56
+
42
57
  '& .cm-content': {
43
58
  padding: 0,
44
- // Base font size (otherwise defined by HTML tag, which might be different for storybook).
59
+ // NOTE: Base font size (otherwise defined by HTML tag, which might be different for storybook).
45
60
  fontSize: '16px',
46
61
  },
47
62
  '&light .cm-content': {
@@ -72,31 +87,41 @@ export const defaultTheme: {
72
87
  '& .cm-line': {
73
88
  paddingInline: 0,
74
89
  },
75
- '& .cm-line *': {},
76
90
  '& .cm-activeLine': {
77
- backgroundColor: 'transparent',
91
+ background: 'inherit',
78
92
  },
79
93
 
80
94
  //
81
- // selection
95
+ // Selection
82
96
  //
83
97
 
84
- '&light .cm-selectionBackground, &light.cm-focused .cm-selectionBackground': {
85
- background: get(tokens, 'extend.colors.primary.150', '#00ffff'),
98
+ '&light .cm-selectionBackground': {
99
+ background: get(tokens, 'extend.colors.primary.100'),
86
100
  },
87
- '&dark .cm-selectionBackground, &dark.cm-focused .cm-selectionBackground': {
88
- background: get(tokens, 'extend.colors.primary.850', '#00ffff'),
101
+ '&light.cm-focused > .cm-scroller > .cm-selectionLayer .cm-selectionBackground': {
102
+ background: get(tokens, 'extend.colors.primary.200'),
89
103
  },
90
- '&light .cm-selectionMatch': {
91
- background: get(tokens, 'extend.colors.primary.250', '#00ffff') + '44',
104
+ '&dark .cm-selectionBackground': {
105
+ background: get(tokens, 'extend.colors.primary.700'),
92
106
  },
93
- '&dark .cm-selectionMatch': {
94
- background: get(tokens, 'extend.colors.primary.600', '#00ffff') + '44',
107
+ '&dark.cm-focused > .cm-scroller > .cm-selectionLayer .cm-selectionBackground': {
108
+ background: get(tokens, 'extend.colors.primary.600'),
109
+ },
110
+
111
+ //
112
+ // Search
113
+ //
114
+
115
+ '&light .cm-searchMatch': {
116
+ backgroundColor: get(tokens, 'extend.colors.yellow.100'),
117
+ },
118
+ '&dark .cm-searchMatch': {
119
+ backgroundColor: get(tokens, 'extend.colors.yellow.700'),
95
120
  },
96
121
 
97
122
  //
98
123
  // collaboration
99
- // TODO(burdon): Review classnames (YJS dependent?)
124
+ // TODO(burdon): Review classnames (YJS dependent?) Note: e2e tests depend on this class.
100
125
  //
101
126
 
102
127
  '&light .cm-ySelection, &light .cm-yLineSelection': {
@@ -211,6 +236,7 @@ export const defaultTheme: {
211
236
 
212
237
  /**
213
238
  * Panels
239
+ * TODO(burdon): Needs styling attention (esp. dark mode).
214
240
  * https://github.com/codemirror/search/blob/main/src/search.ts#L745
215
241
  *
216
242
  * Find/replace panel.
@@ -226,7 +252,6 @@ export const defaultTheme: {
226
252
  */
227
253
  '& .cm-panels': {
228
254
  border: `1px solid ${get(tokens, 'extend.colors.neutral.200')}`,
229
- borderBottom: 'none',
230
255
  },
231
256
  '& .cm-panel': {
232
257
  background: get(tokens, 'extend.colors.neutral.50'),
@@ -236,20 +261,22 @@ export const defaultTheme: {
236
261
  margin: '4px',
237
262
  fontFamily: get(tokens, 'fontFamily.body', []).join(','),
238
263
  background: get(tokens, 'extend.colors.neutral.100'),
264
+ backgroundImage: 'none',
239
265
  border: 'none',
266
+ '&:hover': {
267
+ background: get(tokens, 'extend.colors.neutral.200'),
268
+ },
269
+ '&:active': {
270
+ background: get(tokens, 'extend.colors.neutral.300'),
271
+ backgroundImage: 'none',
272
+ },
240
273
  },
241
-
242
- '& .cm-searchMatch': {
243
- backgroundColor: get(tokens, 'extend.colors.yellow.100'),
244
- },
245
- '& .cm-searchMatch.cm-searchMatch-selected': {
246
- backgroundColor: get(tokens, 'extend.colors.primary.100'),
274
+ '& .cm-panel input[type=checkbox]': {
275
+ marginRight: '0.4rem !important',
247
276
  },
248
277
  };
249
278
 
250
- export const textTheme: {
251
- [selector: string]: StyleSpec;
252
- } = {
279
+ export const textTheme: ThemeStyles = {
253
280
  '& .cm-scroller': {
254
281
  fontFamily: get(tokens, 'fontFamily.body', []).join(','),
255
282
  },
@@ -258,18 +285,14 @@ export const textTheme: {
258
285
  },
259
286
  };
260
287
 
261
- export const markdownTheme: {
262
- [selector: string]: StyleSpec;
263
- } = {
288
+ export const markdownTheme: ThemeStyles = {
264
289
  // NOTE: Must leave base font family as is (i.e., monospace) due to fenced code blocks.
265
290
  '& .cm-placeholder': {
266
291
  fontFamily: get(tokens, 'fontFamily.body', []).join(','),
267
292
  },
268
293
  };
269
294
 
270
- export const codeTheme: {
271
- [selector: string]: StyleSpec;
272
- } = {
295
+ export const codeTheme: ThemeStyles = {
273
296
  '& .cm-scroller': {
274
297
  fontFamily: get(tokens, 'fontFamily.mono', []).join(','),
275
298
  },
@@ -85,8 +85,7 @@ const Story = () => {
85
85
  export default {
86
86
  title: 'react-ui-editor/Automerge',
87
87
  component: Editor,
88
- };
89
-
90
- export const Default = {
91
88
  render: () => <Story />,
92
89
  };
90
+
91
+ export const Default = {};
@@ -17,8 +17,8 @@ import { ViewPlugin, type EditorView, type PluginValue, type ViewUpdate } from '
17
17
  import * as automerge from '@dxos/automerge/automerge';
18
18
  import { type Heads, type Prop } from '@dxos/automerge/automerge';
19
19
 
20
- import { PatchSemaphore } from './PatchSemaphore';
21
20
  import { type IDocHandle } from './handle';
21
+ import { PatchSemaphore } from './semaphore';
22
22
 
23
23
  export type Value = {
24
24
  lastHeads: Heads;
@@ -10,7 +10,8 @@ import { next as automerge } from '@dxos/automerge/automerge';
10
10
 
11
11
  import { type IDocHandle } from './handle';
12
12
  import { type Field, isReconcileTx, getPath, reconcileAnnotationType, updateHeads, getLastHeads } from './plugin';
13
- import { automergeToCodemirror, codemirrorToAutomerge } from './translation';
13
+ import { updateAutomerge } from './update-automerge';
14
+ import { updateCodeMirror } from './update-codemirror';
14
15
 
15
16
  type Doc<T> = automerge.Doc<T>;
16
17
  type Heads = automerge.Heads;
@@ -55,7 +56,7 @@ export class PatchSemaphore {
55
56
  }
56
57
 
57
58
  // Apply the unreconciled transactions to the document.
58
- let newHeads = codemirrorToAutomerge(this._field, handle, transactions, view.state);
59
+ let newHeads = updateAutomerge(this._field, handle, transactions, view.state);
59
60
 
60
61
  // NOTE: null and undefined each come from automerge and repo respectively.
61
62
  if (newHeads === null || newHeads === undefined) {
@@ -65,7 +66,7 @@ export class PatchSemaphore {
65
66
 
66
67
  // Now get the diff between the updated state of the document and the heads and apply that to the codemirror doc.
67
68
  const diff = automerge.equals(oldHeads, newHeads) ? [] : automerge.diff(handle.docSync()!, oldHeads, newHeads);
68
- automergeToCodemirror(view, selection, path, diff);
69
+ updateCodeMirror(view, selection, path, diff);
69
70
 
70
71
  view.dispatch({
71
72
  effects: updateHeads(newHeads),
@@ -8,10 +8,10 @@ import { type EditorState, type Text, type Transaction } from '@codemirror/state
8
8
 
9
9
  import { next as am, type Heads } from '@dxos/automerge/automerge';
10
10
 
11
- import { type IDocHandle } from '../handle';
12
- import { type Field } from '../plugin';
11
+ import { type IDocHandle } from './handle';
12
+ import { type Field } from './plugin';
13
13
 
14
- export const codemirrorToAutomerge = (
14
+ export const updateAutomerge = (
15
15
  field: Field,
16
16
  handle: IDocHandle,
17
17
  transactions: Transaction[],
@@ -16,14 +16,9 @@ import {
16
16
  type SpliceTextPatch,
17
17
  } from '@dxos/automerge/automerge';
18
18
 
19
- import { reconcileAnnotationType } from '../plugin';
19
+ import { reconcileAnnotationType } from './plugin';
20
20
 
21
- export const automergeToCodemirror = (
22
- view: EditorView,
23
- selection: EditorSelection,
24
- target: Prop[],
25
- patches: Patch[],
26
- ) => {
21
+ export const updateCodeMirror = (view: EditorView, selection: EditorSelection, target: Prop[], patches: Patch[]) => {
27
22
  for (const patch of patches) {
28
23
  const changeSpec = handlePatch(patch, target, view.state);
29
24
  if (changeSpec != null) {
@@ -2,11 +2,12 @@
2
2
  // Copyright 2023 DXOS.org
3
3
  //
4
4
 
5
- import { type Extension } from '@codemirror/state';
5
+ import { type Extension, StateField } from '@codemirror/state';
6
6
  import get from 'lodash.get';
7
7
  import { useEffect, useState } from 'react';
8
8
  import { yCollab } from 'y-codemirror.next';
9
9
  import type * as awarenessProtocol from 'y-protocols/awareness';
10
+ import * as Y from 'yjs';
10
11
 
11
12
  import { invariant } from '@dxos/invariant';
12
13
  import {
@@ -19,6 +20,7 @@ import {
19
20
  } from '@dxos/react-client/echo';
20
21
  import { type Identity } from '@dxos/react-client/halo';
21
22
  import type { YText, YXmlFragment } from '@dxos/text-model';
23
+ import { arrayToString, stringToArray } from '@dxos/util';
22
24
 
23
25
  import { automergePlugin } from './automerge';
24
26
  import { SpaceAwarenessProvider } from './yjs';
@@ -26,19 +28,34 @@ import { SpaceAwarenessProvider } from './yjs';
26
28
  // TODO(burdon): Move.
27
29
  type Awareness = awarenessProtocol.Awareness;
28
30
 
29
- export type DocumentRange = {
30
- id: string;
31
+ /**
32
+ * State field makes the model available to other extensions.
33
+ */
34
+ // TODO(burdon): Use facet?
35
+ export const modelState = StateField.define<EditorModel | undefined>({
36
+ create: () => undefined,
37
+ update: (model) => model,
38
+ });
39
+
40
+ export type Range = {
31
41
  from: number;
32
42
  to: number;
33
43
  };
34
44
 
45
+ export type CommentRange = {
46
+ id: string;
47
+ // TODO(burdon): Split into begin/end?
48
+ cursor: string;
49
+ };
50
+
35
51
  // TODO(wittjosiah): Factor out to common package? @dxos/react-client?
36
52
  export type EditorModel = {
37
53
  id: string;
38
54
  // TODO(burdon): Remove.
39
55
  content: string | YText | YXmlFragment | DocAccessor;
40
56
  text: () => string;
41
- ranges: DocumentRange[];
57
+ getCursorFromRange?: (value: Range) => string;
58
+ getRangeFromCursor?: (cursor: string) => Range;
42
59
  extension?: Extension;
43
60
  awareness?: Awareness;
44
61
  peer?: {
@@ -48,7 +65,7 @@ export type EditorModel = {
48
65
  };
49
66
 
50
67
  // TODO(burdon): Remove space/identity dependency. Define interface for the framework re content and presence.
51
- export type UseTextModelOptions = {
68
+ export type UseTextModelProps = {
52
69
  identity?: Identity | null;
53
70
  space?: Space;
54
71
  text?: TextObject;
@@ -56,39 +73,55 @@ export type UseTextModelOptions = {
56
73
 
57
74
  // TODO(burdon): Remove YJS/Automerge deps (from UI component -- create abstraction; incl. all ECHO/Space deps).
58
75
  // 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]);
76
+ export const useTextModel = (props: UseTextModelProps): EditorModel | undefined => {
77
+ const { identity, space, text } = props;
78
+ const [model, setModel] = useState<EditorModel | undefined>(() => createModel(props));
79
+ useEffect(() => setModel(createModel(props)), [identity, space, text]);
64
80
  return model;
65
81
  };
66
82
 
67
- const createModel = (options: UseTextModelOptions) => {
68
- const { text } = options;
83
+ const createModel = (props: UseTextModelProps) => {
84
+ const { text } = props;
69
85
  if (isActualAutomergeObject(text)) {
70
- return createAutomergeModel(options);
86
+ return createAutomergeModel(props);
71
87
  } else {
72
88
  if (!text?.doc) {
73
89
  return undefined;
74
90
  }
75
91
 
76
- return createYjsModel(options);
92
+ return createYjsModel(props);
77
93
  }
78
94
  };
79
95
 
80
- const createYjsModel = ({ identity, space, text }: UseTextModelOptions): EditorModel => {
96
+ const createYjsModel = ({ identity, space, text }: UseTextModelProps): EditorModel => {
81
97
  invariant(text?.doc && text?.content);
82
98
  const provider = space
83
99
  ? new SpaceAwarenessProvider({ space, doc: text.doc, channel: `yjs.awareness.${text.id}` })
84
100
  : undefined;
85
101
 
86
- return {
102
+ const model: EditorModel = {
87
103
  id: text.doc.guid,
88
104
  content: text.content,
89
105
  text: () => text.content!.toString(),
90
- ranges: [],
91
- extension: yCollab(text.content as YText, provider?.awareness),
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)],
92
125
  awareness: provider?.awareness,
93
126
  peer: identity
94
127
  ? {
@@ -97,18 +130,20 @@ const createYjsModel = ({ identity, space, text }: UseTextModelOptions): EditorM
97
130
  }
98
131
  : undefined,
99
132
  };
133
+
134
+ return model;
100
135
  };
101
136
 
102
- const createAutomergeModel = ({ identity, text }: UseTextModelOptions): EditorModel => {
137
+ const createAutomergeModel = ({ identity, text }: UseTextModelProps): EditorModel => {
103
138
  const obj = text as any as AutomergeTextCompat;
104
139
  const doc = getRawDoc(obj, [obj.field]);
105
140
 
106
- return {
141
+ const model: EditorModel = {
107
142
  id: obj.id,
108
143
  content: doc,
109
144
  text: () => get(doc.handle.docSync(), doc.path),
110
- ranges: [],
111
- extension: automergePlugin(doc.handle, 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)],
112
147
  peer: identity
113
148
  ? {
114
149
  id: identity.identityKey.toHex(),
@@ -116,4 +151,6 @@ const createAutomergeModel = ({ identity, text }: UseTextModelOptions): EditorMo
116
151
  }
117
152
  : undefined,
118
153
  };
154
+
155
+ return model;
119
156
  };
@@ -0,0 +1,59 @@
1
+ //
2
+ // Copyright 2024 DXOS.org
3
+ //
4
+
5
+ import { effect } from '@preact/signals-react';
6
+ import { expect } from 'chai';
7
+ import * as Y from 'yjs';
8
+
9
+ import { Trigger } from '@dxos/async';
10
+ import { Expando, setGlobalAutomergePreference, TextObject } from '@dxos/echo-schema';
11
+ import { registerSignalFactory } from '@dxos/echo-signals';
12
+ import { describe, test } from '@dxos/test';
13
+ import { type YText } from '@dxos/text-model';
14
+
15
+ describe('YJS', () => {
16
+ // https://docs.yjs.dev/api/shared-types/y.text
17
+ // https://github.com/yjs/yjs?tab=readme-ov-file#relative-positions
18
+ test('absolute position', () => {
19
+ const obj = new TextObject('hello world');
20
+ const index = obj.text.indexOf('world');
21
+ const relPos = Y.encodeRelativePosition(Y.createRelativePositionFromTypeIndex(obj.content as YText, index));
22
+
23
+ {
24
+ const cursor = Y.createAbsolutePositionFromRelativePosition(Y.decodeRelativePosition(relPos), obj.doc!);
25
+ expect(cursor!.index).to.equal(index);
26
+ expect(obj.text.substring(cursor!.index)).to.equal('world');
27
+ }
28
+
29
+ (obj.content as YText).insert(index, 'cruel ');
30
+ expect(obj.text).to.equal('hello cruel world');
31
+
32
+ {
33
+ const cursor = Y.createAbsolutePositionFromRelativePosition(Y.decodeRelativePosition(relPos), obj.doc!);
34
+ expect(cursor!.index).not.to.equal(index);
35
+ expect(obj.text.substring(cursor!.index)).to.equal('world');
36
+ }
37
+ });
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
+ });
package/src/index.ts CHANGED
@@ -4,6 +4,7 @@
4
4
 
5
5
  export { TextKind } from '@dxos/protocols/proto/dxos/echo/model/text';
6
6
  export { Doc, YText, YXmlFragment } from '@dxos/text-model';
7
+ export { type Extension } from '@codemirror/state';
7
8
 
8
9
  export * from './components';
9
10
  export * from './hooks';
@@ -169,7 +169,6 @@ export class Replicator {
169
169
  id: doc.guid,
170
170
  text: () => content.toString(),
171
171
  content,
172
- ranges: [],
173
172
  awareness: provider.awareness,
174
173
  peer: { id },
175
174
  };
@@ -1 +0,0 @@
1
- {"version":3,"file":"demo.d.ts","sourceRoot":"","sources":["../../../../../../src/components/TextEditor/extensions/demo.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,KAAK,SAAS,EAAE,MAAM,mBAAmB,CAAC;AAKnD,MAAM,MAAM,WAAW,GAAG;IACxB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,KAAK,CAAC,EAAE,MAAM,EAAE,CAAC;CAClB,CAAC;AAIF;;;GAGG;AACH,eAAO,MAAM,IAAI,sBAA0C,WAAW,KAAQ,SAgD7E,CAAC"}
@@ -1,10 +0,0 @@
1
- import { type Extension } from '@codemirror/state';
2
- import { type DecorationSet, MatchDecorator, ViewPlugin, type ViewUpdate } from '@codemirror/view';
3
- export declare const styles: Extension;
4
- export declare function hyperLinkExtension(): ViewPlugin<{
5
- readonly decorator: MatchDecorator;
6
- decorations: DecorationSet;
7
- update(update: ViewUpdate): void;
8
- }>;
9
- export declare const hyperlinkWidget: Extension;
10
- //# sourceMappingURL=experimental.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"experimental.d.ts","sourceRoot":"","sources":["../../../../../../src/components/TextEditor/extensions/experimental.tsx"],"names":[],"mappings":"AAKA,OAAO,EAAE,KAAK,SAAS,EAAE,MAAM,mBAAmB,CAAC;AACnD,OAAO,EAEL,KAAK,aAAa,EAElB,cAAc,EACd,UAAU,EACV,KAAK,UAAU,EAChB,MAAM,kBAAkB,CAAC;AAG1B,eAAO,MAAM,MAAM,WAYjB,CAAC;AAiDH,wBAAgB,kBAAkB;wBAGR,cAAc;;mBAOnB,UAAU;GAwB9B;AAED,eAAO,MAAM,eAAe,EAAE,SAA0C,CAAC"}
@@ -1,24 +0,0 @@
1
- import { type Extension, StateField } from '@codemirror/state';
2
- import { EditorView } from '@codemirror/view';
3
- import { type DocumentRange } from '../../../hooks';
4
- type HighlightRange = DocumentRange & {
5
- active?: boolean;
6
- };
7
- /**
8
- * Update field set.
9
- */
10
- export declare const useHighlights: (view: EditorView | undefined, ranges?: HighlightRange[]) => void;
11
- /**
12
- * State field that tracks highlight ranges.
13
- */
14
- export declare const highlightStateField: StateField<HighlightRange[]>;
15
- /**
16
- * Decorate ranges.
17
- */
18
- export declare const highlightDecorations: Extension;
19
- export type HighlightOptions = {
20
- onChange?: (active?: string) => {};
21
- };
22
- export declare const highlight: (options?: HighlightOptions) => Extension;
23
- export {};
24
- //# sourceMappingURL=highlight.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"highlight.d.ts","sourceRoot":"","sources":["../../../../../../src/components/TextEditor/extensions/highlight.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,KAAK,SAAS,EAAe,UAAU,EAAE,MAAM,mBAAmB,CAAC;AAC5E,OAAO,EAAc,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAI1D,OAAO,EAAE,KAAK,aAAa,EAAE,MAAM,gBAAgB,CAAC;AAsBpD,KAAK,cAAc,GAAG,aAAa,GAAG;IAAE,MAAM,CAAC,EAAE,OAAO,CAAA;CAAE,CAAC;AAK3D;;GAEG;AAEH,eAAO,MAAM,aAAa,SAAU,UAAU,GAAG,SAAS,WAAU,cAAc,EAAE,SAMnF,CAAC;AAEF;;GAEG;AACH,eAAO,MAAM,mBAAmB,8BAW9B,CAAC;AAEH;;GAEG;AAEH,eAAO,MAAM,oBAAoB,WAiB/B,CAAC;AAoCH,MAAM,MAAM,gBAAgB,GAAG;IAC7B,QAAQ,CAAC,EAAE,CAAC,MAAM,CAAC,EAAE,MAAM,KAAK,EAAE,CAAC;CACpC,CAAC;AAEF,eAAO,MAAM,SAAS,aAAa,gBAAgB,KAAQ,SAE1D,CAAC"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"PatchSemaphore.d.ts","sourceRoot":"","sources":["../../../../../src/hooks/automerge/PatchSemaphore.ts"],"names":[],"mappings":"AAMA,OAAO,EAAE,KAAK,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAEnD,OAAO,EAAE,IAAI,IAAI,SAAS,EAAE,MAAM,2BAA2B,CAAC;AAE9D,OAAO,EAAE,KAAK,UAAU,EAAE,MAAM,UAAU,CAAC;AAC3C,OAAO,EAAE,KAAK,KAAK,EAA8E,MAAM,UAAU,CAAC;AAGlH,KAAK,GAAG,CAAC,CAAC,IAAI,SAAS,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;AAC/B,KAAK,KAAK,GAAG,SAAS,CAAC,KAAK,CAAC;AAE7B,KAAK,QAAQ,GAAG,CAAC,OAAO,EAAE,KAAK,EAAE,MAAM,EAAE,CAAC,GAAG,EAAE,GAAG,CAAC,OAAO,CAAC,KAAK,IAAI,KAAK,KAAK,GAAG,SAAS,CAAC;AAE3F;;GAEG;AACH,qBAAa,cAAc;IACzB,MAAM,EAAG,KAAK,CAAC;IACf,YAAY,UAAS;IACrB,MAAM,EAAE,KAAK,CAAC,QAAQ,CAAC,CAAM;gBAEjB,KAAK,CAAC,EAAE,KAAK;IAMzB,SAAS,WAAY,UAAU,QAAQ,UAAU,UA0C/C;CACH"}
@@ -1,5 +0,0 @@
1
- import { type EditorSelection } from '@codemirror/state';
2
- import { type EditorView } from '@codemirror/view';
3
- import { type Patch, type Prop } from '@dxos/automerge/automerge';
4
- export declare const automergeToCodemirror: (view: EditorView, selection: EditorSelection, target: Prop[], patches: Patch[]) => void;
5
- //# sourceMappingURL=automerge-to-codemirror.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"automerge-to-codemirror.d.ts","sourceRoot":"","sources":["../../../../../../src/hooks/automerge/translation/automerge-to-codemirror.ts"],"names":[],"mappings":"AAMA,OAAO,EAA8B,KAAK,eAAe,EAAoB,MAAM,mBAAmB,CAAC;AACvG,OAAO,EAAE,KAAK,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAEnD,OAAO,EAGL,KAAK,KAAK,EACV,KAAK,IAAI,EAGV,MAAM,2BAA2B,CAAC;AAInC,eAAO,MAAM,qBAAqB,SAC1B,UAAU,aACL,eAAe,UAClB,IAAI,EAAE,WACL,KAAK,EAAE,SAkBjB,CAAC"}
@@ -1,6 +0,0 @@
1
- import { type EditorState, type Transaction } from '@codemirror/state';
2
- import { type Heads } from '@dxos/automerge/automerge';
3
- import { type IDocHandle } from '../handle';
4
- import { type Field } from '../plugin';
5
- export declare const codemirrorToAutomerge: (field: Field, handle: IDocHandle, transactions: Transaction[], state: EditorState) => Heads | undefined;
6
- //# sourceMappingURL=codemirror-to-automerge.d.ts.map