@dxos/react-ui-editor 0.3.11-main.5a4849d → 0.3.11-main.5cbcf4e
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 +2506 -1566
- 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 +16 -14
- package/dist/types/src/components/TextEditor/MarkdownEditor.stories.d.ts.map +1 -1
- package/dist/types/src/components/TextEditor/TextEditor.d.ts +20 -17
- package/dist/types/src/components/TextEditor/TextEditor.d.ts.map +1 -1
- package/dist/types/src/components/TextEditor/TextEditor.stories.d.ts +6 -6
- package/dist/types/src/components/TextEditor/TextEditor.stories.d.ts.map +1 -1
- package/dist/types/src/{hooks/automerge → components/TextEditor}/automerge.stories.d.ts +6 -1
- package/dist/types/src/components/TextEditor/automerge.stories.d.ts.map +1 -0
- package/dist/types/src/components/TextEditor/hooks.stories.d.ts +23 -0
- 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 +9 -0
- package/dist/types/src/extensions/automerge/automerge.d.ts.map +1 -0
- 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/cursor.d.ts +5 -0
- package/dist/types/src/extensions/automerge/cursor.d.ts.map +1 -0
- package/dist/types/src/extensions/automerge/defs.d.ts +24 -0
- package/dist/types/src/extensions/automerge/defs.d.ts.map +1 -0
- package/dist/types/src/extensions/automerge/index.d.ts +3 -0
- package/dist/types/src/extensions/automerge/index.d.ts.map +1 -0
- package/dist/types/src/extensions/automerge/semaphore.d.ts +15 -0
- package/dist/types/src/extensions/automerge/semaphore.d.ts.map +1 -0
- package/dist/types/src/extensions/automerge/update-automerge.d.ts +5 -0
- package/dist/types/src/extensions/automerge/update-automerge.d.ts.map +1 -0
- package/dist/types/src/extensions/automerge/update-codemirror.d.ts.map +1 -0
- package/dist/types/src/extensions/awareness.d.ts +46 -0
- package/dist/types/src/extensions/awareness.d.ts.map +1 -0
- package/dist/types/src/extensions/basic.d.ts +4 -5
- package/dist/types/src/extensions/basic.d.ts.map +1 -1
- package/dist/types/src/extensions/comments.d.ts +41 -20
- package/dist/types/src/extensions/comments.d.ts.map +1 -1
- package/dist/types/src/extensions/cursor.d.ts +25 -0
- package/dist/types/src/extensions/cursor.d.ts.map +1 -0
- package/dist/types/src/extensions/index.d.ts +4 -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.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 +31 -0
- package/dist/types/src/hooks/awareness-provider.d.ts.map +1 -0
- 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 +5 -1
- 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 -31
- package/dist/types/src/hooks/useTextModel.d.ts.map +1 -1
- package/dist/types/src/index.d.ts +3 -0
- package/dist/types/src/index.d.ts.map +1 -1
- package/dist/types/src/styles/cursors.d.ts.map +1 -0
- package/dist/types/src/styles/index.d.ts +1 -0
- package/dist/types/src/styles/index.d.ts.map +1 -1
- package/dist/types/src/styles/markdown.d.ts +2 -2
- package/dist/types/src/styles/markdown.d.ts.map +1 -1
- package/dist/types/src/styles/tokens.d.ts +1 -0
- package/dist/types/src/styles/tokens.d.ts.map +1 -1
- package/dist/types/src/themes/default.d.ts.map +1 -1
- package/dist/types/src/util.d.ts +7 -0
- package/dist/types/src/util.d.ts.map +1 -0
- package/package.json +48 -36
- package/src/components/TextEditor/MarkdownEditor.stories.tsx +84 -91
- package/src/components/TextEditor/TextEditor.stories.tsx +12 -46
- package/src/components/TextEditor/TextEditor.tsx +135 -95
- package/src/{hooks/automerge → components/TextEditor}/automerge.stories.tsx +65 -6
- 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 +23 -19
- package/src/extensions/automerge/automerge.spec.tsx +76 -0
- package/src/extensions/automerge/automerge.test.ts +13 -0
- package/src/extensions/automerge/automerge.ts +103 -0
- package/src/extensions/automerge/cursor.ts +52 -0
- package/src/extensions/automerge/defs.ts +41 -0
- package/src/extensions/automerge/index.ts +6 -0
- package/src/extensions/automerge/semaphore.ts +81 -0
- package/src/extensions/automerge/update-automerge.ts +49 -0
- package/src/{hooks → extensions}/automerge/update-codemirror.ts +17 -9
- package/src/extensions/awareness.ts +313 -0
- package/src/extensions/basic.ts +17 -10
- package/src/extensions/blast.ts +4 -1
- package/src/extensions/comments.ts +336 -255
- package/src/extensions/cursor.ts +50 -0
- package/src/extensions/index.ts +4 -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 +11 -26
- package/src/extensions/markdown/hr.ts +71 -0
- package/src/extensions/markdown/image.ts +87 -0
- package/src/extensions/markdown/index.ts +8 -0
- package/src/extensions/{link.ts → markdown/link.ts} +90 -83
- package/src/extensions/{table.ts → markdown/table.ts} +10 -4
- package/src/extensions/markdown/tasklist.ts +110 -0
- package/src/extensions/mention.ts +9 -2
- package/src/extensions/outliner.ts +12 -0
- package/src/hooks/awareness-provider.ts +124 -0
- package/src/hooks/defs.ts +42 -0
- package/src/hooks/index.ts +6 -1
- 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 +48 -115
- package/src/index.ts +3 -0
- package/src/styles/index.ts +1 -0
- package/src/styles/markdown.ts +9 -12
- package/src/styles/tokens.ts +3 -0
- package/src/themes/default.ts +59 -83
- package/src/util.ts +41 -0
- 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/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/mermaid.d.ts +0 -3
- package/dist/types/src/extensions/mermaid.d.ts.map +0 -1
- package/dist/types/src/extensions/table.d.ts.map +0 -1
- package/dist/types/src/extensions/tasklist.d.ts.map +0 -1
- package/dist/types/src/extensions/util.d.ts +0 -5
- package/dist/types/src/extensions/util.d.ts.map +0 -1
- package/dist/types/src/hooks/automerge/automerge.stories.d.ts.map +0 -1
- package/dist/types/src/hooks/automerge/handle.d.ts +0 -9
- package/dist/types/src/hooks/automerge/handle.d.ts.map +0 -1
- package/dist/types/src/hooks/automerge/index.d.ts +0 -3
- package/dist/types/src/hooks/automerge/index.d.ts.map +0 -1
- package/dist/types/src/hooks/automerge/plugin.d.ts +0 -26
- package/dist/types/src/hooks/automerge/plugin.d.ts.map +0 -1
- package/dist/types/src/hooks/automerge/semaphore.d.ts +0 -19
- package/dist/types/src/hooks/automerge/semaphore.d.ts.map +0 -1
- package/dist/types/src/hooks/automerge/update-automerge.d.ts +0 -6
- package/dist/types/src/hooks/automerge/update-automerge.d.ts.map +0 -1
- package/dist/types/src/hooks/automerge/update-codemirror.d.ts.map +0 -1
- package/dist/types/src/hooks/yjs/cursors.d.ts.map +0 -1
- package/dist/types/src/hooks/yjs/index.d.ts +0 -3
- package/dist/types/src/hooks/yjs/index.d.ts.map +0 -1
- package/dist/types/src/hooks/yjs/space-provider.d.ts +0 -27
- package/dist/types/src/hooks/yjs/space-provider.d.ts.map +0 -1
- package/dist/types/src/hooks/yjs/yjs.stories.d.ts +0 -12
- package/dist/types/src/hooks/yjs/yjs.stories.d.ts.map +0 -1
- package/dist/types/src/hooks/yjs/yjs.test.d.ts +0 -2
- package/dist/types/src/hooks/yjs/yjs.test.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 -114
- package/src/extensions/code.ts +0 -99
- package/src/extensions/hr.ts +0 -74
- package/src/extensions/image.ts +0 -69
- package/src/extensions/mermaid.ts +0 -11
- package/src/extensions/tasklist.ts +0 -121
- package/src/extensions/util.ts +0 -18
- package/src/hooks/automerge/handle.ts +0 -14
- package/src/hooks/automerge/index.ts +0 -6
- package/src/hooks/automerge/plugin.ts +0 -132
- package/src/hooks/automerge/semaphore.ts +0 -78
- package/src/hooks/automerge/update-automerge.ts +0 -44
- package/src/hooks/yjs/index.ts +0 -6
- package/src/hooks/yjs/space-provider.ts +0 -126
- package/src/hooks/yjs/yjs.stories.tsx +0 -56
- package/src/hooks/yjs/yjs.test.ts +0 -59
- 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 -189
- /package/dist/types/src/{hooks → extensions}/automerge/update-codemirror.d.ts +0 -0
- /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/dist/types/src/extensions/{tasklist.d.ts → markdown/tasklist.d.ts} +0 -0
- /package/dist/types/src/{hooks/yjs → styles}/cursors.d.ts +0 -0
- /package/src/{hooks/yjs → styles}/cursors.ts +0 -0
|
@@ -2,120 +2,112 @@
|
|
|
2
2
|
// Copyright 2023 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
|
|
8
|
-
keymap,
|
|
9
|
-
type Command,
|
|
10
|
-
type DecorationSet,
|
|
11
|
-
type ViewUpdate,
|
|
12
|
-
Decoration,
|
|
13
|
-
EditorView,
|
|
14
|
-
MatchDecorator,
|
|
15
|
-
ViewPlugin,
|
|
16
|
-
WidgetType,
|
|
17
|
-
type Rect,
|
|
18
|
-
} from '@codemirror/view';
|
|
19
|
-
import get from 'lodash.get';
|
|
5
|
+
import { invertedEffects } from '@codemirror/commands';
|
|
6
|
+
import { type Extension, Facet, StateEffect, StateField, type Text, type ChangeDesc } from '@codemirror/state';
|
|
7
|
+
import { hoverTooltip, keymap, type Command, Decoration, EditorView, type Rect } from '@codemirror/view';
|
|
20
8
|
import sortBy from 'lodash.sortby';
|
|
9
|
+
import { useEffect } from 'react';
|
|
21
10
|
|
|
22
11
|
import { debounce } from '@dxos/async';
|
|
23
|
-
import { invariant } from '@dxos/invariant';
|
|
24
|
-
import { log } from '@dxos/log';
|
|
25
12
|
import { nonNullable } from '@dxos/util';
|
|
26
13
|
|
|
27
|
-
import {
|
|
28
|
-
import { type
|
|
29
|
-
import {
|
|
30
|
-
|
|
31
|
-
// TODO(burdon): Handle delete, cut, copy, and paste (separately) text that includes comment range.
|
|
32
|
-
// TODO(burdon): Consider breaking into separate plugin (since not standalone)? Like mermaid?
|
|
14
|
+
import { Cursor } from './cursor';
|
|
15
|
+
import { type Comment, type Range } from '../hooks';
|
|
16
|
+
import { getToken } from '../styles';
|
|
17
|
+
import { callbackWrapper } from '../util';
|
|
33
18
|
|
|
34
19
|
// TODO(burdon): Reconcile with theme.
|
|
35
20
|
const styles = EditorView.baseTheme({
|
|
36
|
-
'& .cm-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
backgroundColor: 'yellow',
|
|
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': {
|
|
24
|
+
backgroundColor: getToken('extend.colors.yellow.50'),
|
|
41
25
|
},
|
|
42
|
-
'& .cm-
|
|
43
|
-
backgroundColor: '
|
|
26
|
+
'&light .cm-comment-current': {
|
|
27
|
+
backgroundColor: getToken('extend.colors.yellow.100'),
|
|
44
28
|
},
|
|
45
|
-
'& .cm-comment': {
|
|
46
|
-
|
|
29
|
+
'&dark .cm-comment': {
|
|
30
|
+
color: getToken('extend.colors.yellow.50'),
|
|
31
|
+
backgroundColor: getToken('extend.colors.yellow.900'),
|
|
47
32
|
},
|
|
48
|
-
'& .cm-comment-
|
|
49
|
-
|
|
33
|
+
'&dark .cm-comment-current': {
|
|
34
|
+
color: getToken('extend.colors.yellow.100'),
|
|
35
|
+
backgroundColor: getToken('extend.colors.yellow.950'),
|
|
50
36
|
},
|
|
51
37
|
});
|
|
52
38
|
|
|
53
|
-
const
|
|
54
|
-
|
|
55
|
-
|
|
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;
|
|
56
46
|
};
|
|
57
47
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
active?: string;
|
|
48
|
+
type SelectionState = {
|
|
49
|
+
current?: string;
|
|
61
50
|
closest?: string;
|
|
62
51
|
};
|
|
63
52
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
// TODO(burdon): Not part of state; just required for callback.
|
|
68
|
-
location?: Rect | null;
|
|
69
|
-
};
|
|
70
|
-
|
|
71
|
-
export type CommentsState = CommentSelected & {
|
|
72
|
-
ranges: ExtendedCommentRange[];
|
|
53
|
+
export type CommentsState = {
|
|
54
|
+
comments: CommentState[];
|
|
55
|
+
selection: SelectionState;
|
|
73
56
|
};
|
|
74
57
|
|
|
75
|
-
export const
|
|
76
|
-
const
|
|
77
|
-
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) {
|
|
78
61
|
return;
|
|
79
62
|
}
|
|
80
63
|
|
|
81
|
-
view.
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
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
|
+
}
|
|
86
75
|
};
|
|
87
76
|
|
|
88
|
-
export const
|
|
77
|
+
export const setComments = StateEffect.define<Comment[]>();
|
|
89
78
|
|
|
90
|
-
export const setSelection = StateEffect.define<
|
|
79
|
+
export const setSelection = StateEffect.define<SelectionState>();
|
|
91
80
|
|
|
92
81
|
const setCommentState = StateEffect.define<CommentsState>();
|
|
93
82
|
|
|
94
83
|
/**
|
|
95
84
|
* State field (reducer) that tracks comment ranges.
|
|
96
|
-
* The ranges are tracked as
|
|
97
|
-
* therefore must be updated when the document changes.
|
|
85
|
+
* The ranges are tracked as Automerge cursors from which the absolute indexed ranges can be computed.
|
|
98
86
|
*/
|
|
99
|
-
const
|
|
100
|
-
create: () => ({
|
|
87
|
+
const commentsState = StateField.define<CommentsState>({
|
|
88
|
+
create: () => ({ comments: [], selection: {} }),
|
|
101
89
|
update: (value, tr) => {
|
|
102
90
|
for (const effect of tr.effects) {
|
|
103
91
|
// Update selection.
|
|
104
92
|
if (effect.is(setSelection)) {
|
|
105
|
-
return { ...value,
|
|
93
|
+
return { ...value, selection: effect.value };
|
|
106
94
|
}
|
|
107
95
|
|
|
108
96
|
// Update range from store.
|
|
109
|
-
if (effect.is(
|
|
110
|
-
const
|
|
111
|
-
const ranges: ExtendedCommentRange[] = comments
|
|
97
|
+
if (effect.is(setComments)) {
|
|
98
|
+
const comments: CommentState[] = effect.value
|
|
112
99
|
.map((comment) => {
|
|
113
|
-
|
|
114
|
-
|
|
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 };
|
|
115
107
|
})
|
|
116
108
|
.filter(nonNullable);
|
|
117
109
|
|
|
118
|
-
return { ...value,
|
|
110
|
+
return { ...value, comments };
|
|
119
111
|
}
|
|
120
112
|
|
|
121
113
|
// Update entire state.
|
|
@@ -131,64 +123,48 @@ const commentsStateField = StateField.define<CommentsState>({
|
|
|
131
123
|
/**
|
|
132
124
|
* Decorate ranges.
|
|
133
125
|
*/
|
|
134
|
-
const
|
|
135
|
-
const {
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
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
|
+
}
|
|
143
139
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
.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);
|
|
152
147
|
|
|
153
148
|
return Decoration.set(decorations);
|
|
154
149
|
});
|
|
155
150
|
|
|
156
|
-
/**
|
|
157
|
-
* Optional bookmark widget at the start/end of the selection.
|
|
158
|
-
* May correspond to a markdown bookmark.
|
|
159
|
-
*/
|
|
160
|
-
class BookmarkWidget extends WidgetType {
|
|
161
|
-
constructor(
|
|
162
|
-
private readonly _anchor: number,
|
|
163
|
-
private readonly _id: string,
|
|
164
|
-
private readonly _handleClick?: () => void,
|
|
165
|
-
) {
|
|
166
|
-
super();
|
|
167
|
-
invariant(this._id);
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
override eq(other: BookmarkWidget) {
|
|
171
|
-
return this._anchor === other._anchor && this._id === other._id;
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
override toDOM() {
|
|
175
|
-
const span = document.createElement('span');
|
|
176
|
-
span.className = 'cm-bookmark';
|
|
177
|
-
span.textContent = '※';
|
|
178
|
-
if (this._handleClick) {
|
|
179
|
-
span.onclick = () => this._handleClick!();
|
|
180
|
-
}
|
|
181
|
-
return span;
|
|
182
|
-
}
|
|
183
|
-
}
|
|
184
|
-
|
|
185
151
|
export type CommentsOptions = {
|
|
152
|
+
/**
|
|
153
|
+
* Key shortcut to create a new thread.
|
|
154
|
+
*/
|
|
186
155
|
key?: string;
|
|
187
|
-
footnote?: boolean;
|
|
188
156
|
/**
|
|
189
157
|
* Called to create a new thread and return the thread id.
|
|
190
158
|
*/
|
|
191
|
-
onCreate?: (
|
|
159
|
+
onCreate?: (params: { cursor: string; from: number; location?: Rect | null }) => string | undefined;
|
|
160
|
+
/**
|
|
161
|
+
* Selection cut/deleted.
|
|
162
|
+
*/
|
|
163
|
+
onDelete?: (thread: string) => void;
|
|
164
|
+
/**
|
|
165
|
+
* Called when a comment is moved.
|
|
166
|
+
*/
|
|
167
|
+
onUpdate?: (thread: string, cursor: string) => void;
|
|
192
168
|
/**
|
|
193
169
|
* Called to notify which thread is currently closest to the cursor.
|
|
194
170
|
*/
|
|
@@ -196,102 +172,185 @@ export type CommentsOptions = {
|
|
|
196
172
|
/**
|
|
197
173
|
* Called to render tooltip.
|
|
198
174
|
*/
|
|
199
|
-
onHover?: (el: Element) => void;
|
|
175
|
+
onHover?: (el: Element, shortcut: string) => void;
|
|
200
176
|
};
|
|
201
177
|
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
178
|
+
type TrackedComment = { id: string; from: number; to: number };
|
|
179
|
+
|
|
180
|
+
const trackPastedComments = (onUpdate: NonNullable<CommentsOptions['onUpdate']>) => {
|
|
181
|
+
// Tracks indexed selections within text.
|
|
182
|
+
let tracked: { text: Text; comments: TrackedComment[] } | null = null;
|
|
183
|
+
|
|
184
|
+
// Track cut or copy (enables cut-and-paste and copy-delete-paste to restore comment selection).
|
|
185
|
+
const handleTrack = (event: Event, view: EditorView) => {
|
|
186
|
+
const comments = view.state.field(commentsState);
|
|
187
|
+
const { main } = view.state.selection;
|
|
188
|
+
const selectedRanges = comments.comments.filter(
|
|
189
|
+
({ range }) => range.from >= main.from && range.to <= main.to && range.from < range.to,
|
|
190
|
+
);
|
|
191
|
+
|
|
192
|
+
if (!selectedRanges.length) {
|
|
193
|
+
tracked = null;
|
|
194
|
+
} else {
|
|
195
|
+
tracked = {
|
|
196
|
+
text: view.state.doc.slice(main.from, main.to),
|
|
197
|
+
comments: selectedRanges.map(({ comment, range }) => ({
|
|
198
|
+
id: comment.id,
|
|
199
|
+
from: range.from - main.from,
|
|
200
|
+
to: range.to - main.from,
|
|
201
|
+
})),
|
|
202
|
+
};
|
|
225
203
|
}
|
|
204
|
+
};
|
|
226
205
|
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
if (id) {
|
|
233
|
-
// Update range.
|
|
234
|
-
view.dispatch({
|
|
235
|
-
effects: setSelection.of({ active: id }),
|
|
236
|
-
selection: { anchor: from },
|
|
237
|
-
});
|
|
206
|
+
return [
|
|
207
|
+
EditorView.domEventHandlers({
|
|
208
|
+
cut: handleTrack,
|
|
209
|
+
copy: handleTrack,
|
|
210
|
+
}),
|
|
238
211
|
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
}
|
|
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
|
+
}
|
|
246
224
|
}
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
return effects;
|
|
228
|
+
}),
|
|
247
229
|
|
|
248
|
-
|
|
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
|
+
}
|
|
249
244
|
}
|
|
250
|
-
}
|
|
251
245
|
|
|
252
|
-
|
|
253
|
-
|
|
246
|
+
if (tracked) {
|
|
247
|
+
const paste = update.transactions.find((tr) => tr.isUserEvent('input.paste'));
|
|
248
|
+
if (paste) {
|
|
249
|
+
let found = -1;
|
|
250
|
+
paste.changes.iterChanges((fromA, toA, fromB, toB, text) => {
|
|
251
|
+
if (text.eq(tracked!.text)) {
|
|
252
|
+
for (let i = update.transactions.indexOf(paste!) + 1; i < update.transactions.length; i++) {
|
|
253
|
+
fromB = update.transactions[i].changes.mapPos(fromB);
|
|
254
|
+
}
|
|
254
255
|
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
*/
|
|
258
|
-
const bookmarksViewPlugin = ViewPlugin.fromClass(
|
|
259
|
-
class BookmarkViewPlugin {
|
|
260
|
-
// Match markdown footnotes (option).
|
|
261
|
-
// https://www.markdownguide.org/extended-syntax/#footnotes
|
|
262
|
-
static bookmarkMatcher = new MatchDecorator({
|
|
263
|
-
regexp: /\[\^(\w+)\]/g,
|
|
264
|
-
decoration: (match, view, pos) => {
|
|
265
|
-
const id = match[1];
|
|
266
|
-
return Decoration.replace({
|
|
267
|
-
id,
|
|
268
|
-
widget: new BookmarkWidget(pos, id),
|
|
256
|
+
found = fromB;
|
|
257
|
+
}
|
|
269
258
|
});
|
|
270
|
-
},
|
|
271
|
-
});
|
|
272
259
|
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
260
|
+
if (found > -1) {
|
|
261
|
+
for (const moved of tracked.comments) {
|
|
262
|
+
restore.push({ id: moved.id, from: found + moved.from, to: found + moved.to });
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
tracked = null;
|
|
267
|
+
}
|
|
276
268
|
}
|
|
277
269
|
|
|
278
|
-
|
|
279
|
-
|
|
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
|
+
}
|
|
280
277
|
}
|
|
281
|
-
},
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
278
|
+
}),
|
|
279
|
+
];
|
|
280
|
+
};
|
|
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
|
+
|
|
334
|
+
/**
|
|
335
|
+
* Comment threads.
|
|
336
|
+
* 1). Updates the EditorModel to store relative selections for a set of comments threads.
|
|
337
|
+
* Since the selections are relative, they do not need to be updated when the document is edited.
|
|
338
|
+
* 2). Implements a StateField to track absolute selections corresponding to the comments (i.e., when the document is edited).
|
|
339
|
+
* 3). Creates decoration marks to apply classes to each selection.
|
|
340
|
+
* 4). Tracks the current cursor position to:
|
|
341
|
+
* a). Update the decoration to show if the cursor is within a current selection.
|
|
342
|
+
* b). Calls a handler to indicate which is the closest selection (e.g., to update the thread sidebar).
|
|
343
|
+
* 5). Optionally, implements a hoverTooltip to show hints when creating a selection range.
|
|
344
|
+
*/
|
|
345
|
+
export const comments = (options: CommentsOptions = {}): Extension => {
|
|
346
|
+
const { key: shortcut = "meta-'" } = options;
|
|
347
|
+
|
|
348
|
+
const handleSelect = debounce((state: CommentsState) => options.onSelect?.(state), 200);
|
|
290
349
|
|
|
291
350
|
return [
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
351
|
+
optionsFacet.of(options),
|
|
352
|
+
commentsState,
|
|
353
|
+
commentsDecorations,
|
|
295
354
|
styles,
|
|
296
355
|
|
|
297
356
|
//
|
|
@@ -300,8 +359,8 @@ export const comments = (options: CommentsOptions = {}): Extension => {
|
|
|
300
359
|
options.onCreate
|
|
301
360
|
? keymap.of([
|
|
302
361
|
{
|
|
303
|
-
key:
|
|
304
|
-
run: callbackWrapper(
|
|
362
|
+
key: shortcut,
|
|
363
|
+
run: callbackWrapper(createComment),
|
|
305
364
|
},
|
|
306
365
|
])
|
|
307
366
|
: [],
|
|
@@ -320,9 +379,8 @@ export const comments = (options: CommentsOptions = {}): Extension => {
|
|
|
320
379
|
end: selection.to,
|
|
321
380
|
above: true,
|
|
322
381
|
create: () => {
|
|
323
|
-
// TODO(burdon): Dispatch to react callback to render (or use SSR)?
|
|
324
382
|
const el = document.createElement('div');
|
|
325
|
-
options.onHover
|
|
383
|
+
options.onHover!(el, shortcut);
|
|
326
384
|
return { dom: el, offset: { x: 0, y: 8 } };
|
|
327
385
|
},
|
|
328
386
|
};
|
|
@@ -330,76 +388,99 @@ export const comments = (options: CommentsOptions = {}): Extension => {
|
|
|
330
388
|
|
|
331
389
|
return null;
|
|
332
390
|
},
|
|
333
|
-
{
|
|
391
|
+
{
|
|
392
|
+
// TODO(burdon): Hide on change triggered immediately?
|
|
393
|
+
// hideOnChange: true,
|
|
394
|
+
hoverTime: 1_000,
|
|
395
|
+
},
|
|
334
396
|
)
|
|
335
397
|
: [],
|
|
336
398
|
|
|
337
399
|
//
|
|
338
|
-
//
|
|
339
|
-
// TODO(burdon): Is there a better (finer grained) way to do this?
|
|
400
|
+
// Track deleted ranges and update ranges for decorations.
|
|
340
401
|
//
|
|
341
|
-
EditorView.updateListener.of(({ view, state,
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
ranges.forEach((range) => {
|
|
352
|
-
if (from2 === to2) {
|
|
353
|
-
const newRange = model.getRangeFromCursor!(range.cursor);
|
|
354
|
-
if (!newRange || newRange.to - newRange.from === 0) {
|
|
355
|
-
// TODO(burdon): Delete range if empty.
|
|
356
|
-
log.info('deleted comment', { thread: range.id });
|
|
357
|
-
}
|
|
402
|
+
EditorView.updateListener.of(({ view, state, changes }) => {
|
|
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);
|
|
358
412
|
}
|
|
413
|
+
}
|
|
359
414
|
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
}
|
|
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
|
+
}
|
|
366
421
|
});
|
|
422
|
+
});
|
|
367
423
|
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
}
|
|
424
|
+
if (mod) {
|
|
425
|
+
view.dispatch({ effects: setCommentState.of({ comments, ...value }) });
|
|
371
426
|
}
|
|
427
|
+
}),
|
|
372
428
|
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
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));
|
|
387
449
|
if (d < min) {
|
|
388
|
-
|
|
450
|
+
selection.closest = comment.id;
|
|
389
451
|
min = d;
|
|
390
452
|
}
|
|
391
|
-
});
|
|
392
|
-
|
|
393
|
-
if (selected.active !== active || selected.closest !== closest) {
|
|
394
|
-
view.dispatch({ effects: setSelection.of(selected) });
|
|
395
|
-
|
|
396
|
-
// Update callback.
|
|
397
|
-
handleSelect({
|
|
398
|
-
...selected,
|
|
399
|
-
ranges: ranges.map((range) => ({ ...range, location: view.coordsAtPos(range.from) })),
|
|
400
|
-
});
|
|
401
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
|
+
});
|
|
402
468
|
}
|
|
403
469
|
}),
|
|
470
|
+
|
|
471
|
+
options.onUpdate ? trackPastedComments(options.onUpdate) : [],
|
|
404
472
|
];
|
|
405
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
|
+
};
|