@dxos/react-ui-editor 0.3.11-main.6d5715f → 0.3.11-main.870164f

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 (108) hide show
  1. package/dist/lib/browser/index.mjs +961 -1406
  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/codemirror.stories.d.ts.map +1 -1
  5. package/dist/types/src/extensions/comments.d.ts.map +1 -1
  6. package/dist/types/src/extensions/index.d.ts +0 -3
  7. package/dist/types/src/extensions/index.d.ts.map +1 -1
  8. package/dist/types/src/extensions/util.d.ts.map +1 -0
  9. package/dist/types/src/{components/TextEditor → hooks/automerge}/automerge.stories.d.ts +1 -6
  10. package/dist/types/src/hooks/automerge/automerge.stories.d.ts.map +1 -0
  11. package/dist/types/src/hooks/automerge/handle.d.ts +9 -0
  12. package/dist/types/src/hooks/automerge/handle.d.ts.map +1 -0
  13. package/dist/types/src/hooks/automerge/index.d.ts +3 -0
  14. package/dist/types/src/hooks/automerge/index.d.ts.map +1 -0
  15. package/dist/types/src/hooks/automerge/plugin.d.ts +26 -0
  16. package/dist/types/src/hooks/automerge/plugin.d.ts.map +1 -0
  17. package/dist/types/src/{extensions → hooks}/automerge/semaphore.d.ts +4 -4
  18. package/dist/types/src/hooks/automerge/semaphore.d.ts.map +1 -0
  19. package/dist/types/src/hooks/automerge/update-automerge.d.ts +6 -0
  20. package/dist/types/src/hooks/automerge/update-automerge.d.ts.map +1 -0
  21. package/dist/types/src/hooks/automerge/update-codemirror.d.ts.map +1 -0
  22. package/dist/types/src/hooks/index.d.ts +0 -2
  23. package/dist/types/src/hooks/index.d.ts.map +1 -1
  24. package/dist/types/src/hooks/useTextModel.d.ts +7 -8
  25. package/dist/types/src/hooks/useTextModel.d.ts.map +1 -1
  26. package/dist/types/src/hooks/yjs/cursors.d.ts.map +1 -0
  27. package/dist/types/src/hooks/yjs/index.d.ts +3 -0
  28. package/dist/types/src/hooks/yjs/index.d.ts.map +1 -0
  29. package/dist/types/src/hooks/{yjs.d.ts → yjs/space-provider.d.ts} +8 -8
  30. package/dist/types/src/hooks/yjs/space-provider.d.ts.map +1 -0
  31. package/dist/types/src/{components/TextEditor → hooks/yjs}/yjs.stories.d.ts +1 -1
  32. package/dist/types/src/hooks/yjs/yjs.stories.d.ts.map +1 -0
  33. package/dist/types/src/hooks/yjs/yjs.test.d.ts.map +1 -0
  34. package/dist/types/src/styles/index.d.ts +0 -1
  35. package/dist/types/src/styles/index.d.ts.map +1 -1
  36. package/dist/types/src/testing/replicator.d.ts +2 -2
  37. package/dist/types/src/testing/replicator.d.ts.map +1 -1
  38. package/package.json +21 -22
  39. package/src/components/TextEditor/TextEditor.tsx +1 -1
  40. package/src/components/TextEditor/codemirror.stories.ts +2 -3
  41. package/src/extensions/comments.ts +11 -27
  42. package/src/extensions/index.ts +0 -3
  43. package/src/{components/TextEditor → hooks/automerge}/automerge.stories.tsx +6 -66
  44. package/src/hooks/automerge/handle.ts +14 -0
  45. package/src/hooks/automerge/index.ts +6 -0
  46. package/src/{extensions/automerge/automerge.ts → hooks/automerge/plugin.ts} +58 -20
  47. package/src/{extensions → hooks}/automerge/semaphore.ts +12 -14
  48. package/src/{extensions → hooks}/automerge/update-automerge.ts +6 -5
  49. package/src/{extensions → hooks}/automerge/update-codemirror.ts +5 -13
  50. package/src/hooks/index.ts +0 -2
  51. package/src/hooks/useTextModel.ts +93 -19
  52. package/src/hooks/yjs/index.ts +6 -0
  53. package/src/hooks/{yjs.ts → yjs/space-provider.ts} +19 -71
  54. package/src/{components/TextEditor → hooks/yjs}/yjs.stories.tsx +1 -1
  55. package/src/{extensions → hooks}/yjs/yjs.test.ts +25 -1
  56. package/src/styles/index.ts +0 -1
  57. package/src/testing/replicator.ts +15 -10
  58. package/dist/types/src/components/TextEditor/automerge.stories.d.ts.map +0 -1
  59. package/dist/types/src/components/TextEditor/yjs.stories.d.ts.map +0 -1
  60. package/dist/types/src/extensions/automerge/automerge.d.ts +0 -9
  61. package/dist/types/src/extensions/automerge/automerge.d.ts.map +0 -1
  62. package/dist/types/src/extensions/automerge/cursor.d.ts +0 -5
  63. package/dist/types/src/extensions/automerge/cursor.d.ts.map +0 -1
  64. package/dist/types/src/extensions/automerge/defs.d.ts +0 -26
  65. package/dist/types/src/extensions/automerge/defs.d.ts.map +0 -1
  66. package/dist/types/src/extensions/automerge/index.d.ts +0 -3
  67. package/dist/types/src/extensions/automerge/index.d.ts.map +0 -1
  68. package/dist/types/src/extensions/automerge/semaphore.d.ts.map +0 -1
  69. package/dist/types/src/extensions/automerge/update-automerge.d.ts +0 -5
  70. package/dist/types/src/extensions/automerge/update-automerge.d.ts.map +0 -1
  71. package/dist/types/src/extensions/automerge/update-codemirror.d.ts.map +0 -1
  72. package/dist/types/src/extensions/awareness.d.ts +0 -46
  73. package/dist/types/src/extensions/awareness.d.ts.map +0 -1
  74. package/dist/types/src/extensions/yjs/cursor.d.ts +0 -4
  75. package/dist/types/src/extensions/yjs/cursor.d.ts.map +0 -1
  76. package/dist/types/src/extensions/yjs/index.d.ts +0 -2
  77. package/dist/types/src/extensions/yjs/index.d.ts.map +0 -1
  78. package/dist/types/src/extensions/yjs/yjs.d.ts +0 -4
  79. package/dist/types/src/extensions/yjs/yjs.d.ts.map +0 -1
  80. package/dist/types/src/extensions/yjs/yjs.test.d.ts.map +0 -1
  81. package/dist/types/src/hooks/automerge.d.ts +0 -3
  82. package/dist/types/src/hooks/automerge.d.ts.map +0 -1
  83. package/dist/types/src/hooks/awareness-provider.d.ts +0 -28
  84. package/dist/types/src/hooks/awareness-provider.d.ts.map +0 -1
  85. package/dist/types/src/hooks/yjs.d.ts.map +0 -1
  86. package/dist/types/src/styles/cursors.d.ts.map +0 -1
  87. package/dist/types/src/util/cursor.d.ts +0 -17
  88. package/dist/types/src/util/cursor.d.ts.map +0 -1
  89. package/dist/types/src/util/index.d.ts +0 -3
  90. package/dist/types/src/util/index.d.ts.map +0 -1
  91. package/dist/types/src/util/util.d.ts.map +0 -1
  92. package/src/extensions/automerge/cursor.ts +0 -51
  93. package/src/extensions/automerge/defs.ts +0 -62
  94. package/src/extensions/automerge/index.ts +0 -6
  95. package/src/extensions/awareness.ts +0 -316
  96. package/src/extensions/yjs/cursor.ts +0 -21
  97. package/src/extensions/yjs/index.ts +0 -5
  98. package/src/extensions/yjs/yjs.ts +0 -16
  99. package/src/hooks/automerge.ts +0 -52
  100. package/src/hooks/awareness-provider.ts +0 -120
  101. package/src/util/cursor.ts +0 -29
  102. package/src/util/index.ts +0 -6
  103. /package/dist/types/src/{util → extensions}/util.d.ts +0 -0
  104. /package/dist/types/src/{extensions → hooks}/automerge/update-codemirror.d.ts +0 -0
  105. /package/dist/types/src/{styles → hooks/yjs}/cursors.d.ts +0 -0
  106. /package/dist/types/src/{extensions → hooks}/yjs/yjs.test.d.ts +0 -0
  107. /package/src/{util → extensions}/util.ts +0 -0
  108. /package/src/{styles → hooks/yjs}/cursors.ts +0 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dxos/react-ui-editor",
