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