@dxos/react-ui-editor 0.3.11-main.00a28cb → 0.3.11-main.0252f93
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 +1304 -1092
- package/dist/lib/browser/index.mjs.map +4 -4
- package/dist/lib/browser/meta.json +1 -1
- package/dist/types/src/components/TextEditor/MarkdownEditor.stories.d.ts +12 -17
- package/dist/types/src/components/TextEditor/MarkdownEditor.stories.d.ts.map +1 -1
- package/dist/types/src/components/TextEditor/TextEditor.d.ts +13 -5
- package/dist/types/src/components/TextEditor/TextEditor.d.ts.map +1 -1
- package/dist/types/src/components/TextEditor/TextEditor.stories.d.ts +1 -1
- package/dist/types/src/components/TextEditor/comments.stories.d.ts.map +1 -1
- package/dist/types/src/components/Toolbar/Toolbar.d.ts +9 -0
- package/dist/types/src/components/Toolbar/Toolbar.d.ts.map +1 -0
- package/dist/types/src/components/Toolbar/Toolbar.stories.d.ts +14 -0
- package/dist/types/src/components/Toolbar/Toolbar.stories.d.ts.map +1 -0
- package/dist/types/src/extensions/autocomplete.d.ts.map +1 -1
- package/dist/types/src/extensions/automerge/automerge.d.ts.map +1 -1
- package/dist/types/src/extensions/automerge/automerge.spec.d.ts +2 -0
- package/dist/types/src/extensions/automerge/automerge.spec.d.ts.map +1 -0
- package/dist/types/src/extensions/automerge/automerge.test.d.ts +2 -0
- package/dist/types/src/extensions/automerge/automerge.test.d.ts.map +1 -0
- package/dist/types/src/extensions/automerge/defs.d.ts +8 -10
- package/dist/types/src/extensions/automerge/defs.d.ts.map +1 -1
- package/dist/types/src/extensions/automerge/semaphore.d.ts +7 -6
- package/dist/types/src/extensions/automerge/semaphore.d.ts.map +1 -1
- package/dist/types/src/extensions/automerge/update-automerge.d.ts +2 -2
- package/dist/types/src/extensions/automerge/update-automerge.d.ts.map +1 -1
- package/dist/types/src/extensions/awareness.d.ts +2 -2
- package/dist/types/src/extensions/awareness.d.ts.map +1 -1
- package/dist/types/src/extensions/basic.d.ts +4 -5
- package/dist/types/src/extensions/basic.d.ts.map +1 -1
- package/dist/types/src/extensions/comments.d.ts +23 -15
- 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 +0 -7
- package/dist/types/src/extensions/index.d.ts.map +1 -1
- package/dist/types/src/extensions/markdown/bundle.d.ts +3 -6
- package/dist/types/src/extensions/markdown/bundle.d.ts.map +1 -1
- package/dist/types/src/extensions/markdown/code.d.ts.map +1 -0
- package/dist/types/src/extensions/markdown/formatting.d.ts +11 -0
- package/dist/types/src/extensions/markdown/formatting.d.ts.map +1 -0
- package/dist/types/src/extensions/markdown/heading.d.ts +6 -0
- package/dist/types/src/extensions/markdown/heading.d.ts.map +1 -0
- package/dist/types/src/extensions/markdown/highlight.d.ts.map +1 -1
- package/dist/types/src/extensions/markdown/hr.d.ts.map +1 -0
- package/dist/types/src/extensions/markdown/image.d.ts.map +1 -0
- package/dist/types/src/extensions/markdown/index.d.ts +8 -0
- package/dist/types/src/extensions/markdown/index.d.ts.map +1 -1
- package/dist/types/src/extensions/markdown/link.d.ts.map +1 -0
- package/dist/types/src/extensions/markdown/table.d.ts.map +1 -0
- package/dist/types/src/extensions/markdown/tasklist.d.ts +3 -0
- package/dist/types/src/extensions/markdown/tasklist.d.ts.map +1 -0
- package/dist/types/src/extensions/mention.d.ts.map +1 -1
- package/dist/types/src/hooks/awareness-provider.d.ts +7 -4
- package/dist/types/src/hooks/awareness-provider.d.ts.map +1 -1
- package/dist/types/src/hooks/defs.d.ts +25 -0
- package/dist/types/src/hooks/defs.d.ts.map +1 -0
- package/dist/types/src/hooks/index.d.ts +3 -2
- package/dist/types/src/hooks/index.d.ts.map +1 -1
- package/dist/types/src/hooks/useActionHandler.d.ts +4 -0
- package/dist/types/src/hooks/useActionHandler.d.ts.map +1 -0
- package/dist/types/src/hooks/useTextEditor.d.ts +17 -0
- package/dist/types/src/hooks/useTextEditor.d.ts.map +1 -0
- 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/styles/markdown.d.ts +2 -2
- package/dist/types/src/styles/markdown.d.ts.map +1 -1
- package/dist/types/src/themes/default.d.ts.map +1 -1
- package/dist/types/src/util.d.ts +2 -0
- package/dist/types/src/util.d.ts.map +1 -1
- package/package.json +45 -36
- package/src/components/TextEditor/MarkdownEditor.stories.tsx +64 -75
- package/src/components/TextEditor/TextEditor.tsx +103 -71
- package/src/components/TextEditor/comments.stories.tsx +46 -44
- package/src/components/Toolbar/Toolbar.stories.tsx +86 -0
- package/src/components/Toolbar/Toolbar.tsx +115 -0
- package/src/extensions/autocomplete.ts +6 -3
- package/src/extensions/automerge/automerge.spec.tsx +76 -0
- package/src/extensions/automerge/automerge.test.ts +13 -0
- package/src/extensions/automerge/automerge.ts +50 -39
- package/src/extensions/automerge/cursor.ts +3 -3
- package/src/extensions/automerge/defs.ts +11 -36
- package/src/extensions/automerge/semaphore.ts +31 -28
- package/src/extensions/automerge/update-automerge.ts +14 -8
- package/src/extensions/automerge/update-codemirror.ts +5 -5
- package/src/extensions/awareness.ts +17 -17
- package/src/extensions/basic.ts +22 -17
- package/src/extensions/comments.ts +247 -179
- package/src/extensions/cursor.ts +7 -3
- package/src/extensions/index.ts +0 -7
- package/src/extensions/markdown/bundle.ts +20 -30
- package/src/extensions/markdown/code.ts +142 -0
- package/src/extensions/markdown/formatting.ts +159 -0
- package/src/extensions/markdown/heading.ts +59 -0
- package/src/extensions/markdown/highlight.ts +9 -15
- package/src/extensions/markdown/hr.ts +71 -0
- package/src/extensions/markdown/index.ts +8 -0
- package/src/extensions/{link.ts → markdown/link.ts} +5 -2
- package/src/extensions/{tasklist.ts → markdown/tasklist.ts} +48 -34
- package/src/extensions/mention.ts +9 -2
- package/src/hooks/awareness-provider.ts +24 -20
- package/src/hooks/defs.ts +42 -0
- package/src/hooks/index.ts +3 -2
- package/src/hooks/useActionHandler.ts +45 -0
- package/src/hooks/useTextEditor.ts +29 -0
- package/src/hooks/useTextModel.ts +49 -55
- package/src/styles/markdown.ts +9 -10
- package/src/themes/default.ts +1 -3
- package/src/util.ts +26 -3
- package/dist/types/src/components/TextEditor/codemirror.stories.d.ts +0 -12
- package/dist/types/src/components/TextEditor/codemirror.stories.d.ts.map +0 -1
- package/dist/types/src/components/TextEditor/yjs.stories.d.ts +0 -12
- package/dist/types/src/components/TextEditor/yjs.stories.d.ts.map +0 -1
- package/dist/types/src/extensions/code.d.ts.map +0 -1
- package/dist/types/src/extensions/hr.d.ts.map +0 -1
- package/dist/types/src/extensions/image.d.ts.map +0 -1
- package/dist/types/src/extensions/link.d.ts.map +0 -1
- package/dist/types/src/extensions/table.d.ts.map +0 -1
- package/dist/types/src/extensions/tasklist.d.ts +0 -7
- package/dist/types/src/extensions/tasklist.d.ts.map +0 -1
- package/dist/types/src/extensions/yjs/cursor.d.ts +0 -4
- package/dist/types/src/extensions/yjs/cursor.d.ts.map +0 -1
- package/dist/types/src/extensions/yjs/index.d.ts +0 -2
- package/dist/types/src/extensions/yjs/index.d.ts.map +0 -1
- package/dist/types/src/extensions/yjs/yjs.d.ts +0 -4
- package/dist/types/src/extensions/yjs/yjs.d.ts.map +0 -1
- package/dist/types/src/extensions/yjs/yjs.test.d.ts +0 -2
- package/dist/types/src/extensions/yjs/yjs.test.d.ts.map +0 -1
- package/dist/types/src/hooks/automerge.d.ts +0 -3
- package/dist/types/src/hooks/automerge.d.ts.map +0 -1
- package/dist/types/src/hooks/yjs.d.ts +0 -27
- package/dist/types/src/hooks/yjs.d.ts.map +0 -1
- package/dist/types/src/testing/index.d.ts +0 -3
- package/dist/types/src/testing/index.d.ts.map +0 -1
- package/dist/types/src/testing/proto/gen/schema.d.ts +0 -15
- package/dist/types/src/testing/proto/gen/schema.d.ts.map +0 -1
- package/dist/types/src/testing/proto/index.d.ts +0 -2
- package/dist/types/src/testing/proto/index.d.ts.map +0 -1
- package/dist/types/src/testing/replicator.d.ts +0 -45
- package/dist/types/src/testing/replicator.d.ts.map +0 -1
- package/src/components/TextEditor/codemirror.stories.ts +0 -115
- package/src/components/TextEditor/yjs.stories.tsx +0 -56
- package/src/extensions/code.ts +0 -104
- package/src/extensions/hr.ts +0 -72
- package/src/extensions/yjs/cursor.ts +0 -22
- package/src/extensions/yjs/index.ts +0 -5
- package/src/extensions/yjs/yjs.test.ts +0 -35
- package/src/extensions/yjs/yjs.ts +0 -16
- package/src/hooks/automerge.ts +0 -52
- package/src/hooks/yjs.ts +0 -156
- package/src/testing/index.ts +0 -6
- package/src/testing/proto/gen/schema.ts +0 -49
- package/src/testing/proto/index.ts +0 -5
- package/src/testing/proto/schema.proto +0 -15
- package/src/testing/replicator.ts +0 -184
- /package/dist/types/src/extensions/{code.d.ts → markdown/code.d.ts} +0 -0
- /package/dist/types/src/extensions/{hr.d.ts → markdown/hr.d.ts} +0 -0
- /package/dist/types/src/extensions/{image.d.ts → markdown/image.d.ts} +0 -0
- /package/dist/types/src/extensions/{link.d.ts → markdown/link.d.ts} +0 -0
- /package/dist/types/src/extensions/{table.d.ts → markdown/table.d.ts} +0 -0
- /package/src/extensions/{image.ts → markdown/image.ts} +0 -0
- /package/src/extensions/{table.ts → markdown/table.ts} +0 -0
|
@@ -2,17 +2,17 @@
|
|
|
2
2
|
// Copyright 2023 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
-
import {
|
|
5
|
+
import { invertedEffects } from '@codemirror/commands';
|
|
6
|
+
import { type Extension, Facet, StateEffect, StateField, type Text, type ChangeDesc } from '@codemirror/state';
|
|
6
7
|
import { hoverTooltip, keymap, type Command, Decoration, EditorView, type Rect } from '@codemirror/view';
|
|
7
8
|
import sortBy from 'lodash.sortby';
|
|
9
|
+
import { useEffect } from 'react';
|
|
8
10
|
|
|
9
11
|
import { debounce } from '@dxos/async';
|
|
10
|
-
import { invariant } from '@dxos/invariant';
|
|
11
12
|
import { nonNullable } from '@dxos/util';
|
|
12
13
|
|
|
13
|
-
import { semaphoreFacet } from './automerge/defs';
|
|
14
14
|
import { Cursor } from './cursor';
|
|
15
|
-
import { type
|
|
15
|
+
import { type Comment, type Range } from '../hooks';
|
|
16
16
|
import { getToken } from '../styles';
|
|
17
17
|
import { callbackWrapper } from '../util';
|
|
18
18
|
|
|
@@ -21,49 +21,52 @@ const styles = EditorView.baseTheme({
|
|
|
21
21
|
'& .cm-comment': {
|
|
22
22
|
backgroundColor: getToken('extend.colors.yellow.50'),
|
|
23
23
|
},
|
|
24
|
-
'& .cm-comment-
|
|
24
|
+
'& .cm-comment-current': {
|
|
25
25
|
backgroundColor: getToken('extend.colors.yellow.100'),
|
|
26
26
|
},
|
|
27
27
|
});
|
|
28
28
|
|
|
29
|
-
const
|
|
30
|
-
|
|
31
|
-
|
|
29
|
+
const commentMark = Decoration.mark({ class: 'cm-comment' });
|
|
30
|
+
const commentCurrentMark = Decoration.mark({ class: 'cm-comment-current' });
|
|
31
|
+
|
|
32
|
+
type CommentState = {
|
|
33
|
+
comment: Comment;
|
|
34
|
+
range: Range;
|
|
35
|
+
location?: Rect | null;
|
|
32
36
|
};
|
|
33
37
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
active?: string;
|
|
38
|
+
type SelectionState = {
|
|
39
|
+
current?: string;
|
|
37
40
|
closest?: string;
|
|
38
41
|
};
|
|
39
42
|
|
|
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[];
|
|
43
|
+
export type CommentsState = {
|
|
44
|
+
comments: CommentState[];
|
|
45
|
+
selection: SelectionState;
|
|
49
46
|
};
|
|
50
47
|
|
|
51
|
-
export const setFocus = (view: EditorView,
|
|
52
|
-
const
|
|
53
|
-
if (!
|
|
48
|
+
export const setFocus = (view: EditorView, id: string, center = true) => {
|
|
49
|
+
const comment = view.state.field(commentsState).comments.find((range) => range.comment.id === id);
|
|
50
|
+
if (!comment?.comment.cursor) {
|
|
54
51
|
return;
|
|
55
52
|
}
|
|
56
53
|
|
|
57
|
-
view.
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
54
|
+
const range = Cursor.getRangeFromCursor(view.state, comment.comment.cursor);
|
|
55
|
+
if (range) {
|
|
56
|
+
view.dispatch({
|
|
57
|
+
selection: { anchor: range.from },
|
|
58
|
+
effects: [
|
|
59
|
+
//
|
|
60
|
+
EditorView.scrollIntoView(range.from, center ? { y: 'center' } : undefined),
|
|
61
|
+
setSelection.of({ current: id }),
|
|
62
|
+
],
|
|
63
|
+
});
|
|
64
|
+
}
|
|
62
65
|
};
|
|
63
66
|
|
|
64
|
-
export const
|
|
67
|
+
export const setComments = StateEffect.define<Comment[]>();
|
|
65
68
|
|
|
66
|
-
export const setSelection = StateEffect.define<
|
|
69
|
+
export const setSelection = StateEffect.define<SelectionState>();
|
|
67
70
|
|
|
68
71
|
const setCommentState = StateEffect.define<CommentsState>();
|
|
69
72
|
|
|
@@ -71,29 +74,30 @@ const setCommentState = StateEffect.define<CommentsState>();
|
|
|
71
74
|
* State field (reducer) that tracks comment ranges.
|
|
72
75
|
* The ranges are tracked as Automerge cursors from which the absolute indexed ranges can be computed.
|
|
73
76
|
*/
|
|
74
|
-
const
|
|
75
|
-
create: () => ({
|
|
77
|
+
const commentsState = StateField.define<CommentsState>({
|
|
78
|
+
create: () => ({ comments: [], selection: {} }),
|
|
76
79
|
update: (value, tr) => {
|
|
77
|
-
const cursorConverter = tr.state.facet(Cursor.converter);
|
|
78
|
-
|
|
79
80
|
for (const effect of tr.effects) {
|
|
80
81
|
// Update selection.
|
|
81
82
|
if (effect.is(setSelection)) {
|
|
82
|
-
return { ...value,
|
|
83
|
+
return { ...value, selection: effect.value };
|
|
83
84
|
}
|
|
84
85
|
|
|
85
86
|
// Update range from store.
|
|
86
|
-
if (effect.is(
|
|
87
|
-
const
|
|
88
|
-
const ranges: ExtendedCommentRange[] = comments
|
|
87
|
+
if (effect.is(setComments)) {
|
|
88
|
+
const comments: CommentState[] = effect.value
|
|
89
89
|
.map((comment) => {
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
90
|
+
// Skip cut/deleted comments.
|
|
91
|
+
if (!comment.cursor) {
|
|
92
|
+
return undefined;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const range = Cursor.getRangeFromCursor(tr.state, comment.cursor);
|
|
96
|
+
return range && { comment, range };
|
|
93
97
|
})
|
|
94
98
|
.filter(nonNullable);
|
|
95
99
|
|
|
96
|
-
return { ...value,
|
|
100
|
+
return { ...value, comments };
|
|
97
101
|
}
|
|
98
102
|
|
|
99
103
|
// Update entire state.
|
|
@@ -109,24 +113,27 @@ const commentsStateField = StateField.define<CommentsState>({
|
|
|
109
113
|
/**
|
|
110
114
|
* Decorate ranges.
|
|
111
115
|
*/
|
|
112
|
-
const
|
|
113
|
-
const {
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
116
|
+
const commentsDecorations = EditorView.decorations.compute([commentsState], (state) => {
|
|
117
|
+
const {
|
|
118
|
+
selection: { current },
|
|
119
|
+
comments,
|
|
120
|
+
} = state.field(commentsState);
|
|
121
|
+
|
|
122
|
+
const decorations = sortBy(comments ?? [], (range) => range.range.from)
|
|
123
|
+
?.flatMap((comment) => {
|
|
124
|
+
const range = comment.range;
|
|
125
|
+
if (!range || range.from === range.to) {
|
|
126
|
+
console.warn('Invalid range:', range);
|
|
127
|
+
return undefined;
|
|
128
|
+
}
|
|
121
129
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
.filter(nonNullable) ?? [];
|
|
130
|
+
if (comment.comment.id === current) {
|
|
131
|
+
return commentCurrentMark.range(range.from, range.to);
|
|
132
|
+
} else {
|
|
133
|
+
return commentMark.range(range.from, range.to);
|
|
134
|
+
}
|
|
135
|
+
})
|
|
136
|
+
.filter(nonNullable);
|
|
130
137
|
|
|
131
138
|
return Decoration.set(decorations);
|
|
132
139
|
});
|
|
@@ -158,18 +165,18 @@ export type CommentsOptions = {
|
|
|
158
165
|
onHover?: (el: Element, shortcut: string) => void;
|
|
159
166
|
};
|
|
160
167
|
|
|
161
|
-
|
|
168
|
+
type TrackedComment = { id: string; from: number; to: number };
|
|
169
|
+
|
|
162
170
|
const trackPastedComments = (onUpdate: NonNullable<CommentsOptions['onUpdate']>) => {
|
|
163
|
-
// Cut/deleted comments.
|
|
164
171
|
// Tracks indexed selections within text.
|
|
165
|
-
let tracked: { text: Text; comments:
|
|
172
|
+
let tracked: { text: Text; comments: TrackedComment[] } | null = null;
|
|
166
173
|
|
|
167
174
|
// Track cut or copy (enables cut-and-paste and copy-delete-paste to restore comment selection).
|
|
168
175
|
const handleTrack = (event: Event, view: EditorView) => {
|
|
169
|
-
const comments = view.state.field(
|
|
176
|
+
const comments = view.state.field(commentsState);
|
|
170
177
|
const { main } = view.state.selection;
|
|
171
|
-
const selectedRanges = comments.
|
|
172
|
-
(range) => range.from >= main.from && range.to <= main.to && range.from < range.to,
|
|
178
|
+
const selectedRanges = comments.comments.filter(
|
|
179
|
+
({ range }) => range.from >= main.from && range.to <= main.to && range.from < range.to,
|
|
173
180
|
);
|
|
174
181
|
|
|
175
182
|
if (!selectedRanges.length) {
|
|
@@ -177,15 +184,13 @@ const trackPastedComments = (onUpdate: NonNullable<CommentsOptions['onUpdate']>)
|
|
|
177
184
|
} else {
|
|
178
185
|
tracked = {
|
|
179
186
|
text: view.state.doc.slice(main.from, main.to),
|
|
180
|
-
comments: selectedRanges.map((range) => ({
|
|
181
|
-
id:
|
|
187
|
+
comments: selectedRanges.map(({ comment, range }) => ({
|
|
188
|
+
id: comment.id,
|
|
182
189
|
from: range.from - main.from,
|
|
183
190
|
to: range.to - main.from,
|
|
184
191
|
})),
|
|
185
192
|
};
|
|
186
193
|
}
|
|
187
|
-
|
|
188
|
-
// console.log('track', JSON.stringify({ tracked }, undefined, 2));
|
|
189
194
|
};
|
|
190
195
|
|
|
191
196
|
return [
|
|
@@ -194,16 +199,48 @@ const trackPastedComments = (onUpdate: NonNullable<CommentsOptions['onUpdate']>)
|
|
|
194
199
|
copy: handleTrack,
|
|
195
200
|
}),
|
|
196
201
|
|
|
197
|
-
//
|
|
198
|
-
|
|
202
|
+
// Track deleted comments.
|
|
203
|
+
invertedEffects.of((tr) => {
|
|
204
|
+
const { comments } = tr.startState.field(commentsState);
|
|
205
|
+
const effects: StateEffect<any>[] = [];
|
|
206
|
+
tr.changes.iterChangedRanges((fromA, toA) => {
|
|
207
|
+
for (const {
|
|
208
|
+
comment: { id },
|
|
209
|
+
range: { from, to },
|
|
210
|
+
} of comments) {
|
|
211
|
+
if (from < to && from >= fromA && to <= toA) {
|
|
212
|
+
effects.push(restoreCommentEffect.of({ id, from, to }));
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
return effects;
|
|
218
|
+
}),
|
|
219
|
+
|
|
220
|
+
// Handle paste or the undo of comment deletion.
|
|
221
|
+
EditorView.updateListener.of((update) => {
|
|
222
|
+
const restore: TrackedComment[] = [];
|
|
223
|
+
|
|
224
|
+
for (let i = 0; i < update.transactions.length; i++) {
|
|
225
|
+
const tr = update.transactions[i];
|
|
226
|
+
for (let j = 0; j < restore.length; j++) {
|
|
227
|
+
restore[j] = mapTrackedComment(restore[j], tr.changes);
|
|
228
|
+
}
|
|
229
|
+
for (const effect of tr.effects) {
|
|
230
|
+
if (effect.is(restoreCommentEffect)) {
|
|
231
|
+
restore.push(effect.value);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
199
236
|
if (tracked) {
|
|
200
|
-
const paste = transactions.find((tr) => tr.isUserEvent('input.paste'));
|
|
237
|
+
const paste = update.transactions.find((tr) => tr.isUserEvent('input.paste'));
|
|
201
238
|
if (paste) {
|
|
202
239
|
let found = -1;
|
|
203
240
|
paste.changes.iterChanges((fromA, toA, fromB, toB, text) => {
|
|
204
241
|
if (text.eq(tracked!.text)) {
|
|
205
|
-
for (let i = transactions.indexOf(paste!) + 1; i < transactions.length; i++) {
|
|
206
|
-
fromB = transactions[i].changes.mapPos(fromB);
|
|
242
|
+
for (let i = update.transactions.indexOf(paste!) + 1; i < update.transactions.length; i++) {
|
|
243
|
+
fromB = update.transactions[i].changes.mapPos(fromB);
|
|
207
244
|
}
|
|
208
245
|
|
|
209
246
|
found = fromB;
|
|
@@ -211,34 +248,79 @@ const trackPastedComments = (onUpdate: NonNullable<CommentsOptions['onUpdate']>)
|
|
|
211
248
|
});
|
|
212
249
|
|
|
213
250
|
if (found > -1) {
|
|
214
|
-
const
|
|
215
|
-
|
|
216
|
-
// Sync before recomputing cursor/range.
|
|
217
|
-
// TODO(burdon): Generalize sync facet to decouple from automerge?
|
|
218
|
-
view.state.facet(semaphoreFacet).reconcile(view);
|
|
219
|
-
|
|
220
|
-
const active = state.field(commentsStateField).ranges;
|
|
221
|
-
for (const moved of comments) {
|
|
222
|
-
if (active.some((range) => range.id === moved.id && range.from === range.to)) {
|
|
223
|
-
const range = {
|
|
224
|
-
from: found + moved.from,
|
|
225
|
-
to: found + moved.to,
|
|
226
|
-
};
|
|
227
|
-
|
|
228
|
-
const cursor = Cursor.getCursorFromRange(state.facet(Cursor.converter), range);
|
|
229
|
-
// console.log('paste', JSON.stringify({ moved, range, cursor }, undefined, 2));
|
|
230
|
-
onUpdate(moved.id, cursor);
|
|
231
|
-
}
|
|
251
|
+
for (const moved of tracked.comments) {
|
|
252
|
+
restore.push({ id: moved.id, from: found + moved.from, to: found + moved.to });
|
|
232
253
|
}
|
|
233
254
|
}
|
|
234
255
|
|
|
235
256
|
tracked = null;
|
|
236
257
|
}
|
|
237
258
|
}
|
|
259
|
+
|
|
260
|
+
for (const comment of restore) {
|
|
261
|
+
const { comments } = update.startState.field(commentsState);
|
|
262
|
+
const exists = comments.some((c) => c.comment.id === comment.id && c.range.from < c.range.to);
|
|
263
|
+
if (!exists) {
|
|
264
|
+
const cursor = Cursor.getCursorFromRange(update.state, comment);
|
|
265
|
+
onUpdate(comment.id, cursor);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
238
268
|
}),
|
|
239
269
|
];
|
|
240
270
|
};
|
|
241
271
|
|
|
272
|
+
const mapTrackedComment = (comment: TrackedComment, changes: ChangeDesc) => ({
|
|
273
|
+
id: comment.id,
|
|
274
|
+
from: changes.mapPos(comment.from, 1),
|
|
275
|
+
to: changes.mapPos(comment.to, 1),
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
// These are attached to undone/redone transactions in the editor for the purpose of restoring comments
|
|
279
|
+
// that were deleted by the original changes.
|
|
280
|
+
const restoreCommentEffect = StateEffect.define<TrackedComment>({ map: mapTrackedComment });
|
|
281
|
+
|
|
282
|
+
const optionsFacet = Facet.define<CommentsOptions, CommentsOptions>({
|
|
283
|
+
combine: (providers) => providers[0],
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* Create comment thread action.
|
|
288
|
+
*/
|
|
289
|
+
export const createComment: Command = (view) => {
|
|
290
|
+
const options = view.state.facet(optionsFacet);
|
|
291
|
+
const { from, to } = view.state.selection.main;
|
|
292
|
+
if (from === to) {
|
|
293
|
+
return false;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// Don't allow selection at end of document.
|
|
297
|
+
if (to === view.state.doc.length) {
|
|
298
|
+
view.dispatch({
|
|
299
|
+
changes: {
|
|
300
|
+
from: to,
|
|
301
|
+
insert: '\n',
|
|
302
|
+
},
|
|
303
|
+
});
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
const cursor = Cursor.getCursorFromRange(view.state, { from, to });
|
|
307
|
+
if (cursor) {
|
|
308
|
+
// Create thread via callback.
|
|
309
|
+
const id = options.onCreate?.(cursor, view.coordsAtPos(from));
|
|
310
|
+
if (id) {
|
|
311
|
+
// Update range.
|
|
312
|
+
view.dispatch({
|
|
313
|
+
effects: setSelection.of({ current: id }),
|
|
314
|
+
selection: { anchor: to },
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
return true;
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
return false;
|
|
322
|
+
};
|
|
323
|
+
|
|
242
324
|
/**
|
|
243
325
|
* Comment threads.
|
|
244
326
|
* 1). Updates the EditorModel to store relative selections for a set of comments threads.
|
|
@@ -255,39 +337,10 @@ export const comments = (options: CommentsOptions = {}): Extension => {
|
|
|
255
337
|
|
|
256
338
|
const handleSelect = debounce((state: CommentsState) => options.onSelect?.(state), 200);
|
|
257
339
|
|
|
258
|
-
/**
|
|
259
|
-
* Create comment thread action.
|
|
260
|
-
*/
|
|
261
|
-
const createCommentThread: Command = (view) => {
|
|
262
|
-
const cursorConverter = view.state.facet(Cursor.converter);
|
|
263
|
-
|
|
264
|
-
invariant(options.onCreate);
|
|
265
|
-
const { from, to } = view.state.selection.main;
|
|
266
|
-
if (from === to) {
|
|
267
|
-
return false;
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
const cursor = Cursor.getCursorFromRange(cursorConverter, { from, to });
|
|
271
|
-
if (cursor) {
|
|
272
|
-
// Create thread via callback.
|
|
273
|
-
const id = callbackWrapper(options.onCreate)(cursor, view.coordsAtPos(from));
|
|
274
|
-
if (id) {
|
|
275
|
-
// Update range.
|
|
276
|
-
view.dispatch({
|
|
277
|
-
effects: setSelection.of({ active: id }),
|
|
278
|
-
selection: { anchor: from },
|
|
279
|
-
});
|
|
280
|
-
|
|
281
|
-
return true;
|
|
282
|
-
}
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
return false;
|
|
286
|
-
};
|
|
287
|
-
|
|
288
340
|
return [
|
|
289
|
-
|
|
290
|
-
|
|
341
|
+
optionsFacet.of(options),
|
|
342
|
+
commentsState,
|
|
343
|
+
commentsDecorations,
|
|
291
344
|
styles,
|
|
292
345
|
|
|
293
346
|
//
|
|
@@ -297,7 +350,7 @@ export const comments = (options: CommentsOptions = {}): Extension => {
|
|
|
297
350
|
? keymap.of([
|
|
298
351
|
{
|
|
299
352
|
key: shortcut,
|
|
300
|
-
run: callbackWrapper(
|
|
353
|
+
run: callbackWrapper(createComment),
|
|
301
354
|
},
|
|
302
355
|
])
|
|
303
356
|
: [],
|
|
@@ -334,75 +387,90 @@ export const comments = (options: CommentsOptions = {}): Extension => {
|
|
|
334
387
|
: [],
|
|
335
388
|
|
|
336
389
|
//
|
|
337
|
-
//
|
|
338
|
-
// TODO(burdon): Is there a better (finer grained) way to do this?
|
|
390
|
+
// Track deleted ranges and update ranges for decorations.
|
|
339
391
|
//
|
|
340
392
|
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
|
-
}
|
|
393
|
+
let mod = false;
|
|
394
|
+
const { comments, ...value } = state.field(commentsState);
|
|
395
|
+
changes.iterChanges((from, to, from2, to2) => {
|
|
396
|
+
comments.forEach(({ comment, range }) => {
|
|
397
|
+
// Test if range deleted.
|
|
398
|
+
if (from2 === to2) {
|
|
399
|
+
const newRange = Cursor.getRangeFromCursor(view.state, comment.cursor!);
|
|
400
|
+
if (!newRange || newRange.to - newRange.from === 0) {
|
|
401
|
+
options.onDelete?.(comment.id);
|
|
357
402
|
}
|
|
403
|
+
}
|
|
358
404
|
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
}
|
|
405
|
+
// Update range.
|
|
406
|
+
if (from <= range.to) {
|
|
407
|
+
const newRange = Cursor.getRangeFromCursor(view.state, comment.cursor!);
|
|
408
|
+
Object.assign(range, newRange);
|
|
409
|
+
mod = true;
|
|
410
|
+
}
|
|
365
411
|
});
|
|
412
|
+
});
|
|
366
413
|
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
}
|
|
414
|
+
if (mod) {
|
|
415
|
+
view.dispatch({ effects: setCommentState.of({ comments, ...value }) });
|
|
370
416
|
}
|
|
417
|
+
}),
|
|
371
418
|
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
419
|
+
//
|
|
420
|
+
// Track selection/proximity.
|
|
421
|
+
//
|
|
422
|
+
EditorView.updateListener.of(({ view, state }) => {
|
|
423
|
+
let min = Infinity;
|
|
424
|
+
const {
|
|
425
|
+
selection: { current, closest },
|
|
426
|
+
comments,
|
|
427
|
+
} = state.field(commentsState);
|
|
428
|
+
|
|
429
|
+
const { head } = state.selection.main;
|
|
430
|
+
const selection: SelectionState = {};
|
|
431
|
+
comments.forEach(({ comment, range }) => {
|
|
432
|
+
if (head >= range.from && head <= range.to) {
|
|
433
|
+
selection.current = comment.id;
|
|
434
|
+
selection.closest = undefined;
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
if (!selection.current) {
|
|
438
|
+
const d = Math.min(Math.abs(head - range.from), Math.abs(head - range.to));
|
|
388
439
|
if (d < min) {
|
|
389
|
-
|
|
440
|
+
selection.closest = comment.id;
|
|
390
441
|
min = d;
|
|
391
442
|
}
|
|
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
443
|
}
|
|
444
|
+
});
|
|
445
|
+
|
|
446
|
+
if (selection.current !== current || selection.closest !== closest) {
|
|
447
|
+
view.dispatch({ effects: setSelection.of(selection) });
|
|
448
|
+
|
|
449
|
+
// Update callback.
|
|
450
|
+
handleSelect({
|
|
451
|
+
selection,
|
|
452
|
+
comments: comments.map(({ comment, range }) => ({
|
|
453
|
+
comment,
|
|
454
|
+
range,
|
|
455
|
+
location: view.coordsAtPos(range.from),
|
|
456
|
+
})),
|
|
457
|
+
});
|
|
403
458
|
}
|
|
404
459
|
}),
|
|
405
460
|
|
|
406
461
|
options.onUpdate ? trackPastedComments(options.onUpdate) : [],
|
|
407
462
|
];
|
|
408
463
|
};
|
|
464
|
+
|
|
465
|
+
/**
|
|
466
|
+
* Update comments state field.
|
|
467
|
+
*/
|
|
468
|
+
export const useComments = (view: EditorView | null, comments: Comment[] = []) => {
|
|
469
|
+
useEffect(() => {
|
|
470
|
+
if (view) {
|
|
471
|
+
view.dispatch({
|
|
472
|
+
effects: setComments.of(comments),
|
|
473
|
+
});
|
|
474
|
+
}
|
|
475
|
+
}, [view, comments]);
|
|
476
|
+
};
|
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
|
@@ -7,17 +7,10 @@ export * from './automerge';
|
|
|
7
7
|
export * from './awareness';
|
|
8
8
|
export * from './basic';
|
|
9
9
|
export * from './blast';
|
|
10
|
-
export * from './code';
|
|
11
10
|
export * from './comments';
|
|
12
11
|
export * from './cursor';
|
|
13
|
-
export * from './hr';
|
|
14
|
-
export * from './image';
|
|
15
|
-
export * from './link';
|
|
16
12
|
export * from './listener';
|
|
17
13
|
export * from './markdown';
|
|
18
14
|
export * from './mention';
|
|
19
15
|
export * from './outliner';
|
|
20
|
-
export * from './table';
|
|
21
|
-
export * from './tasklist';
|
|
22
16
|
export * from './typewriter';
|
|
23
|
-
export * from './yjs';
|