3
- "version": "0.3.11-main.6d5715f",
3
+ "version": "0.3.11-main.870164f",
4
4
  "description": "Document editing experience within a DXOS shell.",
5
5
  "homepage": "https://dxos.org",
6
6
  "bugs": "https://github.com/dxos/dxos/issues",
@@ -38,20 +38,19 @@
38
38
  "y-codemirror.next": "^0.3.2",
39
39
  "y-protocols": "^1.0.5",
40
40
  "yjs": "^13.6.10",
41
- "@dxos/async": "0.3.11-main.6d5715f",
42
- "@dxos/context": "0.3.11-main.6d5715f",
43
- "@dxos/automerge": "0.3.11-main.6d5715f",
44
- "@dxos/display-name": "0.3.11-main.6d5715f",
45
- "@dxos/debug": "0.3.11-main.6d5715f",
46
- "@dxos/echo-schema": "0.3.11-main.6d5715f",
47
- "@dxos/invariant": "0.3.11-main.6d5715f",
48
- "@dxos/log": "0.3.11-main.6d5715f",
49
- "@dxos/protocols": "0.3.11-main.6d5715f",
50
- "@dxos/react-async": "0.3.11-main.6d5715f",
51
- "@dxos/react-ui": "0.3.11-main.6d5715f",
52
- "@dxos/react-ui-theme": "0.3.11-main.6d5715f",
53
- "@dxos/util": "0.3.11-main.6d5715f",
54
- "@dxos/text-model": "0.3.11-main.6d5715f"
41
+ "@dxos/async": "0.3.11-main.870164f",
42
+ "@dxos/automerge": "0.3.11-main.870164f",
43
+ "@dxos/display-name": "0.3.11-main.870164f",
44
+ "@dxos/invariant": "0.3.11-main.870164f",
45
+ "@dxos/debug": "0.3.11-main.870164f",
46
+ "@dxos/log": "0.3.11-main.870164f",
47
+ "@dxos/echo-schema": "0.3.11-main.870164f",
48
+ "@dxos/protocols": "0.3.11-main.870164f",
49
+ "@dxos/react-async": "0.3.11-main.870164f",
50
+ "@dxos/react-ui": "0.3.11-main.870164f",
51
+ "@dxos/text-model": "0.3.11-main.870164f",
52
+ "@dxos/react-ui-theme": "0.3.11-main.870164f",
53
+ "@dxos/util": "0.3.11-main.870164f"
55
54
  },
