@dxos/react-ui-editor 0.3.11-main.42059c4 → 0.3.11-main.463e5cd
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 +1610 -1059
- 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 +13 -15
- 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/automerge.stories.d.ts.map +1 -1
- package/dist/types/src/components/TextEditor/{yjs.stories.d.ts → hooks.stories.d.ts} +13 -2
- package/dist/types/src/components/TextEditor/hooks.stories.d.ts.map +1 -0
- package/dist/types/src/components/Toolbar/Toolbar.d.ts +25 -0
- package/dist/types/src/components/Toolbar/Toolbar.d.ts.map +1 -0
- package/dist/types/src/components/Toolbar/Toolbar.stories.d.ts +23 -0
- package/dist/types/src/components/Toolbar/Toolbar.stories.d.ts.map +1 -0
- package/dist/types/src/components/Toolbar/index.d.ts +2 -0
- package/dist/types/src/components/Toolbar/index.d.ts.map +1 -0
- package/dist/types/src/components/index.d.ts +1 -0
- package/dist/types/src/components/index.d.ts.map +1 -1
- 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 +1 -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 +13 -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/extensions/outliner.d.ts +4 -0
- package/dist/types/src/extensions/outliner.d.ts.map +1 -0
- 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 +4 -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/useEditorView.d.ts +17 -0
- package/dist/types/src/hooks/useEditorView.d.ts.map +1 -0
- package/dist/types/src/hooks/useTextEditor.d.ts +39 -0
- package/dist/types/src/hooks/useTextEditor.d.ts.map +1 -0
- package/dist/types/src/hooks/useTextModel.d.ts +14 -27
- package/dist/types/src/hooks/useTextModel.d.ts.map +1 -1
- package/dist/types/src/index.d.ts +1 -1
- package/dist/types/src/index.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 +46 -36
- package/src/components/TextEditor/MarkdownEditor.stories.tsx +73 -82
- package/src/components/TextEditor/TextEditor.tsx +107 -71
- package/src/components/TextEditor/automerge.stories.tsx +1 -2
- package/src/components/TextEditor/hooks.stories.tsx +106 -0
- package/src/components/Toolbar/Toolbar.stories.tsx +90 -0
- package/src/components/Toolbar/Toolbar.tsx +166 -0
- package/src/components/Toolbar/index.ts +5 -0
- package/src/components/index.ts +1 -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 -41
- 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 +22 -18
- package/src/extensions/basic.ts +17 -10
- package/src/extensions/blast.ts +4 -1
- package/src/extensions/comments.ts +253 -179
- package/src/extensions/cursor.ts +7 -3
- package/src/extensions/index.ts +1 -7
- package/src/extensions/markdown/bundle.ts +23 -28
- package/src/extensions/markdown/code.ts +150 -0
- package/src/extensions/markdown/formatting.ts +188 -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} +16 -7
- package/src/extensions/{tasklist.ts → markdown/tasklist.ts} +48 -33
- package/src/extensions/mention.ts +9 -2
- package/src/extensions/outliner.ts +12 -0
- package/src/hooks/awareness-provider.ts +24 -20
- package/src/hooks/defs.ts +42 -0
- package/src/hooks/index.ts +4 -2
- package/src/hooks/useActionHandler.ts +62 -0
- package/src/hooks/useEditorView.ts +29 -0
- package/src/hooks/useTextEditor.ts +153 -0
- package/src/hooks/useTextModel.ts +66 -56
- package/src/index.ts +1 -1
- package/src/styles/markdown.ts +9 -10
- package/src/themes/default.ts +9 -6
- 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/comments.stories.d.ts +0 -26
- package/dist/types/src/components/TextEditor/comments.stories.d.ts.map +0 -1
- 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/comments.stories.tsx +0 -355
- package/src/components/TextEditor/yjs.stories.tsx +0 -56
- package/src/extensions/code.ts +0 -106
- package/src/extensions/hr.ts +0 -73
- 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
|
|
52
|
-
const
|
|
53
|
-
if (!
|
|
48
|
+
export const focusComment = (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,8 +184,8 @@ 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
|
})),
|
|
@@ -192,16 +199,48 @@ const trackPastedComments = (onUpdate: NonNullable<CommentsOptions['onUpdate']>)
|
|
|
192
199
|
copy: handleTrack,
|
|
193
200
|
}),
|
|
194
201
|
|
|
195
|
-
//
|
|
196
|
-
|
|
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
|
+
|
|
197
236
|
if (tracked) {
|
|
198
|
-
const paste = transactions.find((tr) => tr.isUserEvent('input.paste'));
|
|
237
|
+
const paste = update.transactions.find((tr) => tr.isUserEvent('input.paste'));
|
|
199
238
|
if (paste) {
|
|
200
239
|
let found = -1;
|
|
201
240
|
paste.changes.iterChanges((fromA, toA, fromB, toB, text) => {
|
|
202
241
|
if (text.eq(tracked!.text)) {
|
|
203
|
-
for (let i = transactions.indexOf(paste!) + 1; i < transactions.length; i++) {
|
|
204
|
-
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);
|
|
205
244
|
}
|
|
206
245
|
|
|
207
246
|
found = fromB;
|
|
@@ -209,34 +248,79 @@ const trackPastedComments = (onUpdate: NonNullable<CommentsOptions['onUpdate']>)
|
|
|
209
248
|
});
|
|
210
249
|
|
|
211
250
|
if (found > -1) {
|
|
212
|
-
const
|
|
213
|
-
|
|
214
|
-
// Sync before recomputing cursor/range.
|
|
215
|
-
// TODO(burdon): Decouple from automerge?
|
|
216
|
-
view.state.facet(semaphoreFacet).reconcile(view);
|
|
217
|
-
|
|
218
|
-
const active = state.field(commentsStateField).ranges;
|
|
219
|
-
for (const moved of comments) {
|
|
220
|
-
if (active.some((range) => range.id === moved.id && range.from === range.to)) {
|
|
221
|
-
const range = {
|
|
222
|
-
from: found + moved.from,
|
|
223
|
-
to: found + moved.to,
|
|
224
|
-
};
|
|
225
|
-
|
|
226
|
-
const cursor = Cursor.getCursorFromRange(state.facet(Cursor.converter), range);
|
|
227
|
-
// console.log('paste', JSON.stringify({ moved, range, cursor }, undefined, 2));
|
|
228
|
-
onUpdate(moved.id, cursor);
|
|
229
|
-
}
|
|
251
|
+
for (const moved of tracked.comments) {
|
|
252
|
+
restore.push({ id: moved.id, from: found + moved.from, to: found + moved.to });
|
|
230
253
|
}
|
|
231
254
|
}
|
|
232
255
|
|
|
233
256
|
tracked = null;
|
|
234
257
|
}
|
|
235
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
|
+
}
|
|
236
268
|
}),
|
|
237
269
|
];
|
|
238
270
|
};
|
|
239
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
|
+
|
|
240
324
|
/**
|
|
241
325
|
* Comment threads.
|
|
242
326
|
* 1). Updates the EditorModel to store relative selections for a set of comments threads.
|
|
@@ -253,39 +337,10 @@ export const comments = (options: CommentsOptions = {}): Extension => {
|
|
|
253
337
|
|
|
254
338
|
const handleSelect = debounce((state: CommentsState) => options.onSelect?.(state), 200);
|
|
255
339
|
|
|
256
|
-
/**
|
|
257
|
-
* Create comment thread action.
|
|
258
|
-
*/
|
|
259
|
-
const createCommentThread: Command = (view) => {
|
|
260
|
-
const cursorConverter = view.state.facet(Cursor.converter);
|
|
261
|
-
|
|
262
|
-
invariant(options.onCreate);
|
|
263
|
-
const { from, to } = view.state.selection.main;
|
|
264
|
-
if (from === to) {
|
|
265
|
-
return false;
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
const cursor = Cursor.getCursorFromRange(cursorConverter, { from, to });
|
|
269
|
-
if (cursor) {
|
|
270
|
-
// Create thread via callback.
|
|
271
|
-
const id = callbackWrapper(options.onCreate)(cursor, view.coordsAtPos(from));
|
|
272
|
-
if (id) {
|
|
273
|
-
// Update range.
|
|
274
|
-
view.dispatch({
|
|
275
|
-
effects: setSelection.of({ active: id }),
|
|
276
|
-
selection: { anchor: from },
|
|
277
|
-
});
|
|
278
|
-
|
|
279
|
-
return true;
|
|
280
|
-
}
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
return false;
|
|
284
|
-
};
|
|
285
|
-
|
|
286
340
|
return [
|
|
287
|
-
|
|
288
|
-
|
|
341
|
+
optionsFacet.of(options),
|
|
342
|
+
commentsState,
|
|
343
|
+
commentsDecorations,
|
|
289
344
|
styles,
|
|
290
345
|
|
|
291
346
|
//
|
|
@@ -295,7 +350,7 @@ export const comments = (options: CommentsOptions = {}): Extension => {
|
|
|
295
350
|
? keymap.of([
|
|
296
351
|
{
|
|
297
352
|
key: shortcut,
|
|
298
|
-
run: callbackWrapper(
|
|
353
|
+
run: callbackWrapper(createComment),
|
|
299
354
|
},
|
|
300
355
|
])
|
|
301
356
|
: [],
|
|
@@ -315,7 +370,7 @@ export const comments = (options: CommentsOptions = {}): Extension => {
|
|
|
315
370
|
above: true,
|
|
316
371
|
create: () => {
|
|
317
372
|
const el = document.createElement('div');
|
|
318
|
-
options.onHover
|
|
373
|
+
options.onHover!(el, shortcut);
|
|
319
374
|
return { dom: el, offset: { x: 0, y: 8 } };
|
|
320
375
|
},
|
|
321
376
|
};
|
|
@@ -323,80 +378,99 @@ export const comments = (options: CommentsOptions = {}): Extension => {
|
|
|
323
378
|
|
|
324
379
|
return null;
|
|
325
380
|
},
|
|
326
|
-
{
|
|
381
|
+
{
|
|
382
|
+
// TODO(burdon): Hide on change triggered immediately?
|
|
383
|
+
// hideOnChange: true,
|
|
384
|
+
hoverTime: 1_000,
|
|
385
|
+
},
|
|
327
386
|
)
|
|
328
387
|
: [],
|
|
329
388
|
|
|
330
389
|
//
|
|
331
|
-
//
|
|
332
|
-
// TODO(burdon): Is there a better (finer grained) way to do this?
|
|
390
|
+
// Track deleted ranges and update ranges for decorations.
|
|
333
391
|
//
|
|
334
392
|
EditorView.updateListener.of(({ view, state, changes }) => {
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
// TODO(burdon): Skip deleted (tracked) ranges.
|
|
345
|
-
ranges.forEach((range) => {
|
|
346
|
-
if (from2 === to2) {
|
|
347
|
-
const newRange = Cursor.getRangeFromCursor(cursorConverter, range.cursor);
|
|
348
|
-
if (!newRange || newRange.to - newRange.from === 0) {
|
|
349
|
-
options.onDelete?.(range.id);
|
|
350
|
-
}
|
|
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);
|
|
351
402
|
}
|
|
403
|
+
}
|
|
352
404
|
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
}
|
|
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
|
+
}
|
|
359
411
|
});
|
|
412
|
+
});
|
|
360
413
|
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
}
|
|
414
|
+
if (mod) {
|
|
415
|
+
view.dispatch({ effects: setCommentState.of({ comments, ...value }) });
|
|
364
416
|
}
|
|
417
|
+
}),
|
|
365
418
|
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
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));
|
|
382
439
|
if (d < min) {
|
|
383
|
-
|
|
440
|
+
selection.closest = comment.id;
|
|
384
441
|
min = d;
|
|
385
442
|
}
|
|
386
|
-
});
|
|
387
|
-
|
|
388
|
-
if (selected.active !== active || selected.closest !== closest) {
|
|
389
|
-
view.dispatch({ effects: setSelection.of(selected) });
|
|
390
|
-
|
|
391
|
-
// Update callback.
|
|
392
|
-
handleSelect({
|
|
393
|
-
...selected,
|
|
394
|
-
ranges: ranges.map((range) => ({ ...range, location: view.coordsAtPos(range.from) })),
|
|
395
|
-
});
|
|
396
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
|
+
});
|
|
397
458
|
}
|
|
398
459
|
}),
|
|
399
460
|
|
|
400
461
|
options.onUpdate ? trackPastedComments(options.onUpdate) : [],
|
|
401
462
|
];
|
|
402
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,16 +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
|
-
export * from './
|
|
20
|
-
export * from './tasklist';
|
|
15
|
+
export * from './outliner';
|
|
21
16
|
export * from './typewriter';
|
|
22
|
-
export * from './yjs';
|