@dxos/react-ui-editor 0.3.11-main.08776d7 → 0.3.11-main.0999ea0
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 +1211 -1005
- package/dist/lib/browser/index.mjs.map +4 -4
- package/dist/lib/browser/meta.json +1 -1
- package/dist/types/src/components/TextEditor/MarkdownEditor.stories.d.ts +12 -17
- package/dist/types/src/components/TextEditor/MarkdownEditor.stories.d.ts.map +1 -1
- package/dist/types/src/components/TextEditor/TextEditor.d.ts +13 -7
- package/dist/types/src/components/TextEditor/TextEditor.d.ts.map +1 -1
- package/dist/types/src/components/TextEditor/TextEditor.stories.d.ts +1 -1
- package/dist/types/src/components/TextEditor/comments.stories.d.ts.map +1 -1
- package/dist/types/src/components/Toolbar/Toolbar.d.ts +9 -0
- package/dist/types/src/components/Toolbar/Toolbar.d.ts.map +1 -0
- package/dist/types/src/components/Toolbar/Toolbar.stories.d.ts +14 -0
- package/dist/types/src/components/Toolbar/Toolbar.stories.d.ts.map +1 -0
- package/dist/types/src/extensions/autocomplete.d.ts.map +1 -1
- package/dist/types/src/extensions/automerge/automerge.d.ts.map +1 -1
- package/dist/types/src/extensions/automerge/automerge.spec.d.ts +2 -0
- package/dist/types/src/extensions/automerge/automerge.spec.d.ts.map +1 -0
- package/dist/types/src/extensions/automerge/automerge.test.d.ts +2 -0
- package/dist/types/src/extensions/automerge/automerge.test.d.ts.map +1 -0
- package/dist/types/src/extensions/automerge/defs.d.ts +6 -6
- package/dist/types/src/extensions/automerge/defs.d.ts.map +1 -1
- package/dist/types/src/extensions/automerge/semaphore.d.ts +6 -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 +11 -3
- package/dist/types/src/extensions/comments.d.ts.map +1 -1
- package/dist/types/src/extensions/index.d.ts +0 -8
- package/dist/types/src/extensions/index.d.ts.map +1 -1
- package/dist/types/src/extensions/markdown/bundle.d.ts +3 -6
- package/dist/types/src/extensions/markdown/bundle.d.ts.map +1 -1
- package/dist/types/src/extensions/markdown/code.d.ts.map +1 -0
- package/dist/types/src/extensions/markdown/formatting.d.ts +11 -0
- package/dist/types/src/extensions/markdown/formatting.d.ts.map +1 -0
- package/dist/types/src/extensions/markdown/heading.d.ts +6 -0
- package/dist/types/src/extensions/markdown/heading.d.ts.map +1 -0
- package/dist/types/src/extensions/markdown/highlight.d.ts.map +1 -1
- package/dist/types/src/extensions/markdown/hr.d.ts.map +1 -0
- package/dist/types/src/extensions/markdown/image.d.ts.map +1 -0
- package/dist/types/src/extensions/markdown/index.d.ts +8 -0
- package/dist/types/src/extensions/markdown/index.d.ts.map +1 -1
- package/dist/types/src/extensions/markdown/link.d.ts.map +1 -0
- package/dist/types/src/extensions/markdown/table.d.ts.map +1 -0
- package/dist/types/src/extensions/markdown/tasklist.d.ts +3 -0
- package/dist/types/src/extensions/markdown/tasklist.d.ts.map +1 -0
- package/dist/types/src/extensions/mention.d.ts.map +1 -1
- package/dist/types/src/hooks/awareness-provider.d.ts +7 -4
- package/dist/types/src/hooks/awareness-provider.d.ts.map +1 -1
- package/dist/types/src/hooks/defs.d.ts +1 -4
- package/dist/types/src/hooks/defs.d.ts.map +1 -1
- package/dist/types/src/hooks/index.d.ts +2 -2
- package/dist/types/src/hooks/index.d.ts.map +1 -1
- package/dist/types/src/hooks/useActionHandler.d.ts +4 -0
- package/dist/types/src/hooks/useActionHandler.d.ts.map +1 -0
- package/dist/types/src/hooks/useTextEditor.d.ts +17 -0
- package/dist/types/src/hooks/useTextEditor.d.ts.map +1 -0
- package/dist/types/src/hooks/useTextModel.d.ts.map +1 -1
- package/dist/types/src/styles/markdown.d.ts +2 -2
- package/dist/types/src/styles/markdown.d.ts.map +1 -1
- package/dist/types/src/themes/default.d.ts.map +1 -1
- package/dist/types/src/util.d.ts +2 -0
- package/dist/types/src/util.d.ts.map +1 -1
- package/package.json +45 -36
- package/src/components/TextEditor/MarkdownEditor.stories.tsx +61 -72
- package/src/components/TextEditor/TextEditor.tsx +101 -77
- package/src/components/TextEditor/comments.stories.tsx +16 -21
- package/src/components/Toolbar/Toolbar.stories.tsx +86 -0
- package/src/components/Toolbar/Toolbar.tsx +115 -0
- package/src/extensions/autocomplete.ts +6 -3
- package/src/extensions/automerge/automerge.spec.tsx +76 -0
- package/src/extensions/automerge/automerge.test.ts +13 -0
- package/src/extensions/automerge/automerge.ts +53 -13
- package/src/extensions/automerge/cursor.ts +3 -3
- package/src/extensions/automerge/defs.ts +9 -29
- package/src/extensions/automerge/semaphore.ts +24 -28
- package/src/extensions/automerge/update-automerge.ts +14 -8
- package/src/extensions/automerge/update-codemirror.ts +5 -5
- package/src/extensions/awareness.ts +17 -17
- package/src/extensions/basic.ts +22 -17
- package/src/extensions/comments.ts +149 -92
- package/src/extensions/index.ts +0 -8
- package/src/extensions/markdown/bundle.ts +20 -30
- package/src/extensions/markdown/code.ts +142 -0
- package/src/extensions/markdown/formatting.ts +159 -0
- package/src/extensions/markdown/heading.ts +59 -0
- package/src/extensions/markdown/highlight.ts +9 -15
- package/src/extensions/markdown/hr.ts +71 -0
- package/src/extensions/markdown/index.ts +8 -0
- package/src/extensions/{link.ts → markdown/link.ts} +5 -2
- package/src/extensions/{tasklist.ts → markdown/tasklist.ts} +48 -34
- package/src/extensions/mention.ts +9 -2
- package/src/hooks/awareness-provider.ts +24 -20
- package/src/hooks/defs.ts +1 -6
- package/src/hooks/index.ts +2 -2
- package/src/hooks/useActionHandler.ts +45 -0
- package/src/hooks/useTextEditor.ts +29 -0
- package/src/hooks/useTextModel.ts +48 -11
- package/src/styles/markdown.ts +9 -10
- package/src/themes/default.ts +1 -3
- package/src/util.ts +26 -3
- package/dist/types/src/components/TextEditor/codemirror.stories.d.ts +0 -12
- package/dist/types/src/components/TextEditor/codemirror.stories.d.ts.map +0 -1
- package/dist/types/src/components/TextEditor/yjs.stories.d.ts +0 -12
- package/dist/types/src/components/TextEditor/yjs.stories.d.ts.map +0 -1
- package/dist/types/src/extensions/code.d.ts.map +0 -1
- package/dist/types/src/extensions/hr.d.ts.map +0 -1
- package/dist/types/src/extensions/image.d.ts.map +0 -1
- package/dist/types/src/extensions/link.d.ts.map +0 -1
- package/dist/types/src/extensions/sync.d.ts +0 -7
- package/dist/types/src/extensions/sync.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 -4
- package/dist/types/src/hooks/automerge.d.ts.map +0 -1
- package/dist/types/src/hooks/yjs.d.ts +0 -28
- package/dist/types/src/hooks/yjs.d.ts.map +0 -1
- package/dist/types/src/testing/index.d.ts +0 -3
- package/dist/types/src/testing/index.d.ts.map +0 -1
- package/dist/types/src/testing/proto/gen/schema.d.ts +0 -15
- package/dist/types/src/testing/proto/gen/schema.d.ts.map +0 -1
- package/dist/types/src/testing/proto/index.d.ts +0 -2
- package/dist/types/src/testing/proto/index.d.ts.map +0 -1
- package/dist/types/src/testing/replicator.d.ts +0 -45
- package/dist/types/src/testing/replicator.d.ts.map +0 -1
- package/src/components/TextEditor/codemirror.stories.ts +0 -115
- package/src/components/TextEditor/yjs.stories.tsx +0 -56
- package/src/extensions/code.ts +0 -104
- package/src/extensions/hr.ts +0 -72
- package/src/extensions/sync.ts +0 -15
- 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 -53
- package/src/hooks/yjs.ts +0 -157
- 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,16 +2,16 @@
|
|
|
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
14
|
import { Cursor } from './cursor';
|
|
14
|
-
import { syncFacet } from './sync';
|
|
15
15
|
import { type Comment, type Range } from '../hooks';
|
|
16
16
|
import { getToken } from '../styles';
|
|
17
17
|
import { callbackWrapper } from '../util';
|
|
@@ -21,15 +21,13 @@ 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
|
-
highlightActive: Decoration.mark({ class: 'cm-comment-active' }),
|
|
32
|
-
};
|
|
29
|
+
const commentMark = Decoration.mark({ class: 'cm-comment' });
|
|
30
|
+
const commentCurrentMark = Decoration.mark({ class: 'cm-comment-current' });
|
|
33
31
|
|
|
34
32
|
type CommentState = {
|
|
35
33
|
comment: Comment;
|
|
@@ -38,7 +36,7 @@ type CommentState = {
|
|
|
38
36
|
};
|
|
39
37
|
|
|
40
38
|
type SelectionState = {
|
|
41
|
-
|
|
39
|
+
current?: string;
|
|
42
40
|
closest?: string;
|
|
43
41
|
};
|
|
44
42
|
|
|
@@ -47,17 +45,23 @@ export type CommentsState = {
|
|
|
47
45
|
selection: SelectionState;
|
|
48
46
|
};
|
|
49
47
|
|
|
50
|
-
export const setFocus = (view: EditorView,
|
|
51
|
-
const comment = view.state.field(commentsState).comments.find((range) => range.comment.id ===
|
|
52
|
-
if (!comment) {
|
|
48
|
+
export const setFocus = (view: EditorView, id: string, center = true) => {
|
|
49
|
+
const comment = view.state.field(commentsState).comments.find((range) => range.comment.id === id);
|
|
50
|
+
if (!comment?.comment.cursor) {
|
|
53
51
|
return;
|
|
54
52
|
}
|
|
55
53
|
|
|
56
|
-
view.
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
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
|
+
}
|
|
61
65
|
};
|
|
62
66
|
|
|
63
67
|
export const setComments = StateEffect.define<Comment[]>();
|
|
@@ -89,7 +93,6 @@ const commentsState = StateField.define<CommentsState>({
|
|
|
89
93
|
}
|
|
90
94
|
|
|
91
95
|
const range = Cursor.getRangeFromCursor(tr.state, comment.cursor);
|
|
92
|
-
// console.log('update', JSON.stringify({ range, cursor: comment.cursor }, undefined, 2));
|
|
93
96
|
return range && { comment, range };
|
|
94
97
|
})
|
|
95
98
|
.filter(nonNullable);
|
|
@@ -110,9 +113,9 @@ const commentsState = StateField.define<CommentsState>({
|
|
|
110
113
|
/**
|
|
111
114
|
* Decorate ranges.
|
|
112
115
|
*/
|
|
113
|
-
const
|
|
116
|
+
const commentsDecorations = EditorView.decorations.compute([commentsState], (state) => {
|
|
114
117
|
const {
|
|
115
|
-
selection: {
|
|
118
|
+
selection: { current },
|
|
116
119
|
comments,
|
|
117
120
|
} = state.field(commentsState);
|
|
118
121
|
|
|
@@ -124,10 +127,10 @@ const highlightDecorations = EditorView.decorations.compute([commentsState], (st
|
|
|
124
127
|
return undefined;
|
|
125
128
|
}
|
|
126
129
|
|
|
127
|
-
if (comment.comment.id ===
|
|
128
|
-
return
|
|
130
|
+
if (comment.comment.id === current) {
|
|
131
|
+
return commentCurrentMark.range(range.from, range.to);
|
|
129
132
|
} else {
|
|
130
|
-
return
|
|
133
|
+
return commentMark.range(range.from, range.to);
|
|
131
134
|
}
|
|
132
135
|
})
|
|
133
136
|
.filter(nonNullable);
|
|
@@ -162,12 +165,11 @@ export type CommentsOptions = {
|
|
|
162
165
|
onHover?: (el: Element, shortcut: string) => void;
|
|
163
166
|
};
|
|
164
167
|
|
|
165
|
-
|
|
168
|
+
type TrackedComment = { id: string; from: number; to: number };
|
|
169
|
+
|
|
166
170
|
const trackPastedComments = (onUpdate: NonNullable<CommentsOptions['onUpdate']>) => {
|
|
167
|
-
// Cut/deleted comments.
|
|
168
171
|
// Tracks indexed selections within text.
|
|
169
|
-
|
|
170
|
-
let tracked: { text: Text; comments: { id: string; from: number; to: number }[] } | null = null;
|
|
172
|
+
let tracked: { text: Text; comments: TrackedComment[] } | null = null;
|
|
171
173
|
|
|
172
174
|
// Track cut or copy (enables cut-and-paste and copy-delete-paste to restore comment selection).
|
|
173
175
|
const handleTrack = (event: Event, view: EditorView) => {
|
|
@@ -189,8 +191,6 @@ const trackPastedComments = (onUpdate: NonNullable<CommentsOptions['onUpdate']>)
|
|
|
189
191
|
})),
|
|
190
192
|
};
|
|
191
193
|
}
|
|
192
|
-
|
|
193
|
-
// console.log('track', JSON.stringify({ tracked }, undefined, 2));
|
|
194
194
|
};
|
|
195
195
|
|
|
196
196
|
return [
|
|
@@ -199,16 +199,48 @@ const trackPastedComments = (onUpdate: NonNullable<CommentsOptions['onUpdate']>)
|
|
|
199
199
|
copy: handleTrack,
|
|
200
200
|
}),
|
|
201
201
|
|
|
202
|
-
//
|
|
203
|
-
|
|
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
|
+
|
|
204
236
|
if (tracked) {
|
|
205
|
-
const paste = transactions.find((tr) => tr.isUserEvent('input.paste'));
|
|
237
|
+
const paste = update.transactions.find((tr) => tr.isUserEvent('input.paste'));
|
|
206
238
|
if (paste) {
|
|
207
239
|
let found = -1;
|
|
208
240
|
paste.changes.iterChanges((fromA, toA, fromB, toB, text) => {
|
|
209
241
|
if (text.eq(tracked!.text)) {
|
|
210
|
-
for (let i = transactions.indexOf(paste!) + 1; i < transactions.length; i++) {
|
|
211
|
-
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);
|
|
212
244
|
}
|
|
213
245
|
|
|
214
246
|
found = fromB;
|
|
@@ -216,29 +248,79 @@ const trackPastedComments = (onUpdate: NonNullable<CommentsOptions['onUpdate']>)
|
|
|
216
248
|
});
|
|
217
249
|
|
|
218
250
|
if (found > -1) {
|
|
219
|
-
// Sync before recomputing cursor/range.
|
|
220
|
-
// TODO(burdon): Try to avoid this? See TextEditor dispatch.
|
|
221
|
-
state.facet(syncFacet)?.reconcile(view);
|
|
222
|
-
|
|
223
251
|
for (const moved of tracked.comments) {
|
|
224
|
-
|
|
225
|
-
from: found + moved.from,
|
|
226
|
-
to: found + moved.to,
|
|
227
|
-
};
|
|
228
|
-
|
|
229
|
-
const cursor = Cursor.getCursorFromRange(state, range);
|
|
230
|
-
// console.log('paste', JSON.stringify({ moved, range, cursor }, undefined, 2));
|
|
231
|
-
onUpdate(moved.id, cursor);
|
|
252
|
+
restore.push({ id: moved.id, from: found + moved.from, to: found + moved.to });
|
|
232
253
|
}
|
|
233
254
|
}
|
|
234
255
|
|
|
235
256
|
tracked = null;
|
|
236
257
|
}
|
|
237
258
|
}
|
|
259
|
+
|
|
260
|
+
for (const comment of restore) {
|
|
261
|
+
const { comments } = update.startState.field(commentsState);
|
|
262
|
+
const exists = comments.some((c) => c.comment.id === comment.id && c.range.from < c.range.to);
|
|
263
|
+
if (!exists) {
|
|
264
|
+
const cursor = Cursor.getCursorFromRange(update.state, comment);
|
|
265
|
+
onUpdate(comment.id, cursor);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
238
268
|
}),
|
|
239
269
|
];
|
|
240
270
|
};
|
|
241
271
|
|
|
272
|
+
const mapTrackedComment = (comment: TrackedComment, changes: ChangeDesc) => ({
|
|
273
|
+
id: comment.id,
|
|
274
|
+
from: changes.mapPos(comment.from, 1),
|
|
275
|
+
to: changes.mapPos(comment.to, 1),
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
// These are attached to undone/redone transactions in the editor for the purpose of restoring comments
|
|
279
|
+
// that were deleted by the original changes.
|
|
280
|
+
const restoreCommentEffect = StateEffect.define<TrackedComment>({ map: mapTrackedComment });
|
|
281
|
+
|
|
282
|
+
const optionsFacet = Facet.define<CommentsOptions, CommentsOptions>({
|
|
283
|
+
combine: (providers) => providers[0],
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* Create comment thread action.
|
|
288
|
+
*/
|
|
289
|
+
export const createComment: Command = (view) => {
|
|
290
|
+
const options = view.state.facet(optionsFacet);
|
|
291
|
+
const { from, to } = view.state.selection.main;
|
|
292
|
+
if (from === to) {
|
|
293
|
+
return false;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// Don't allow selection at end of document.
|
|
297
|
+
if (to === view.state.doc.length) {
|
|
298
|
+
view.dispatch({
|
|
299
|
+
changes: {
|
|
300
|
+
from: to,
|
|
301
|
+
insert: '\n',
|
|
302
|
+
},
|
|
303
|
+
});
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
const cursor = Cursor.getCursorFromRange(view.state, { from, to });
|
|
307
|
+
if (cursor) {
|
|
308
|
+
// Create thread via callback.
|
|
309
|
+
const id = options.onCreate?.(cursor, view.coordsAtPos(from));
|
|
310
|
+
if (id) {
|
|
311
|
+
// Update range.
|
|
312
|
+
view.dispatch({
|
|
313
|
+
effects: setSelection.of({ current: id }),
|
|
314
|
+
selection: { anchor: to },
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
return true;
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
return false;
|
|
322
|
+
};
|
|
323
|
+
|
|
242
324
|
/**
|
|
243
325
|
* Comment threads.
|
|
244
326
|
* 1). Updates the EditorModel to store relative selections for a set of comments threads.
|
|
@@ -255,47 +337,10 @@ export const comments = (options: CommentsOptions = {}): Extension => {
|
|
|
255
337
|
|
|
256
338
|
const handleSelect = debounce((state: CommentsState) => options.onSelect?.(state), 200);
|
|
257
339
|
|
|
258
|
-
/**
|
|
259
|
-
* Create comment thread action.
|
|
260
|
-
*/
|
|
261
|
-
const createCommentThread: Command = (view) => {
|
|
262
|
-
invariant(options.onCreate);
|
|
263
|
-
const { from, to } = view.state.selection.main;
|
|
264
|
-
if (from === to) {
|
|
265
|
-
return false;
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
// Don't allow selection at end of document.
|
|
269
|
-
if (to === view.state.doc.length) {
|
|
270
|
-
view.dispatch({
|
|
271
|
-
changes: {
|
|
272
|
-
from: to,
|
|
273
|
-
insert: '\n',
|
|
274
|
-
},
|
|
275
|
-
});
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
const cursor = Cursor.getCursorFromRange(view.state, { from, to });
|
|
279
|
-
if (cursor) {
|
|
280
|
-
// Create thread via callback.
|
|
281
|
-
const id = callbackWrapper(options.onCreate)(cursor, view.coordsAtPos(from));
|
|
282
|
-
if (id) {
|
|
283
|
-
// Update range.
|
|
284
|
-
view.dispatch({
|
|
285
|
-
effects: setSelection.of({ active: id }),
|
|
286
|
-
selection: { anchor: from },
|
|
287
|
-
});
|
|
288
|
-
|
|
289
|
-
return true;
|
|
290
|
-
}
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
return false;
|
|
294
|
-
};
|
|
295
|
-
|
|
296
340
|
return [
|
|
341
|
+
optionsFacet.of(options),
|
|
297
342
|
commentsState,
|
|
298
|
-
|
|
343
|
+
commentsDecorations,
|
|
299
344
|
styles,
|
|
300
345
|
|
|
301
346
|
//
|
|
@@ -305,8 +350,7 @@ export const comments = (options: CommentsOptions = {}): Extension => {
|
|
|
305
350
|
? keymap.of([
|
|
306
351
|
{
|
|
307
352
|
key: shortcut,
|
|
308
|
-
|
|
309
|
-
run: callbackWrapper(createCommentThread),
|
|
353
|
+
run: callbackWrapper(createComment),
|
|
310
354
|
},
|
|
311
355
|
])
|
|
312
356
|
: [],
|
|
@@ -375,10 +419,10 @@ export const comments = (options: CommentsOptions = {}): Extension => {
|
|
|
375
419
|
//
|
|
376
420
|
// Track selection/proximity.
|
|
377
421
|
//
|
|
378
|
-
EditorView.updateListener.of(({ view, state
|
|
422
|
+
EditorView.updateListener.of(({ view, state }) => {
|
|
379
423
|
let min = Infinity;
|
|
380
424
|
const {
|
|
381
|
-
selection: {
|
|
425
|
+
selection: { current, closest },
|
|
382
426
|
comments,
|
|
383
427
|
} = state.field(commentsState);
|
|
384
428
|
|
|
@@ -386,11 +430,11 @@ export const comments = (options: CommentsOptions = {}): Extension => {
|
|
|
386
430
|
const selection: SelectionState = {};
|
|
387
431
|
comments.forEach(({ comment, range }) => {
|
|
388
432
|
if (head >= range.from && head <= range.to) {
|
|
389
|
-
selection.
|
|
433
|
+
selection.current = comment.id;
|
|
390
434
|
selection.closest = undefined;
|
|
391
435
|
}
|
|
392
436
|
|
|
393
|
-
if (!selection.
|
|
437
|
+
if (!selection.current) {
|
|
394
438
|
const d = Math.min(Math.abs(head - range.from), Math.abs(head - range.to));
|
|
395
439
|
if (d < min) {
|
|
396
440
|
selection.closest = comment.id;
|
|
@@ -399,7 +443,7 @@ export const comments = (options: CommentsOptions = {}): Extension => {
|
|
|
399
443
|
}
|
|
400
444
|
});
|
|
401
445
|
|
|
402
|
-
if (selection.
|
|
446
|
+
if (selection.current !== current || selection.closest !== closest) {
|
|
403
447
|
view.dispatch({ effects: setSelection.of(selection) });
|
|
404
448
|
|
|
405
449
|
// Update callback.
|
|
@@ -417,3 +461,16 @@ export const comments = (options: CommentsOptions = {}): Extension => {
|
|
|
417
461
|
options.onUpdate ? trackPastedComments(options.onUpdate) : [],
|
|
418
462
|
];
|
|
419
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/index.ts
CHANGED
|
@@ -7,18 +7,10 @@ export * from './automerge';
|
|
|
7
7
|
export * from './awareness';
|
|
8
8
|
export * from './basic';
|
|
9
9
|
export * from './blast';
|
|
10
|
-
export * from './code';
|
|
11
10
|
export * from './comments';
|
|
12
11
|
export * from './cursor';
|
|
13
|
-
export * from './hr';
|
|
14
|
-
export * from './image';
|
|
15
|
-
export * from './link';
|
|
16
12
|
export * from './listener';
|
|
17
13
|
export * from './markdown';
|
|
18
14
|
export * from './mention';
|
|
19
15
|
export * from './outliner';
|
|
20
|
-
export * from './sync';
|
|
21
|
-
export * from './table';
|
|
22
|
-
export * from './tasklist';
|
|
23
16
|
export * from './typewriter';
|
|
24
|
-
export * from './yjs';
|
|
@@ -23,34 +23,26 @@ import {
|
|
|
23
23
|
import type { ThemeMode } from '@dxos/react-ui';
|
|
24
24
|
|
|
25
25
|
import { markdownHighlightStyle, markdownTagsExtensions } from './highlight';
|
|
26
|
+
import { type TextEditorProps } from '../../components';
|
|
26
27
|
|
|
27
28
|
export type MarkdownBundleOptions = {
|
|
28
|
-
readonly?: boolean;
|
|
29
29
|
themeMode?: ThemeMode;
|
|
30
|
-
|
|
31
|
-
};
|
|
30
|
+
} & Pick<TextEditorProps, 'placeholder'>;
|
|
32
31
|
|
|
33
32
|
/**
|
|
34
33
|
* Markdown bundle.
|
|
35
34
|
* Refs:
|
|
36
|
-
* https://github.com/codemirror/basic-setup
|
|
37
|
-
* https://github.com/codemirror/basic-setup/blob/main/src/codemirror.ts#L50
|
|
38
35
|
* https://codemirror.net/docs/community
|
|
39
36
|
* https://codemirror.net/docs/ref/#codemirror.basicSetup
|
|
40
37
|
*/
|
|
41
38
|
// TODO(burdon): Add Composer here: https://codemirror.net/docs/community
|
|
42
|
-
export const markdownBundle = ({
|
|
43
|
-
readonly,
|
|
44
|
-
themeMode,
|
|
45
|
-
placeholder: _placeholder,
|
|
46
|
-
}: MarkdownBundleOptions): Extension[] => {
|
|
39
|
+
export const markdownBundle = ({ themeMode, placeholder: _placeholder }: MarkdownBundleOptions): Extension[] => {
|
|
47
40
|
return [
|
|
48
41
|
EditorState.allowMultipleSelections.of(true),
|
|
49
42
|
EditorState.tabSize.of(2),
|
|
50
43
|
EditorView.lineWrapping,
|
|
51
44
|
|
|
52
|
-
|
|
53
|
-
|
|
45
|
+
// https://github.com/codemirror/basic-setup
|
|
54
46
|
bracketMatching(),
|
|
55
47
|
closeBrackets(),
|
|
56
48
|
crosshairCursor(),
|
|
@@ -59,7 +51,7 @@ export const markdownBundle = ({
|
|
|
59
51
|
highlightActiveLine(),
|
|
60
52
|
history(),
|
|
61
53
|
indentOnInput(),
|
|
62
|
-
_placeholder
|
|
54
|
+
_placeholder ? placeholder(_placeholder) : [],
|
|
63
55
|
|
|
64
56
|
// Main extension.
|
|
65
57
|
// https://github.com/codemirror/lang-markdown
|
|
@@ -86,21 +78,19 @@ export const markdownBundle = ({
|
|
|
86
78
|
themeMode === 'dark' ? syntaxHighlighting(oneDarkHighlightStyle) : syntaxHighlighting(defaultHighlightStyle),
|
|
87
79
|
|
|
88
80
|
// Custom styles.
|
|
89
|
-
syntaxHighlighting(markdownHighlightStyle(
|
|
90
|
-
].filter(Boolean) as Extension[];
|
|
91
|
-
};
|
|
81
|
+
syntaxHighlighting(markdownHighlightStyle()),
|
|
92
82
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
83
|
+
keymap.of([
|
|
84
|
+
// https://codemirror.net/docs/ref/#commands.indentWithTab
|
|
85
|
+
indentWithTab,
|
|
86
|
+
// https://codemirror.net/docs/ref/#autocomplete.closeBracketsKeymap
|
|
87
|
+
...closeBracketsKeymap,
|
|
88
|
+
// https://codemirror.net/docs/ref/#commands.historyKeymap
|
|
89
|
+
...historyKeymap,
|
|
90
|
+
// https://codemirror.net/docs/ref/#search.searchKeymap
|
|
91
|
+
...searchKeymap,
|
|
92
|
+
// https://codemirror.net/docs/ref/#commands.standardKeymap
|
|
93
|
+
...standardKeymap,
|
|
94
|
+
]),
|
|
95
|
+
];
|
|
96
|
+
};
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2023 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { syntaxTree } from '@codemirror/language';
|
|
6
|
+
import { RangeSetBuilder } from '@codemirror/state';
|
|
7
|
+
import {
|
|
8
|
+
ViewPlugin,
|
|
9
|
+
type DecorationSet,
|
|
10
|
+
EditorView,
|
|
11
|
+
type ViewUpdate,
|
|
12
|
+
Decoration,
|
|
13
|
+
type BlockInfo,
|
|
14
|
+
WidgetType,
|
|
15
|
+
} from '@codemirror/view';
|
|
16
|
+
|
|
17
|
+
import { mx } from '@dxos/react-ui-theme';
|
|
18
|
+
|
|
19
|
+
import { getToken } from '../../styles';
|
|
20
|
+
|
|
21
|
+
// TODO(burdon): Reconcile with theme.
|
|
22
|
+
const styles = EditorView.baseTheme({
|
|
23
|
+
'& .cm-code': {
|
|
24
|
+
paddingInline: '1rem !important',
|
|
25
|
+
fontFamily: getToken('fontFamily.mono', []).join(','),
|
|
26
|
+
},
|
|
27
|
+
'& .cm-codeblock': {
|
|
28
|
+
display: 'inline-block',
|
|
29
|
+
width: '100%',
|
|
30
|
+
position: 'relative',
|
|
31
|
+
'&::after': {
|
|
32
|
+
content: '""',
|
|
33
|
+
position: 'absolute',
|
|
34
|
+
inset: 0,
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
'&light .cm-codeblock': {
|
|
38
|
+
'&::after': {
|
|
39
|
+
background: getToken('extend.semanticColors.input.light'),
|
|
40
|
+
mixBlendMode: 'darken',
|
|
41
|
+
},
|
|
42
|
+
},
|
|
43
|
+
'&dark .cm-codeblock': {
|
|
44
|
+
'&::after': {
|
|
45
|
+
background: getToken('extend.semanticColors.input.dark'),
|
|
46
|
+
mixBlendMode: 'lighten',
|
|
47
|
+
},
|
|
48
|
+
},
|
|
49
|
+
'& .cm-codeblock-first': {
|
|
50
|
+
'&::after': {
|
|
51
|
+
borderTopLeftRadius: '.5rem',
|
|
52
|
+
borderTopRightRadius: '.5rem',
|
|
53
|
+
},
|
|
54
|
+
},
|
|
55
|
+
'& .cm-codeblock-last': {
|
|
56
|
+
'&::after': {
|
|
57
|
+
borderBottomLeftRadius: '.5rem',
|
|
58
|
+
borderBottomRightRadius: '.5rem',
|
|
59
|
+
},
|
|
60
|
+
},
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
// TODO(burdon): Start from EditorView.lineBlockAt(pos).
|
|
64
|
+
const getLineRange = (lines: BlockInfo[], from: number, to: number) => {
|
|
65
|
+
const start = lines.findIndex((line) => line.from >= from);
|
|
66
|
+
const end = lines.findIndex((line) => line.to >= to);
|
|
67
|
+
return [start, end];
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
class LineWidget extends WidgetType {
|
|
71
|
+
constructor(private readonly _className: string) {
|
|
72
|
+
super();
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
override toDOM() {
|
|
76
|
+
const el = document.createElement('span');
|
|
77
|
+
el.innerHTML = ' ';
|
|
78
|
+
el.className = mx('cm-code cm-codeblock', this._className);
|
|
79
|
+
return el;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const top = new LineWidget('cm-codeblock-first');
|
|
84
|
+
const bottom = new LineWidget('cm-codeblock-last');
|
|
85
|
+
|
|
86
|
+
const buildDecorations = (view: EditorView): DecorationSet => {
|
|
87
|
+
const builder = new RangeSetBuilder<Decoration>();
|
|
88
|
+
const { state } = view;
|
|
89
|
+
const blocks = view.viewportLineBlocks;
|
|
90
|
+
const cursor = state.selection.main.head;
|
|
91
|
+
|
|
92
|
+
// TODO(burdon): Add copy to clipboard widget.
|
|
93
|
+
for (const { from, to } of view.visibleRanges) {
|
|
94
|
+
syntaxTree(state).iterate({
|
|
95
|
+
enter: (node) => {
|
|
96
|
+
if (node.name === 'FencedCode') {
|
|
97
|
+
const edit = !view.state.readOnly && cursor >= node.from && cursor <= node.to;
|
|
98
|
+
const range = getLineRange(blocks, node.from, node.to);
|
|
99
|
+
for (let i = range[0]; i <= range[1]; i++) {
|
|
100
|
+
const block = blocks[i];
|
|
101
|
+
if (!edit && (i === range[0] || i === range[1])) {
|
|
102
|
+
builder.add(block.from, block.to, Decoration.replace({ widget: i === range[0] ? top : bottom }));
|
|
103
|
+
} else {
|
|
104
|
+
builder.add(
|
|
105
|
+
block.from,
|
|
106
|
+
block.from,
|
|
107
|
+
Decoration.line({
|
|
108
|
+
class: mx('cm-code cm-codeblock'),
|
|
109
|
+
}),
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
},
|
|
115
|
+
from,
|
|
116
|
+
to,
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return builder.finish();
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
export const code = () => {
|
|
124
|
+
return [
|
|
125
|
+
ViewPlugin.fromClass(
|
|
126
|
+
class {
|
|
127
|
+
decorations: DecorationSet;
|
|
128
|
+
constructor(view: EditorView) {
|
|
129
|
+
this.decorations = buildDecorations(view);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
update(update: ViewUpdate) {
|
|
133
|
+
this.decorations = buildDecorations(update.view);
|
|
134
|
+
}
|
|
135
|
+
},
|
|
136
|
+
{
|
|
137
|
+
decorations: (value) => value.decorations,
|
|
138
|
+
},
|
|
139
|
+
),
|
|
140
|
+
styles,
|
|
141
|
+
];
|
|
142
|
+
};
|