56
55
  "devDependencies": {
57
56
  "@automerge/automerge-repo-network-broadcastchannel": "^1.0.19",
@@ -67,18 +66,18 @@
67
66
  "deepsignal": "1.4.0-shallow.0",
68
67
  "react": "^18.2.0",
69
68
  "react-dom": "^18.2.0",
70
- "@dxos/config": "0.3.11-main.6d5715f",
71
- "@dxos/react-client": "0.3.11-main.6d5715f",
72
- "@dxos/echo-typegen": "0.3.11-main.6d5715f",
73
- "@dxos/echo-signals": "0.3.11-main.6d5715f",
74
- "@dxos/react-ui": "0.3.11-main.6d5715f",
75
- "@dxos/storybook-utils": "0.3.11-main.6d5715f"
69
+ "@dxos/config": "0.3.11-main.870164f",
70
+ "@dxos/echo-typegen": "0.3.11-main.870164f",
71
+ "@dxos/echo-signals": "0.3.11-main.870164f",
72
+ "@dxos/react-client": "0.3.11-main.870164f",
73
+ "@dxos/storybook-utils": "0.3.11-main.870164f",
74
+ "@dxos/react-ui": "0.3.11-main.870164f"
76
75
  },
77
76
  "peerDependencies": {
78
77
  "@phosphor-icons/react": "^2.0.5",
79
78
  "react": "^18.2.0",
80
79
  "react-dom": "^18.2.0",
81
- "@dxos/react-client": "0.3.11-main.6d5715f"
80
+ "@dxos/react-client": "0.3.11-main.870164f"
82
81
  },
