@dxos/react-ui-editor 0.3.11-main.00a28cb → 0.3.11-main.08776d7
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 +258 -252
- 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 +4 -2
- package/dist/types/src/components/TextEditor/TextEditor.d.ts.map +1 -1
- package/dist/types/src/components/TextEditor/comments.stories.d.ts.map +1 -1
- package/dist/types/src/extensions/automerge/automerge.d.ts.map +1 -1
- package/dist/types/src/extensions/automerge/defs.d.ts +5 -7
- package/dist/types/src/extensions/automerge/defs.d.ts.map +1 -1
- package/dist/types/src/extensions/automerge/semaphore.d.ts +5 -4
- package/dist/types/src/extensions/automerge/semaphore.d.ts.map +1 -1
- package/dist/types/src/extensions/comments.d.ts +12 -12
- package/dist/types/src/extensions/comments.d.ts.map +1 -1
- package/dist/types/src/extensions/cursor.d.ts +3 -3
- package/dist/types/src/extensions/cursor.d.ts.map +1 -1
- package/dist/types/src/extensions/index.d.ts +1 -0
- package/dist/types/src/extensions/index.d.ts.map +1 -1
- package/dist/types/src/extensions/sync.d.ts +7 -0
- package/dist/types/src/extensions/sync.d.ts.map +1 -0
- package/dist/types/src/hooks/automerge.d.ts +2 -1
- package/dist/types/src/hooks/automerge.d.ts.map +1 -1
- package/dist/types/src/hooks/defs.d.ts +28 -0
- package/dist/types/src/hooks/defs.d.ts.map +1 -0
- package/dist/types/src/hooks/index.d.ts +1 -0
- package/dist/types/src/hooks/index.d.ts.map +1 -1
- package/dist/types/src/hooks/useTextModel.d.ts +2 -27
- package/dist/types/src/hooks/useTextModel.d.ts.map +1 -1
- package/dist/types/src/hooks/yjs.d.ts +2 -1
- package/dist/types/src/hooks/yjs.d.ts.map +1 -1
- package/package.json +23 -23
- package/src/components/TextEditor/MarkdownEditor.stories.tsx +5 -5
- package/src/components/TextEditor/TextEditor.tsx +46 -38
- package/src/components/TextEditor/comments.stories.tsx +35 -28
- package/src/extensions/automerge/automerge.ts +10 -39
- package/src/extensions/automerge/defs.ts +8 -13
- package/src/extensions/automerge/semaphore.ts +14 -7
- package/src/extensions/comments.ts +137 -126
- package/src/extensions/cursor.ts +7 -3
- package/src/extensions/index.ts +1 -0
- package/src/extensions/sync.ts +15 -0
- package/src/hooks/automerge.ts +2 -1
- package/src/hooks/defs.ts +47 -0
- package/src/hooks/index.ts +1 -0
- package/src/hooks/useTextModel.ts +2 -45
- package/src/hooks/yjs.ts +2 -1
|
@@ -10,9 +10,9 @@ import { debounce } from '@dxos/async';
|
|
|
10
10
|
import { invariant } from '@dxos/invariant';
|
|
11
11
|
import { nonNullable } from '@dxos/util';
|
|
12
12
|
|
|
13
|
-
import { semaphoreFacet } from './automerge/defs';
|
|
14
13
|
import { Cursor } from './cursor';
|
|
15
|
-
import {
|
|
14
|
+
import { syncFacet } from './sync';
|
|
15
|
+
import { type Comment, type Range } from '../hooks';
|
|
16
16
|
import { getToken } from '../styles';
|
|
17
17
|
import { callbackWrapper } from '../util';
|
|
18
18
|
|
|
@@ -31,39 +31,38 @@ const marks = {
|
|
|
31
31
|
highlightActive: Decoration.mark({ class: 'cm-comment-active' }),
|
|
32
32
|
};
|
|
33
33
|
|
|
34
|
-
|
|
35
|
-
|
|
34
|
+
type CommentState = {
|
|
35
|
+
comment: Comment;
|
|
36
|
+
range: Range;
|
|
37
|
+
location?: Rect | null;
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
type SelectionState = {
|
|
36
41
|
active?: string;
|
|
37
42
|
closest?: string;
|
|
38
43
|
};
|
|
39
44
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
// TODO(burdon): Not part of state; just required for callback.
|
|
44
|
-
location?: Rect | null;
|
|
45
|
-
};
|
|
46
|
-
|
|
47
|
-
export type CommentsState = CommentSelected & {
|
|
48
|
-
ranges: ExtendedCommentRange[];
|
|
45
|
+
export type CommentsState = {
|
|
46
|
+
comments: CommentState[];
|
|
47
|
+
selection: SelectionState;
|
|
49
48
|
};
|
|
50
49
|
|
|
51
50
|
export const setFocus = (view: EditorView, thread: string) => {
|
|
52
|
-
const
|
|
53
|
-
if (!
|
|
51
|
+
const comment = view.state.field(commentsState).comments.find((range) => range.comment.id === thread);
|
|
52
|
+
if (!comment) {
|
|
54
53
|
return;
|
|
55
54
|
}
|
|
56
55
|
|
|
57
56
|
view.dispatch({
|
|
58
57
|
effects: setSelection.of({ active: thread }),
|
|
59
|
-
selection: { anchor: range.from },
|
|
58
|
+
selection: { anchor: comment.range.from },
|
|
60
59
|
scrollIntoView: true, // TODO(burdon): Scroll to y-position (or center of screen?)
|
|
61
60
|
});
|
|
62
61
|
};
|
|
63
62
|
|
|
64
|
-
export const
|
|
63
|
+
export const setComments = StateEffect.define<Comment[]>();
|
|
65
64
|
|
|
66
|
-
export const setSelection = StateEffect.define<
|
|
65
|
+
export const setSelection = StateEffect.define<SelectionState>();
|
|
67
66
|
|
|
68
67
|
const setCommentState = StateEffect.define<CommentsState>();
|
|
69
68
|
|
|
@@ -71,29 +70,31 @@ const setCommentState = StateEffect.define<CommentsState>();
|
|
|
71
70
|
* State field (reducer) that tracks comment ranges.
|
|
72
71
|
* The ranges are tracked as Automerge cursors from which the absolute indexed ranges can be computed.
|
|
73
72
|
*/
|
|
74
|
-
const
|
|
75
|
-
create: () => ({
|
|
73
|
+
const commentsState = StateField.define<CommentsState>({
|
|
74
|
+
create: () => ({ comments: [], selection: {} }),
|
|
76
75
|
update: (value, tr) => {
|
|
77
|
-
const cursorConverter = tr.state.facet(Cursor.converter);
|
|
78
|
-
|
|
79
76
|
for (const effect of tr.effects) {
|
|
80
77
|
// Update selection.
|
|
81
78
|
if (effect.is(setSelection)) {
|
|
82
|
-
return { ...value,
|
|
79
|
+
return { ...value, selection: effect.value };
|
|
83
80
|
}
|
|
84
81
|
|
|
85
82
|
// Update range from store.
|
|
86
|
-
if (effect.is(
|
|
87
|
-
const
|
|
88
|
-
const ranges: ExtendedCommentRange[] = comments
|
|
83
|
+
if (effect.is(setComments)) {
|
|
84
|
+
const comments: CommentState[] = effect.value
|
|
89
85
|
.map((comment) => {
|
|
90
|
-
|
|
86
|
+
// Skip cut/deleted comments.
|
|
87
|
+
if (!comment.cursor) {
|
|
88
|
+
return undefined;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const range = Cursor.getRangeFromCursor(tr.state, comment.cursor);
|
|
91
92
|
// console.log('update', JSON.stringify({ range, cursor: comment.cursor }, undefined, 2));
|
|
92
|
-
return range && {
|
|
93
|
+
return range && { comment, range };
|
|
93
94
|
})
|
|
94
95
|
.filter(nonNullable);
|
|
95
96
|
|
|
96
|
-
return { ...value,
|
|
97
|
+
return { ...value, comments };
|
|
97
98
|
}
|
|
98
99
|
|
|
99
100
|
// Update entire state.
|
|
@@ -109,24 +110,27 @@ const commentsStateField = StateField.define<CommentsState>({
|
|
|
109
110
|
/**
|
|
110
111
|
* Decorate ranges.
|
|
111
112
|
*/
|
|
112
|
-
const highlightDecorations = EditorView.decorations.compute([
|
|
113
|
-
const {
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
113
|
+
const highlightDecorations = EditorView.decorations.compute([commentsState], (state) => {
|
|
114
|
+
const {
|
|
115
|
+
selection: { active },
|
|
116
|
+
comments,
|
|
117
|
+
} = state.field(commentsState);
|
|
118
|
+
|
|
119
|
+
const decorations = sortBy(comments ?? [], (range) => range.range.from)
|
|
120
|
+
?.flatMap((comment) => {
|
|
121
|
+
const range = comment.range;
|
|
122
|
+
if (!range || range.from === range.to) {
|
|
123
|
+
console.warn('Invalid range:', range);
|
|
124
|
+
return undefined;
|
|
125
|
+
}
|
|
121
126
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
.filter(nonNullable) ?? [];
|
|
127
|
+
if (comment.comment.id === active) {
|
|
128
|
+
return marks.highlightActive.range(range.from, range.to);
|
|
129
|
+
} else {
|
|
130
|
+
return marks.highlight.range(range.from, range.to);
|
|
131
|
+
}
|
|
132
|
+
})
|
|
133
|
+
.filter(nonNullable);
|
|
130
134
|
|
|
131
135
|
return Decoration.set(decorations);
|
|
132
136
|
});
|
|
@@ -162,14 +166,15 @@ export type CommentsOptions = {
|
|
|
162
166
|
const trackPastedComments = (onUpdate: NonNullable<CommentsOptions['onUpdate']>) => {
|
|
163
167
|
// Cut/deleted comments.
|
|
164
168
|
// Tracks indexed selections within text.
|
|
169
|
+
// TODO(burdon): Move to main state field?
|
|
165
170
|
let tracked: { text: Text; comments: { id: string; from: number; to: number }[] } | null = null;
|
|
166
171
|
|
|
167
172
|
// Track cut or copy (enables cut-and-paste and copy-delete-paste to restore comment selection).
|
|
168
173
|
const handleTrack = (event: Event, view: EditorView) => {
|
|
169
|
-
const comments = view.state.field(
|
|
174
|
+
const comments = view.state.field(commentsState);
|
|
170
175
|
const { main } = view.state.selection;
|
|
171
|
-
const selectedRanges = comments.
|
|
172
|
-
(range) => range.from >= main.from && range.to <= main.to && range.from < range.to,
|
|
176
|
+
const selectedRanges = comments.comments.filter(
|
|
177
|
+
({ range }) => range.from >= main.from && range.to <= main.to && range.from < range.to,
|
|
173
178
|
);
|
|
174
179
|
|
|
175
180
|
if (!selectedRanges.length) {
|
|
@@ -177,8 +182,8 @@ const trackPastedComments = (onUpdate: NonNullable<CommentsOptions['onUpdate']>)
|
|
|
177
182
|
} else {
|
|
178
183
|
tracked = {
|
|
179
184
|
text: view.state.doc.slice(main.from, main.to),
|
|
180
|
-
comments: selectedRanges.map((range) => ({
|
|
181
|
-
id:
|
|
185
|
+
comments: selectedRanges.map(({ comment, range }) => ({
|
|
186
|
+
id: comment.id,
|
|
182
187
|
from: range.from - main.from,
|
|
183
188
|
to: range.to - main.from,
|
|
184
189
|
})),
|
|
@@ -211,24 +216,19 @@ const trackPastedComments = (onUpdate: NonNullable<CommentsOptions['onUpdate']>)
|
|
|
211
216
|
});
|
|
212
217
|
|
|
213
218
|
if (found > -1) {
|
|
214
|
-
const comments = tracked.comments;
|
|
215
|
-
|
|
216
219
|
// Sync before recomputing cursor/range.
|
|
217
|
-
// TODO(burdon):
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
const
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
// console.log('paste', JSON.stringify({ moved, range, cursor }, undefined, 2));
|
|
230
|
-
onUpdate(moved.id, cursor);
|
|
231
|
-
}
|
|
220
|
+
// TODO(burdon): Try to avoid this? See TextEditor dispatch.
|
|
221
|
+
state.facet(syncFacet)?.reconcile(view);
|
|
222
|
+
|
|
223
|
+
for (const moved of tracked.comments) {
|
|
224
|
+
const range = {
|
|
225
|
+
from: found + moved.from,
|
|
226
|
+
to: found + moved.to,
|
|
227
|
+
};
|
|
228
|
+
|
|
229
|
+
const cursor = Cursor.getCursorFromRange(state, range);
|
|
230
|
+
// console.log('paste', JSON.stringify({ moved, range, cursor }, undefined, 2));
|
|
231
|
+
onUpdate(moved.id, cursor);
|
|
232
232
|
}
|
|
233
233
|
}
|
|
234
234
|
|
|
@@ -259,15 +259,23 @@ export const comments = (options: CommentsOptions = {}): Extension => {
|
|
|
259
259
|
* Create comment thread action.
|
|
260
260
|
*/
|
|
261
261
|
const createCommentThread: Command = (view) => {
|
|
262
|
-
const cursorConverter = view.state.facet(Cursor.converter);
|
|
263
|
-
|
|
264
262
|
invariant(options.onCreate);
|
|
265
263
|
const { from, to } = view.state.selection.main;
|
|
266
264
|
if (from === to) {
|
|
267
265
|
return false;
|
|
268
266
|
}
|
|
269
267
|
|
|
270
|
-
|
|
268
|
+
// Don't allow selection at end of document.
|
|
269
|
+
if (to === view.state.doc.length) {
|
|
270
|
+
view.dispatch({
|
|
271
|
+
changes: {
|
|
272
|
+
from: to,
|
|
273
|
+
insert: '\n',
|
|
274
|
+
},
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
const cursor = Cursor.getCursorFromRange(view.state, { from, to });
|
|
271
279
|
if (cursor) {
|
|
272
280
|
// Create thread via callback.
|
|
273
281
|
const id = callbackWrapper(options.onCreate)(cursor, view.coordsAtPos(from));
|
|
@@ -286,7 +294,7 @@ export const comments = (options: CommentsOptions = {}): Extension => {
|
|
|
286
294
|
};
|
|
287
295
|
|
|
288
296
|
return [
|
|
289
|
-
|
|
297
|
+
commentsState,
|
|
290
298
|
highlightDecorations,
|
|
291
299
|
styles,
|
|
292
300
|
|
|
@@ -297,6 +305,7 @@ export const comments = (options: CommentsOptions = {}): Extension => {
|
|
|
297
305
|
? keymap.of([
|
|
298
306
|
{
|
|
299
307
|
key: shortcut,
|
|
308
|
+
// TODO(burdon): Check if there's a better way?
|
|
300
309
|
run: callbackWrapper(createCommentThread),
|
|
301
310
|
},
|
|
302
311
|
])
|
|
@@ -334,72 +343,74 @@ export const comments = (options: CommentsOptions = {}): Extension => {
|
|
|
334
343
|
: [],
|
|
335
344
|
|
|
336
345
|
//
|
|
337
|
-
//
|
|
338
|
-
// TODO(burdon): Is there a better (finer grained) way to do this?
|
|
346
|
+
// Track deleted ranges and update ranges for decorations.
|
|
339
347
|
//
|
|
340
348
|
EditorView.updateListener.of(({ view, state, changes }) => {
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
// TODO(burdon): Skip deleted (tracked) ranges.
|
|
351
|
-
ranges.forEach((range) => {
|
|
352
|
-
if (from2 === to2) {
|
|
353
|
-
const newRange = Cursor.getRangeFromCursor(cursorConverter, range.cursor);
|
|
354
|
-
if (!newRange || newRange.to - newRange.from === 0) {
|
|
355
|
-
options.onDelete?.(range.id);
|
|
356
|
-
}
|
|
349
|
+
let mod = false;
|
|
350
|
+
const { comments, ...value } = state.field(commentsState);
|
|
351
|
+
changes.iterChanges((from, to, from2, to2) => {
|
|
352
|
+
comments.forEach(({ comment, range }) => {
|
|
353
|
+
// Test if range deleted.
|
|
354
|
+
if (from2 === to2) {
|
|
355
|
+
const newRange = Cursor.getRangeFromCursor(view.state, comment.cursor!);
|
|
356
|
+
if (!newRange || newRange.to - newRange.from === 0) {
|
|
357
|
+
options.onDelete?.(comment.id);
|
|
357
358
|
}
|
|
359
|
+
}
|
|
358
360
|
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
}
|
|
361
|
+
// Update range.
|
|
362
|
+
if (from <= range.to) {
|
|
363
|
+
const newRange = Cursor.getRangeFromCursor(view.state, comment.cursor!);
|
|
364
|
+
Object.assign(range, newRange);
|
|
365
|
+
mod = true;
|
|
366
|
+
}
|
|
365
367
|
});
|
|
368
|
+
});
|
|
366
369
|
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
}
|
|
370
|
+
if (mod) {
|
|
371
|
+
view.dispatch({ effects: setCommentState.of({ comments, ...value }) });
|
|
370
372
|
}
|
|
373
|
+
}),
|
|
371
374
|
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
375
|
+
//
|
|
376
|
+
// Track selection/proximity.
|
|
377
|
+
//
|
|
378
|
+
EditorView.updateListener.of(({ view, state, changes }) => {
|
|
379
|
+
let min = Infinity;
|
|
380
|
+
const {
|
|
381
|
+
selection: { active, closest },
|
|
382
|
+
comments,
|
|
383
|
+
} = state.field(commentsState);
|
|
384
|
+
|
|
385
|
+
const { head } = state.selection.main;
|
|
386
|
+
const selection: SelectionState = {};
|
|
387
|
+
comments.forEach(({ comment, range }) => {
|
|
388
|
+
if (head >= range.from && head <= range.to) {
|
|
389
|
+
selection.active = comment.id;
|
|
390
|
+
selection.closest = undefined;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
if (!selection.active) {
|
|
394
|
+
const d = Math.min(Math.abs(head - range.from), Math.abs(head - range.to));
|
|
388
395
|
if (d < min) {
|
|
389
|
-
|
|
396
|
+
selection.closest = comment.id;
|
|
390
397
|
min = d;
|
|
391
398
|
}
|
|
392
|
-
});
|
|
393
|
-
|
|
394
|
-
if (selected.active !== active || selected.closest !== closest) {
|
|
395
|
-
view.dispatch({ effects: setSelection.of(selected) });
|
|
396
|
-
|
|
397
|
-
// Update callback.
|
|
398
|
-
handleSelect({
|
|
399
|
-
...selected,
|
|
400
|
-
ranges: ranges.map((range) => ({ ...range, location: view.coordsAtPos(range.from) })),
|
|
401
|
-
});
|
|
402
399
|
}
|
|
400
|
+
});
|
|
401
|
+
|
|
402
|
+
if (selection.active !== active || selection.closest !== closest) {
|
|
403
|
+
view.dispatch({ effects: setSelection.of(selection) });
|
|
404
|
+
|
|
405
|
+
// Update callback.
|
|
406
|
+
handleSelect({
|
|
407
|
+
selection,
|
|
408
|
+
comments: comments.map(({ comment, range }) => ({
|
|
409
|
+
comment,
|
|
410
|
+
range,
|
|
411
|
+
location: view.coordsAtPos(range.from),
|
|
412
|
+
})),
|
|
413
|
+
});
|
|
403
414
|
}
|
|
404
415
|
}),
|
|
405
416
|
|
package/src/extensions/cursor.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// Copyright 2024 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
-
import { Facet } from '@codemirror/state';
|
|
5
|
+
import { type EditorState, Facet } from '@codemirror/state';
|
|
6
6
|
|
|
7
7
|
import type { Range } from '../hooks';
|
|
8
8
|
|
|
@@ -31,13 +31,17 @@ export class Cursor {
|
|
|
31
31
|
combine: (providers) => providers[0] ?? defaultCursorConverter,
|
|
32
32
|
});
|
|
33
33
|
|
|
34
|
-
static readonly getCursorFromRange = (
|
|
34
|
+
static readonly getCursorFromRange = (state: EditorState, range: Range) => {
|
|
35
|
+
const cursorConverter = state.facet(Cursor.converter);
|
|
36
|
+
|
|
35
37
|
const from = cursorConverter.toCursor(range.from);
|
|
36
38
|
const to = cursorConverter.toCursor(range.to, -1);
|
|
37
39
|
return [from, to].join(':');
|
|
38
40
|
};
|
|
39
41
|
|
|
40
|
-
static readonly getRangeFromCursor = (
|
|
42
|
+
static readonly getRangeFromCursor = (state: EditorState, cursor: string) => {
|
|
43
|
+
const cursorConverter = state.facet(Cursor.converter);
|
|
44
|
+
|
|
41
45
|
const parts = cursor.split(':');
|
|
42
46
|
const from = cursorConverter.fromCursor(parts[0]);
|
|
43
47
|
const to = cursorConverter.fromCursor(parts[1]);
|
package/src/extensions/index.ts
CHANGED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2024 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { Facet } from '@codemirror/state';
|
|
6
|
+
import type { EditorView } from '@codemirror/view';
|
|
7
|
+
|
|
8
|
+
export interface Syncer {
|
|
9
|
+
// TODO(burdon): Rename?
|
|
10
|
+
reconcile(view: EditorView): void;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export const syncFacet = Facet.define<Syncer, Syncer>({
|
|
14
|
+
combine: (values) => values.at(-1)!, // Take last.
|
|
15
|
+
});
|
package/src/hooks/automerge.ts
CHANGED
|
@@ -8,7 +8,8 @@ import { type AutomergeTextCompat, getRawDoc } from '@dxos/echo-schema';
|
|
|
8
8
|
import { isNotNullOrUndefined } from '@dxos/util';
|
|
9
9
|
|
|
10
10
|
import { SpaceAwarenessProvider } from './awareness-provider';
|
|
11
|
-
import { type EditorModel, modelState
|
|
11
|
+
import { type EditorModel, modelState } from './defs';
|
|
12
|
+
import { type UseTextModelProps } from './useTextModel';
|
|
12
13
|
import { automerge, awareness, AwarenessProvider } from '../extensions';
|
|
13
14
|
import { cursorColor } from '../styles';
|
|
14
15
|
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2024 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { type Extension, StateField } from '@codemirror/state';
|
|
6
|
+
import type * as YP from 'y-protocols/awareness';
|
|
7
|
+
|
|
8
|
+
import { type DocAccessor } from '@dxos/echo-schema';
|
|
9
|
+
import type { YText, YXmlFragment } from '@dxos/text-model';
|
|
10
|
+
|
|
11
|
+
// Runtime data structure.
|
|
12
|
+
export type Range = {
|
|
13
|
+
from: number;
|
|
14
|
+
to: number;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
// Persistent data structure.
|
|
18
|
+
export type Comment = {
|
|
19
|
+
id: string;
|
|
20
|
+
cursor?: string;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export type EditorModel = {
|
|
24
|
+
id: string;
|
|
25
|
+
text: () => string;
|
|
26
|
+
extension?: Extension;
|
|
27
|
+
|
|
28
|
+
// TODO(burdon): Remove.
|
|
29
|
+
content: string | YText | YXmlFragment | DocAccessor;
|
|
30
|
+
|
|
31
|
+
// TODO(burdon): Move into extension.
|
|
32
|
+
awareness?: YP.Awareness;
|
|
33
|
+
|
|
34
|
+
// TODO(burdon): Remove.
|
|
35
|
+
peer?: {
|
|
36
|
+
id: string;
|
|
37
|
+
name?: string;
|
|
38
|
+
};
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* State field makes the model available to other extensions.
|
|
43
|
+
*/
|
|
44
|
+
export const modelState = StateField.define<EditorModel | undefined>({
|
|
45
|
+
create: () => undefined,
|
|
46
|
+
update: (model) => model,
|
|
47
|
+
});
|
package/src/hooks/index.ts
CHANGED
|
@@ -2,56 +2,15 @@
|
|
|
2
2
|
// Copyright 2023 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
-
import { StateField, type Extension } from '@codemirror/state';
|
|
6
5
|
import { useEffect, useState } from 'react';
|
|
7
|
-
import type * as YP from 'y-protocols/awareness';
|
|
8
6
|
|
|
9
|
-
import { isAutomergeObject, type
|
|
7
|
+
import { isAutomergeObject, type Space, type TextObject } from '@dxos/react-client/echo';
|
|
10
8
|
import { type Identity } from '@dxos/react-client/halo';
|
|
11
|
-
import type { YText, YXmlFragment } from '@dxos/text-model';
|
|
12
9
|
|
|
13
10
|
import { createAutomergeModel } from './automerge';
|
|
11
|
+
import { type EditorModel } from './defs';
|
|
14
12
|
import { createYjsModel } from './yjs';
|
|
15
13
|
|
|
16
|
-
// TODO(burdon): Factor out defs.
|
|
17
|
-
|
|
18
|
-
/**
|
|
19
|
-
* State field makes the model available to other extensions.
|
|
20
|
-
*/
|
|
21
|
-
export const modelState = StateField.define<EditorModel | undefined>({
|
|
22
|
-
create: () => undefined,
|
|
23
|
-
update: (model) => model,
|
|
24
|
-
});
|
|
25
|
-
|
|
26
|
-
export type Range = {
|
|
27
|
-
from: number;
|
|
28
|
-
to: number;
|
|
29
|
-
};
|
|
30
|
-
|
|
31
|
-
export type CommentRange = {
|
|
32
|
-
id: string;
|
|
33
|
-
cursor: string; // TODO(burdon): Rename range.
|
|
34
|
-
};
|
|
35
|
-
|
|
36
|
-
// TODO(wittjosiah): Factor out to common package? @dxos/react-client?
|
|
37
|
-
export type EditorModel = {
|
|
38
|
-
id: string;
|
|
39
|
-
text: () => string;
|
|
40
|
-
extension?: Extension;
|
|
41
|
-
|
|
42
|
-
// TODO(burdon): Remove.
|
|
43
|
-
content: string | YText | YXmlFragment | DocAccessor;
|
|
44
|
-
|
|
45
|
-
// TODO(burdon): Move into extension.
|
|
46
|
-
awareness?: YP.Awareness;
|
|
47
|
-
|
|
48
|
-
// TODO(burdon): Remove.
|
|
49
|
-
peer?: {
|
|
50
|
-
id: string;
|
|
51
|
-
name?: string;
|
|
52
|
-
};
|
|
53
|
-
};
|
|
54
|
-
|
|
55
14
|
// TODO(burdon): Remove space/identity dependency. Define interface for the framework re content and presence.
|
|
56
15
|
export type UseTextModelProps = {
|
|
57
16
|
identity?: Identity | null;
|
|
@@ -59,8 +18,6 @@ export type UseTextModelProps = {
|
|
|
59
18
|
text?: TextObject;
|
|
60
19
|
};
|
|
61
20
|
|
|
62
|
-
// TODO(burdon): Remove YJS/Automerge deps (from UI component -- create abstraction; incl. all ECHO/Space deps).
|
|
63
|
-
// TODO(wittjosiah): Factor out to common package? @dxos/react-client?
|
|
64
21
|
export const useTextModel = (props: UseTextModelProps): EditorModel | undefined => {
|
|
65
22
|
const { identity, space, text } = props;
|
|
66
23
|
const [model, setModel] = useState<EditorModel | undefined>();
|
package/src/hooks/yjs.ts
CHANGED
|
@@ -14,7 +14,8 @@ import { type Space } from '@dxos/react-client/echo';
|
|
|
14
14
|
import { type GossipMessage } from '@dxos/react-client/mesh';
|
|
15
15
|
import type { YText } from '@dxos/text-model';
|
|
16
16
|
|
|
17
|
-
import { type EditorModel, modelState
|
|
17
|
+
import { type EditorModel, modelState } from './defs';
|
|
18
|
+
import { type UseTextModelProps } from './useTextModel';
|
|
18
19
|
import { yjs } from '../extensions';
|
|
19
20
|
|
|
20
21
|
export const createYjsModel = ({ identity, space, text }: UseTextModelProps): EditorModel => {
|