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