83
82
  "publishConfig": {
84
83
  "access": "public"
@@ -77,7 +77,7 @@ export const BaseTextEditor = forwardRef<TextEditorRef, TextEditorProps>(
77
77
  >();
78
78
  useImperativeHandle(forwardedRef, () => ({ root, state, view }), [view, state, root]);
79
79
 
80
- // TODO(burdon): Factor out as extension.
80
+ // TODO(burdon): Factor out?
81
81
  const { awareness, peer } = model;
82
82
  useEffect(() => {
83
83
  if (awareness && peer) {
@@ -17,8 +17,7 @@ import { type Identity } from '@dxos/react-client/halo';
17
17
  import { joinCommonSpace, TestBuilder, textGenerator } from '@dxos/react-client/testing';
18
18
  import { YText } from '@dxos/text-model';
19
19
 
20
- import { YAwarenessProvider } from '../../hooks';
21
- import { cursorColor } from '../../styles';
20
+ import { cursorColor, SpaceAwarenessProvider } from '../../hooks';
22
21
  import { EditorDocument, types as schema } from '../../testing';
23
22
 
24
23
  export default {
@@ -36,7 +35,7 @@ const updateEditor = async (id: number, editor: HTMLDivElement, identity: Identi
36
35
  return;
37
36
  }
38
37
 
39
- const { awareness } = new YAwarenessProvider({ space, doc, channel: `yjs.awareness.${text.id}` });
38
+ const { awareness } = new SpaceAwarenessProvider({ space, doc, channel: `yjs.awareness.${text.id}` });
40
39
  awareness.setLocalStateField('user', {
41
40
  name: identity.profile?.displayName ?? generateName(identity.identityKey.toHex()),
42
41
  // TODO(wittjosiah): Pick colours from theme based on identity key.
@@ -24,9 +24,9 @@ import { invariant } from '@dxos/invariant';
24
24
  import { log } from '@dxos/log';
25
25
  import { nonNullable } from '@dxos/util';
26
26
 
27
- import { type CommentRange, type EditorModel, type Range } from '../hooks';
27
+ import { callbackWrapper } from './util';
28
+ import { type CommentRange, type EditorModel, modelState, type Range } from '../hooks';
28
29
  import { tokens } from '../styles';
29
- import { callbackWrapper, CursorConverter } from '../util';
30
30
 
31
31
  // TODO(burdon): Handle delete, cut, copy, and paste (separately) text that includes comment range.
32
32
  // TODO(burdon): Consider breaking into separate plugin (since not standalone)? Like mermaid?
@@ -99,8 +99,6 @@ const setCommentState = StateEffect.define<CommentsState>();
99
99
  const commentsStateField = StateField.define<CommentsState>({
100
100
  create: () => ({ ranges: [] }),
101
101
  update: (value, tr) => {
102
- const cursorConverter = tr.state.facet(CursorConverter);
103
-
104
102
  for (const effect of tr.effects) {
105
103
  // Update selection.
106
104
  if (effect.is(setSelection)) {
@@ -109,10 +107,10 @@ const commentsStateField = StateField.define<CommentsState>({
109
107
 
110
108
  // Update range from store.
111
109
  if (effect.is(setCommentRange)) {
112
- const { comments } = effect.value;
110
+ const { model, comments } = effect.value;
113
111
  const ranges: ExtendedCommentRange[] = comments
114
112
  .map((comment) => {
115
- const range = getRangeFromCursor(cursorConverter, comment.cursor);
113
+ const range = model.getRangeFromCursor!(comment.cursor);
116
114
  return range && { ...comment, ...range };
117
115
  })
118
116
  .filter(nonNullable);
@@ -220,15 +218,14 @@ export const comments = (options: CommentsOptions = {}): Extension => {
220
218
  * Create comment thread action.
221
219
  */
222
220
  const createCommentThread: Command = (view) => {
223
- const cursorConverter = view.state.facet(CursorConverter);
224
-
225
221
  invariant(options.onCreate);
226
222
  const { head, from, to } = view.state.selection.main;
227
223
  if (from === to) {
228
224
  return false;
229
225
  }
230
226
 
231
- const relPos = getCursorFromRange(cursorConverter, { from, to });
227
+ const model = view.state.field(modelState);
228
+ const relPos = model?.getCursorFromRange?.({ from, to });
232
229
  if (relPos) {
233
230
  // Create thread via callback.
234
231
  const id = callbackWrapper(options.onCreate)(relPos);
@@ -341,19 +338,19 @@ export const comments = (options: CommentsOptions = {}): Extension => {
341
338
  // Monitor cursor movement and text updates.
342
339
  // TODO(burdon): Is there a better (finer grained) way to do this?
343
340
  //
344
- EditorView.updateListener.of(({ view, state, changes }) => {
345
- const cursorConverter = view.state.facet(CursorConverter);
346
-
341
+ EditorView.updateListener.of(({ view, state, startState, changes }) => {
347
342
  //
348
343
  // Test if need to recompute indexed range if document changes before the end of the range.
349
344
  //
350
345
  {
351
346
  let mod = false;
347
+ const model = state.field(modelState);
352
348
  const { active, ranges } = state.field(commentsStateField);
353
349
  changes.iterChanges((from, to, from2, to2) => {
350
+ invariant(model);
354
351
  ranges.forEach((range) => {
355
352
  if (from2 === to2) {
356
- const newRange = getRangeFromCursor(cursorConverter, range.cursor);
353
+ const newRange = model.getRangeFromCursor!(range.cursor);
357
354
  if (!newRange || newRange.to - newRange.from === 0) {
358
355
  // TODO(burdon): Delete range if empty.
359
356
  log.info('deleted comment', { thread: range.id });
@@ -361,7 +358,7 @@ export const comments = (options: CommentsOptions = {}): Extension => {
361
358
  }
362
359
 
363
360
  if (from <= range.to) {
364
- const newRange = getRangeFromCursor(cursorConverter, range.cursor);
361
+ const newRange = model.getRangeFromCursor!(range.cursor);
365
362
  Object.assign(range, newRange);
366
363
  mod = true;
367
364
  }
@@ -406,16 +403,3 @@ export const comments = (options: CommentsOptions = {}): Extension => {
406
403
  }),
407
404
  ];
408
405
  };
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
- };
@@ -3,8 +3,6 @@
3
3
  //
4
4
 
5
5
  export * from './autocomplete';
6
- export * from './automerge';
7
- export * from './awareness';
8
6
  export * from './basic';
9
7
  export * from './blast';
10
8
  export * from './code';
@@ -19,4 +17,3 @@ export * from './mermaid';
19
17
  export * from './table';
20
18
  export * from './tasklist';
21
19
  export * from './typewriter';
22
- export * from './yjs';
@@ -2,29 +2,19 @@
2
2
  // Copyright 2023 DXOS.org
3
3
  //
4
4
 
5
- import '@dxosTheme';
6
-
5
+ import '@preact/signals-react'; // Register react integration
7
6
  import { BroadcastChannelNetworkAdapter } from '@automerge/automerge-repo-network-broadcastchannel';
8
7
  import { EditorView } from '@codemirror/view';
9
- import '@preact/signals-react'; // Register react integration
10
8
  import { basicSetup } from 'codemirror';
11
9
  import get from 'lodash.get';
12
10
  import React, { useEffect, useRef, useState } from 'react';
13
11
 
12
+ // TODO(burdon): Why separate imports?
14
13
  import { type Prop } from '@dxos/automerge/automerge';
15
- import { Repo, type DocHandle } from '@dxos/automerge/automerge-repo';
16
- import { Filter, setGlobalAutomergePreference } from '@dxos/echo-schema';
17
- import { type PublicKey } from '@dxos/keys';
18
- import { Expando, TextObject, useSpace } from '@dxos/react-client/echo';
19
- import { useIdentity } from '@dxos/react-client/halo';
20
- import { ClientRepeater } from '@dxos/react-client/testing';
21
- import { withTheme } from '@dxos/storybook-utils';
14
+ import { type DocHandle, Repo } from '@dxos/automerge/automerge-repo';
22
15
 
23
- import { MarkdownEditor } from './TextEditor';
24
- import { type IDocHandle, automerge, awareness } from '../../extensions';
25
- import { useTextModel } from '../../hooks';
26
-
27
- // TODO(burdon): Move to components.
16
+ import { type IDocHandle } from './handle';
17
+ import { automergePlugin } from './plugin';
28
18
 
29
19
  type EditorProps = {
30
20
  handle: IDocHandle;
@@ -38,7 +28,7 @@ const Editor = ({ handle, path }: EditorProps) => {
38
28
  useEffect(() => {
39
29
  const view = (editorRoot.current = new EditorView({
40
30
  doc: get(handle.docSync()!, path),
41
- extensions: [basicSetup, automerge({ handle, path }), awareness()],
31
+ extensions: [basicSetup, automergePlugin(handle, path)],
42
32
  parent: containerRef.current as any,
43
33
  }));
44
34
 
@@ -99,53 +89,3 @@ export default {
99
89
  };
100
90
 
101
91
  export const Default = {};
102
-
103
- const EchoStory = ({ id, spaceKey }: { id: number; spaceKey: PublicKey }) => {
104
- const identity = useIdentity();
105
- const space = useSpace(spaceKey);
106
- // TODO(dmaretskyi): useQuery doesn't work.
107
- const [obj] = space?.db.query(Filter.from({ type: 'test' })).objects ?? [];
108
-
109
- const model = useTextModel({
110
- text: obj?.content,
111
- identity,
112
- space,
113
- });
114
-
115
- if (!model) {
116
- return null;
117
- }
118
-
119
- return (
120
- // <div className={mx(fixedInsetFlexLayout, groupSurface)}>
121
- <div className='flex justify-center overflow-y-scroll'>
122
- <div className='flex flex-col w-[800px] py-16'>
123
- <MarkdownEditor model={model} />
124
- <div className='flex shrink-0 h-[300px]'></div>
125
- </div>
126
- </div>
127
- // </div>
128
- );
129
- };
130
-
131
- export const WithEcho = {
132
- render: () => {
133
- setGlobalAutomergePreference(true);
134
- return (
135
- <ClientRepeater
136
- count={2}
137
- createSpace
138
- onCreateSpace={async (space) => {
139
- space.db.add(
140
- new Expando({
141
- type: 'test',
142
- content: new TextObject('Hello world!'),
143
- }),
144
- );
145
- }}
146
- Component={EchoStory}
147
- />
148
- );
149
- },
150
- decorators: [withTheme],
151
- };
@@ -0,0 +1,14 @@
1
+ //
2
+ // Copyright 2023 DXOS.org
3
+ //
4
+
5
+ import type * as A from '@dxos/automerge/automerge';
6
+
7
+ export type IDocHandle<T = any> = {
8
+ docSync(): A.Doc<T> | undefined;
9
+ change(callback: A.ChangeFn<T>, options?: A.ChangeOptions<T>): void;
10
+ changeAt(heads: A.Heads, callback: A.ChangeFn<T>, options?: A.ChangeOptions<T>): string[] | undefined;
11
+
12
+ addListener(event: 'change', listener: () => void): void;
13
+ removeListener(event: 'change', listener: () => void): void;
14
+ };
@@ -0,0 +1,6 @@
1
+ //
2
+ // Copyright 2023 DXOS.org
3
+ //
4
+
5
+ export { type IDocHandle } from './handle';
6
+ export { automergePlugin, type AutomergePlugin } from './plugin';
@@ -1,34 +1,57 @@
1
1
  //
2
2
  // Copyright 2023 DXOS.org
3
- // Copyright 2024 Automerge
4
- // Ref: https://github.com/automerge/automerge-codemirror
5
3
  //
6
4
 
7
- import { Facet, StateField, type Extension, type Transaction } from '@codemirror/state';
5
+ import {
6
+ Annotation,
7
+ type EditorState,
8
+ type Extension,
9
+ Facet,
10
+ StateEffect,
11
+ StateField,
12
+ type Transaction,
13
+ type TransactionSpec,
14
+ } from '@codemirror/state';
8
15
  import { ViewPlugin, type EditorView, type PluginValue, type ViewUpdate } from '@codemirror/view';
9
16
 
10
- import { next as A } from '@dxos/automerge/automerge';
11
- import { type Prop } from '@dxos/automerge/automerge';
17
+ import * as automerge from '@dxos/automerge/automerge';
18
+ import { type Heads, type Prop } from '@dxos/automerge/automerge';
12
19
 
13
- import { cursorConverter } from './cursor';
14
- import { effectType, type IDocHandle, isReconcileTx, type Value } from './defs';
20
+ import { type IDocHandle } from './handle';
15
21
  import { PatchSemaphore } from './semaphore';
16
- import { CursorConverter } from '../../util';
22
+
23
+ export type Value = {
24
+ lastHeads: Heads;
25
+ path: Prop[];
26
+ unreconciledTransactions: Transaction[];
27
+ };
28
+
29
+ type UpdateHeads = {
30
+ newHeads: Heads;
31
+ };
32
+
33
+ export const effectType = StateEffect.define<UpdateHeads>({});
34
+
35
+ export const updateHeads = (newHeads: Heads): StateEffect<UpdateHeads> => effectType.of({ newHeads });
36
+
37
+ export const getLastHeads = (state: EditorState, field: Field): Heads => state.field(field).lastHeads;
38
+
39
+ export const getPath = (state: EditorState, field: Field): Prop[] => state.field(field).path;
40
+
41
+ export type Field = StateField<Value>;
17
42
 
18
43
  const semaphoreFacet = Facet.define<PatchSemaphore, PatchSemaphore>({
19
44
  combine: (values) => values.at(-1)!, // Take last.
20
45
  });
21
46
 
22
- export type AutomergeOptions = {
23
- handle: IDocHandle;
24
- path: Prop[];
47
+ export type AutomergePlugin = {
48
+ extension: Extension;
25
49
  };
26
50
 
27
- export const automerge = ({ handle, path }: AutomergeOptions): Extension => {
28
- // TODO(burdon): Rename.
51
+ export const automergePlugin = (handle: IDocHandle, path: Prop[]): AutomergePlugin => {
29
52
  const stateField: StateField<Value> = StateField.define({
30
53
  create: () => ({
31
- lastHeads: A.getHeads(handle.docSync()!),
54
+ lastHeads: automerge.getHeads(handle.docSync()!),
32
55
  unreconciledTransactions: [],
33
56
  path: path.slice(),
34
57
  }),
@@ -84,11 +107,26 @@ export const automerge = ({ handle, path }: AutomergeOptions): Extension => {
84
107
  );
85
108
 
86
109
  return {
87
- extension: [
88
- CursorConverter.of(cursorConverter(handle, path)),
89
- semaphoreFacet.of(semaphore),
90
- stateField,
91
- viewPlugin,
92
- ],
110
+ extension: [stateField, semaphoreFacet.of(semaphore), viewPlugin],
93
111
  };
94
112
  };
113
+
114
+ export const reconcileAnnotationType = Annotation.define<unknown>();
115
+
116
+ export const isReconcileTx = (tr: Transaction): boolean => !!tr.annotation(reconcileAnnotationType);
117
+
118
+ export const makeReconcile = (tr: TransactionSpec) => {
119
+ if (tr.annotations != null) {
120
+ if (tr.annotations instanceof Array) {
121
+ tr.annotations = [...tr.annotations, reconcileAnnotationType.of({})];
122
+ } else {
123
+ tr.annotations = [tr.annotations, reconcileAnnotationType.of({})];
124
+ }
125
+ } else {
126
+ tr.annotations = [reconcileAnnotationType.of({})];
127
+ }
128
+ // return {
129
+ // ...tr,
130
+ // annotations: reconcileAnnotationType.of({})
131
+ // }
132
+ };
@@ -1,23 +1,15 @@
1
1
  //
2
2
  // Copyright 2023 DXOS.org
3
- // Copyright 2024 Automerge
4
- // Ref: https://github.com/automerge/automerge-codemirror
5
3
  //
6
4
 
7
- import { type StateField } from '@codemirror/state';
5
+ // TODO(burdon): Additional copyright?
6
+
8
7
  import { type EditorView } from '@codemirror/view';
9
8
 
10
9
  import { next as automerge } from '@dxos/automerge/automerge';
11
10
 
12
- import {
13
- getLastHeads,
14
- getPath,
15
- isReconcileTx,
16
- reconcileAnnotationType,
17
- updateHeads,
18
- type IDocHandle,
19
- type Value,
20
- } from './defs';
11
+ import { type IDocHandle } from './handle';
12
+ import { type Field, isReconcileTx, getPath, reconcileAnnotationType, updateHeads, getLastHeads } from './plugin';
21
13
  import { updateAutomerge } from './update-automerge';
22
14
  import { updateCodeMirror } from './update-codemirror';
23
15
 
@@ -30,10 +22,15 @@ type ChangeFn = (atHeads: Heads, change: (doc: Doc<unknown>) => void) => Heads |
30
22
  * TODO(burdon): Comment.
31
23
  */
32
24
  export class PatchSemaphore {
25
+ _field!: Field;
33
26
  _inReconcile = false;
34
27
  _queue: Array<ChangeFn> = [];
35
28
 
36
- constructor(private readonly _field: StateField<Value>) {}
29
+ constructor(field?: Field) {
30
+ if (field !== undefined) {
31
+ this._field = field;
32
+ }
33
+ }
37
34
 
38
35
  reconcile = (handle: IDocHandle, view: EditorView) => {
39
36
  if (this._inReconcile) {
@@ -45,8 +42,9 @@ export class PatchSemaphore {
45
42
  const oldHeads = getLastHeads(view.state, this._field);
46
43
  let selection = view.state.selection;
47
44
 
48
- // First undo all the unreconciled transactions.
49
45
  const transactions = view.state.field(this._field).unreconciledTransactions.filter((tx) => !isReconcileTx(tx));
46
+
47
+ // First undo all the unreconciled transactions.
50
48
  const toInvert = transactions.slice().reverse();
51
49
  for (const tx of toInvert) {
52
50
  const inverted = tx.changes.invert(tx.startState.doc);
@@ -1,17 +1,18 @@
1
1
  //
2
2
  // Copyright 2023 DXOS.org
3
- // Copyright 2024 Automerge
4
- // Ref: https://github.com/automerge/automerge-codemirror
5
3
  //
6
4
 
7
- import { type EditorState, type StateField, type Text, type Transaction } from '@codemirror/state';
5
+ // TODO(burdon): Additional copyright?
6
+
7
+ 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, type Value } from './defs';
11
+ import { type IDocHandle } from './handle';
12
+ import { type Field } from './plugin';
12
13
 
13
14
  export const updateAutomerge = (
14
- field: StateField<Value>,
15
+ field: Field,
15
16
  handle: IDocHandle,
16
17
  transactions: Transaction[],
17
18
  state: EditorState,
@@ -1,9 +1,9 @@
1
1
  //
2
2
  // Copyright 2023 DXOS.org
3
- // Copyright 2024 Automerge
4
- // Ref: https://github.com/automerge/automerge-codemirror
5
3
  //
6
4
 
5
+ // TODO(burdon): Additional copyright?
6
+
7
7
  import { ChangeSet, type ChangeSpec, type EditorSelection, type EditorState } from '@codemirror/state';
8
8
  import { type EditorView } from '@codemirror/view';
9
9
 
@@ -16,7 +16,7 @@ import {
16
16
  type SpliceTextPatch,
17
17
  } from '@dxos/automerge/automerge';
18
18
 
19
- import { reconcileAnnotationType } from './defs';
19
+ import { reconcileAnnotationType } from './plugin';
20
20
 
21
21
  export const updateCodeMirror = (view: EditorView, selection: EditorSelection, target: Prop[], patches: Patch[]) => {
22
22
  for (const patch of patches) {
@@ -56,7 +56,6 @@ const handleInsert = (target: Prop[], patch: InsertPatch): Array<ChangeSpec> =>
56
56
  if (index == null) {
57
57
  return [];
58
58
  }
59
-
60
59
  const text = patch.values.map((v) => (v ? v.toString() : '')).join('');
61
60
  return [{ from: index, to: index, insert: text }];
62
61
  };
@@ -66,7 +65,6 @@ const handleSplice = (target: Prop[], patch: SpliceTextPatch): Array<ChangeSpec>
66
65
  if (index == null) {
67
66
  return [];
68
67
  }
69
-
70
68
  return [{ from: index, insert: patch.value }];
71
69
  };
72
70
 
@@ -75,7 +73,6 @@ const handleDel = (target: Prop[], patch: DelPatch): Array<ChangeSpec> => {
75
73
  if (index == null) {
76
74
  return [];
77
75
  }
78
-
79
76
  const length = patch.length || 1;
80
77
  return [{ from: index, to: index + length }];
81
78
  };
@@ -85,32 +82,27 @@ const handlePut = (target: Prop[], patch: PutPatch, state: EditorState): Array<C
85
82
  if (index == null) {
86
83
  return [];
87
84
  }
88
-
89
85
  const length = state.doc.length;
90
86
  if (typeof patch.value !== 'string') {
91
87
  return []; // TODO(dmaretskyi): How to handle non string values?
92
88
  }
93
-
94
89
  return [{ from: 0, to: length, insert: patch.value as any }];
95
90
  };
96
91
 
97
- // If the path of the patch is of the form [path, <index>] then we know this is a path to a character
98
- // within the sequence given by path.
92
+ // If the path of the patch is of the form [path, <index>] then we know this is
93
+ // a path to a character within the sequence given by path
99
94
  const charPath = (textPath: Prop[], candidatePath: Prop[]): number | null => {
100
95
  if (candidatePath.length !== textPath.length + 1) {
101
96
  return null;
102
97
  }
103
-
104
98
  for (let i = 0; i < textPath.length; i++) {
105
99
  if (textPath[i] !== candidatePath[i]) {
106
100
  return null;
107
101
  }
108
102
  }
109
-
110
103
  const index = candidatePath[candidatePath.length - 1];
111
104
  if (typeof index === 'number') {
112
105
  return index;
113
106
  }
114
-
115
107
  return null;
116
108
  };
@@ -2,7 +2,5 @@
2
2
  // Copyright 2023 DXOS.org
3
3
  //
4
4
 
5
- export * from './automerge';
6
- export * from './awareness-provider';
7
5
  export * from './useTextModel';
8
6
  export * from './yjs';