@dxos/react-ui-editor 0.3.11-main.c8e9429 → 0.3.11-main.cc2fe2c
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 +1020 -1022
- 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 -14
- package/dist/types/src/components/TextEditor/MarkdownEditor.stories.d.ts.map +1 -1
- package/dist/types/src/components/TextEditor/TextEditor.d.ts +13 -5
- package/dist/types/src/components/TextEditor/TextEditor.d.ts.map +1 -1
- package/dist/types/src/components/TextEditor/TextEditor.stories.d.ts +1 -1
- package/dist/types/src/components/TextEditor/comments.stories.d.ts.map +1 -1
- package/dist/types/src/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/defs.d.ts +6 -8
- 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/awareness.d.ts +1 -1
- 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 +18 -14
- 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/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 +7 -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 +2 -2
- package/dist/types/src/hooks/index.d.ts.map +1 -1
- package/dist/types/src/hooks/useTextEditor.d.ts +17 -0
- package/dist/types/src/hooks/useTextEditor.d.ts.map +1 -0
- package/dist/types/src/hooks/useTextModel.d.ts +2 -27
- package/dist/types/src/hooks/useTextModel.d.ts.map +1 -1
- package/dist/types/src/styles/markdown.d.ts +2 -2
- package/dist/types/src/styles/markdown.d.ts.map +1 -1
- package/dist/types/src/themes/default.d.ts.map +1 -1
- package/dist/types/src/util.d.ts +2 -0
- package/dist/types/src/util.d.ts.map +1 -1
- package/package.json +31 -34
- package/src/components/TextEditor/MarkdownEditor.stories.tsx +69 -72
- package/src/components/TextEditor/TextEditor.tsx +91 -66
- package/src/components/TextEditor/comments.stories.tsx +45 -43
- package/src/extensions/autocomplete.ts +6 -3
- package/src/extensions/automerge/automerge.ts +35 -39
- package/src/extensions/automerge/defs.ts +8 -33
- package/src/extensions/automerge/semaphore.ts +17 -12
- package/src/extensions/automerge/update-automerge.ts +2 -2
- package/src/extensions/awareness.ts +14 -14
- package/src/extensions/basic.ts +22 -17
- package/src/extensions/comments.ts +161 -141
- package/src/extensions/cursor.ts +7 -3
- package/src/extensions/index.ts +1 -7
- package/src/extensions/markdown/bundle.ts +20 -30
- package/src/extensions/markdown/code.ts +127 -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 +7 -0
- package/src/extensions/{link.ts → markdown/link.ts} +1 -2
- package/src/extensions/{tasklist.ts → markdown/tasklist.ts} +48 -34
- 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 +2 -2
- package/src/hooks/useTextEditor.ts +29 -0
- package/src/hooks/useTextModel.ts +47 -56
- package/src/styles/markdown.ts +9 -10
- package/src/themes/default.ts +0 -2
- package/src/util.ts +26 -3
- package/dist/types/src/components/TextEditor/codemirror.stories.d.ts +0 -12
- package/dist/types/src/components/TextEditor/codemirror.stories.d.ts.map +0 -1
- package/dist/types/src/components/TextEditor/yjs.stories.d.ts +0 -12
- package/dist/types/src/components/TextEditor/yjs.stories.d.ts.map +0 -1
- package/dist/types/src/extensions/code.d.ts.map +0 -1
- package/dist/types/src/extensions/hr.d.ts.map +0 -1
- package/dist/types/src/extensions/image.d.ts.map +0 -1
- package/dist/types/src/extensions/link.d.ts.map +0 -1
- package/dist/types/src/extensions/table.d.ts.map +0 -1
- package/dist/types/src/extensions/tasklist.d.ts +0 -7
- package/dist/types/src/extensions/tasklist.d.ts.map +0 -1
- package/dist/types/src/extensions/yjs/cursor.d.ts +0 -4
- package/dist/types/src/extensions/yjs/cursor.d.ts.map +0 -1
- package/dist/types/src/extensions/yjs/index.d.ts +0 -2
- package/dist/types/src/extensions/yjs/index.d.ts.map +0 -1
- package/dist/types/src/extensions/yjs/yjs.d.ts +0 -4
- package/dist/types/src/extensions/yjs/yjs.d.ts.map +0 -1
- package/dist/types/src/extensions/yjs/yjs.test.d.ts +0 -2
- package/dist/types/src/extensions/yjs/yjs.test.d.ts.map +0 -1
- package/dist/types/src/hooks/automerge.d.ts +0 -3
- package/dist/types/src/hooks/automerge.d.ts.map +0 -1
- package/dist/types/src/hooks/yjs.d.ts +0 -27
- package/dist/types/src/hooks/yjs.d.ts.map +0 -1
- package/dist/types/src/testing/index.d.ts +0 -3
- package/dist/types/src/testing/index.d.ts.map +0 -1
- package/dist/types/src/testing/proto/gen/schema.d.ts +0 -15
- package/dist/types/src/testing/proto/gen/schema.d.ts.map +0 -1
- package/dist/types/src/testing/proto/index.d.ts +0 -2
- package/dist/types/src/testing/proto/index.d.ts.map +0 -1
- package/dist/types/src/testing/replicator.d.ts +0 -45
- package/dist/types/src/testing/replicator.d.ts.map +0 -1
- package/src/components/TextEditor/codemirror.stories.ts +0 -115
- package/src/components/TextEditor/yjs.stories.tsx +0 -56
- package/src/extensions/code.ts +0 -104
- package/src/extensions/hr.ts +0 -72
- package/src/extensions/yjs/cursor.ts +0 -22
- package/src/extensions/yjs/index.ts +0 -5
- package/src/extensions/yjs/yjs.test.ts +0 -35
- package/src/extensions/yjs/yjs.ts +0 -16
- package/src/hooks/automerge.ts +0 -52
- package/src/hooks/yjs.ts +0 -156
- package/src/testing/index.ts +0 -6
- package/src/testing/proto/gen/schema.ts +0 -49
- package/src/testing/proto/index.ts +0 -5
- package/src/testing/proto/schema.proto +0 -15
- package/src/testing/replicator.ts +0 -184
- /package/dist/types/src/extensions/{code.d.ts → markdown/code.d.ts} +0 -0
- /package/dist/types/src/extensions/{hr.d.ts → markdown/hr.d.ts} +0 -0
- /package/dist/types/src/extensions/{image.d.ts → markdown/image.d.ts} +0 -0
- /package/dist/types/src/extensions/{link.d.ts → markdown/link.d.ts} +0 -0
- /package/dist/types/src/extensions/{table.d.ts → markdown/table.d.ts} +0 -0
- /package/src/extensions/{image.ts → markdown/image.ts} +0 -0
- /package/src/extensions/{table.ts → markdown/table.ts} +0 -0
|
@@ -17,8 +17,8 @@ import { fixedInsetFlexLayout, mx } from '@dxos/react-ui-theme';
|
|
|
17
17
|
import { withTheme } from '@dxos/storybook-utils';
|
|
18
18
|
|
|
19
19
|
import { MarkdownEditor, TextEditor } from './TextEditor';
|
|
20
|
-
import { comments, type CommentsOptions,
|
|
21
|
-
import { type
|
|
20
|
+
import { comments, type CommentsOptions, setFocus, useComments } from '../../extensions';
|
|
21
|
+
import { type Comment, type Range, useTextModel } from '../../hooks';
|
|
22
22
|
|
|
23
23
|
faker.seed(101);
|
|
24
24
|
|
|
@@ -28,31 +28,34 @@ faker.seed(101);
|
|
|
28
28
|
|
|
29
29
|
const Editor: FC<{
|
|
30
30
|
item: { text: TextObject };
|
|
31
|
-
|
|
32
|
-
|
|
31
|
+
comments: Comment[];
|
|
32
|
+
selected?: string;
|
|
33
33
|
onCreateComment: CommentsOptions['onCreate'];
|
|
34
34
|
onDeleteComment: CommentsOptions['onDelete'];
|
|
35
35
|
onUpdateComment: CommentsOptions['onUpdate'];
|
|
36
36
|
onSelectComment: CommentsOptions['onSelect'];
|
|
37
|
-
}> = ({
|
|
37
|
+
}> = ({
|
|
38
|
+
item,
|
|
39
|
+
selected: selectedValue,
|
|
40
|
+
comments: commentRanges,
|
|
41
|
+
onCreateComment,
|
|
42
|
+
onDeleteComment,
|
|
43
|
+
onUpdateComment,
|
|
44
|
+
onSelectComment,
|
|
45
|
+
}) => {
|
|
38
46
|
const model = useTextModel({ text: item.text });
|
|
39
|
-
const
|
|
47
|
+
const view = useRef<EditorView>(null);
|
|
40
48
|
const [selected, setSelected] = useState<string>();
|
|
41
49
|
useEffect(() => {
|
|
42
|
-
if (!
|
|
43
|
-
|
|
44
|
-
if (
|
|
45
|
-
|
|
46
|
-
const range = Cursor.getRangeFromCursor(editorRef.current!.state.facet(Cursor.converter), cursor);
|
|
47
|
-
if (range) {
|
|
48
|
-
// TODO(burdon): Scroll selection to center of screen?
|
|
49
|
-
editorRef.current?.dispatch({ selection: { anchor: range.from }, scrollIntoView: true });
|
|
50
|
-
}
|
|
50
|
+
if (!view.current?.hasFocus && selectedValue !== selected) {
|
|
51
|
+
setSelected(selectedValue);
|
|
52
|
+
if (selectedValue) {
|
|
53
|
+
setFocus(view.current!, selectedValue);
|
|
51
54
|
}
|
|
52
|
-
|
|
53
|
-
setSelected(commentSelected);
|
|
54
55
|
}
|
|
55
|
-
}, [selected, commentRanges,
|
|
56
|
+
}, [selected, commentRanges, selectedValue]);
|
|
57
|
+
|
|
58
|
+
useComments(view.current, commentRanges);
|
|
56
59
|
|
|
57
60
|
const extensions = useMemo(() => {
|
|
58
61
|
return [
|
|
@@ -72,7 +75,7 @@ const Editor: FC<{
|
|
|
72
75
|
// TODO(burdon): Highlight currently selected comment.
|
|
73
76
|
return (
|
|
74
77
|
<div className='flex grow overflow-y-scroll'>
|
|
75
|
-
<MarkdownEditor ref={
|
|
78
|
+
<MarkdownEditor ref={view} model={model} extensions={extensions} />
|
|
76
79
|
</div>
|
|
77
80
|
);
|
|
78
81
|
};
|
|
@@ -83,11 +86,10 @@ const Editor: FC<{
|
|
|
83
86
|
|
|
84
87
|
type CommentThread = {
|
|
85
88
|
id: string;
|
|
86
|
-
|
|
89
|
+
cursor?: string;
|
|
90
|
+
range?: Range;
|
|
87
91
|
yPos?: number;
|
|
88
|
-
selection?: Range;
|
|
89
92
|
messages: TextObject[];
|
|
90
|
-
deleted?: boolean;
|
|
91
93
|
};
|
|
92
94
|
|
|
93
95
|
const Thread: FC<{
|
|
@@ -129,16 +131,16 @@ const Thread: FC<{
|
|
|
129
131
|
className={mx(
|
|
130
132
|
'flex flex-col m-1 rounded shadow divide-y bg-white',
|
|
131
133
|
selected && 'ring',
|
|
132
|
-
thread.
|
|
134
|
+
!thread.cursor && 'opacity-50',
|
|
133
135
|
)}
|
|
134
136
|
>
|
|
135
137
|
<div className='flex p-2 gap-2 items-center text-xs font-mono text-neutral-500 font-thin'>
|
|
136
138
|
<span>id:{thread.id.slice(0, 4)}</span>
|
|
137
|
-
<span>from:{thread.
|
|
138
|
-
<span>to:{thread.
|
|
139
|
+
<span>from:{thread.range?.from}</span>
|
|
140
|
+
<span>to:{thread.range?.to}</span>
|
|
139
141
|
<span>y:{thread.yPos}</span>
|
|
140
142
|
<span className='grow' />
|
|
141
|
-
{thread.
|
|
143
|
+
{!thread.cursor && <Trash />}
|
|
142
144
|
</div>
|
|
143
145
|
|
|
144
146
|
{thread.messages.map((message, i) => (
|
|
@@ -151,10 +153,10 @@ const Thread: FC<{
|
|
|
151
153
|
<div ref={containerRef} onClick={() => onSelect()} className='flex'>
|
|
152
154
|
<TextEditor
|
|
153
155
|
ref={editorRef}
|
|
154
|
-
|
|
156
|
+
autoFocus={focus}
|
|
155
157
|
model={model}
|
|
156
158
|
placeholder={'Enter comment...'}
|
|
157
|
-
slots={{ root: { className: 'grow
|
|
159
|
+
slots={{ root: { className: 'grow rounded-b' } }}
|
|
158
160
|
extensions={[
|
|
159
161
|
keymap.of([
|
|
160
162
|
{
|
|
@@ -218,9 +220,10 @@ type StoryProps = {
|
|
|
218
220
|
const Story = ({ text, autoCreate }: StoryProps) => {
|
|
219
221
|
const [item] = useState({ text: new TextObject(text) });
|
|
220
222
|
const [threads, setThreads] = useState<CommentThread[]>([]);
|
|
221
|
-
const commentRanges = useMemo(() => threads.map((thread) => thread.range), [threads]);
|
|
222
223
|
const [selected, setSelected] = useState<string>();
|
|
223
224
|
|
|
225
|
+
const comments = useMemo<Comment[]>(() => threads.map(({ id, cursor }) => ({ id, cursor })), [threads]);
|
|
226
|
+
|
|
224
227
|
// Filter by visibility.
|
|
225
228
|
const visibleThreads = useMemo(() => threads.filter((thread) => thread.yPos !== undefined), [threads]);
|
|
226
229
|
|
|
@@ -231,14 +234,14 @@ const Story = ({ text, autoCreate }: StoryProps) => {
|
|
|
231
234
|
...threads,
|
|
232
235
|
{
|
|
233
236
|
id,
|
|
234
|
-
|
|
237
|
+
cursor,
|
|
235
238
|
yPos: location ? Math.floor(location.top) : undefined,
|
|
236
239
|
messages: autoCreate
|
|
237
240
|
? faker.helpers.multiple(() => new TextObject(faker.lorem.sentence()), { count: { min: 1, max: 3 } })
|
|
238
241
|
: [],
|
|
239
242
|
},
|
|
240
243
|
]);
|
|
241
|
-
setSelected(id);
|
|
244
|
+
setSelected(id); // TODO(burdon): Not required.
|
|
242
245
|
return id;
|
|
243
246
|
};
|
|
244
247
|
|
|
@@ -247,7 +250,7 @@ const Story = ({ text, autoCreate }: StoryProps) => {
|
|
|
247
250
|
setThreads((threads) =>
|
|
248
251
|
threads.map((thread) => {
|
|
249
252
|
if (thread.id === id) {
|
|
250
|
-
thread.
|
|
253
|
+
thread.cursor = undefined;
|
|
251
254
|
}
|
|
252
255
|
|
|
253
256
|
return thread;
|
|
@@ -262,8 +265,7 @@ const Story = ({ text, autoCreate }: StoryProps) => {
|
|
|
262
265
|
setThreads((threads) =>
|
|
263
266
|
threads.map((thread) => {
|
|
264
267
|
if (thread.id === id) {
|
|
265
|
-
thread.
|
|
266
|
-
delete thread.deleted;
|
|
268
|
+
thread.cursor = cursor;
|
|
267
269
|
}
|
|
268
270
|
|
|
269
271
|
return thread;
|
|
@@ -271,21 +273,21 @@ const Story = ({ text, autoCreate }: StoryProps) => {
|
|
|
271
273
|
);
|
|
272
274
|
};
|
|
273
275
|
|
|
274
|
-
const handleSelectComment: CommentsOptions['onSelect'] = ({
|
|
275
|
-
log.info('select', { active:
|
|
276
|
+
const handleSelectComment: CommentsOptions['onSelect'] = ({ comments, selection: { current, closest } }) => {
|
|
277
|
+
log.info('select', { active: current?.slice(0, 4), closest: closest?.slice(0, 4) });
|
|
276
278
|
setThreads((threads) =>
|
|
277
279
|
threads.map((thread) => {
|
|
278
|
-
const
|
|
279
|
-
if (
|
|
280
|
-
thread.yPos =
|
|
281
|
-
thread.
|
|
280
|
+
const comment = comments.find(({ comment }) => comment.id === thread.id);
|
|
281
|
+
if (comment) {
|
|
282
|
+
thread.yPos = comment.location ? Math.floor(comment.location.top) : undefined;
|
|
283
|
+
thread.range = { from: comment.range.from, to: comment.range.to };
|
|
282
284
|
}
|
|
283
285
|
|
|
284
286
|
return thread;
|
|
285
287
|
}),
|
|
286
288
|
);
|
|
287
289
|
|
|
288
|
-
setSelected(
|
|
290
|
+
setSelected(current ?? closest);
|
|
289
291
|
};
|
|
290
292
|
|
|
291
293
|
const handleSelectThread = (id: string) => {
|
|
@@ -303,8 +305,8 @@ const Story = ({ text, autoCreate }: StoryProps) => {
|
|
|
303
305
|
<div className='flex flex-col h-full w-[600px]'>
|
|
304
306
|
<Editor
|
|
305
307
|
item={item}
|
|
306
|
-
|
|
307
|
-
|
|
308
|
+
selected={selected}
|
|
309
|
+
comments={comments}
|
|
308
310
|
onCreateComment={handleCreateComment}
|
|
309
311
|
onDeleteComment={handleDeleteComment}
|
|
310
312
|
onUpdateComment={handleUpdateComment}
|
|
@@ -35,10 +35,13 @@ export const autocomplete = ({ onSearch }: AutocompleteOptions) => {
|
|
|
35
35
|
// https://codemirror.net/examples/autocompletion
|
|
36
36
|
// https://codemirror.net/docs/ref/#autocomplete.autocompletion
|
|
37
37
|
autocompletion({
|
|
38
|
-
|
|
38
|
+
activateOnTyping: true,
|
|
39
39
|
|
|
40
|
-
//
|
|
41
|
-
|
|
40
|
+
// closeOnBlur: false,
|
|
41
|
+
// defaultKeymap: false,
|
|
42
|
+
|
|
43
|
+
// TODO(burdon): Styles/fragments.
|
|
44
|
+
tooltipClass: () => 'shadow rounded',
|
|
42
45
|
}),
|
|
43
46
|
|
|
44
47
|
// TODO(burdon): Option to create new page?
|
|
@@ -4,14 +4,14 @@
|
|
|
4
4
|
// Ref: https://github.com/automerge/automerge-codemirror
|
|
5
5
|
//
|
|
6
6
|
|
|
7
|
-
import { StateField, type Extension
|
|
8
|
-
import {
|
|
7
|
+
import { StateField, type Extension } from '@codemirror/state';
|
|
8
|
+
import { EditorView, ViewPlugin } from '@codemirror/view';
|
|
9
9
|
|
|
10
|
-
import { next as A } from '@dxos/automerge/automerge';
|
|
11
|
-
import {
|
|
10
|
+
import { type Prop, next as A } from '@dxos/automerge/automerge';
|
|
11
|
+
import { invariant } from '@dxos/invariant';
|
|
12
12
|
|
|
13
13
|
import { cursorConverter } from './cursor';
|
|
14
|
-
import { effectType, type IDocHandle, isReconcileTx,
|
|
14
|
+
import { effectType, type IDocHandle, isReconcileTx, type State } from './defs';
|
|
15
15
|
import { PatchSemaphore } from './semaphore';
|
|
16
16
|
import { Cursor } from '../cursor';
|
|
17
17
|
|
|
@@ -21,18 +21,18 @@ export type AutomergeOptions = {
|
|
|
21
21
|
};
|
|
22
22
|
|
|
23
23
|
export const automerge = ({ handle, path }: AutomergeOptions): Extension => {
|
|
24
|
-
const
|
|
24
|
+
const state = StateField.define<State>({
|
|
25
25
|
create: () => ({
|
|
26
|
+
path: path.slice(),
|
|
26
27
|
lastHeads: A.getHeads(handle.docSync()!),
|
|
27
28
|
unreconciledTransactions: [],
|
|
28
|
-
path: path.slice(),
|
|
29
29
|
}),
|
|
30
30
|
|
|
31
|
-
update: (value
|
|
32
|
-
const result:
|
|
31
|
+
update: (value, tr) => {
|
|
32
|
+
const result: State = {
|
|
33
|
+
path: path.slice(),
|
|
33
34
|
lastHeads: value.lastHeads,
|
|
34
35
|
unreconciledTransactions: value.unreconciledTransactions.slice(),
|
|
35
|
-
path: path.slice(),
|
|
36
36
|
};
|
|
37
37
|
|
|
38
38
|
let clearUnreconciled = false;
|
|
@@ -55,38 +55,34 @@ export const automerge = ({ handle, path }: AutomergeOptions): Extension => {
|
|
|
55
55
|
},
|
|
56
56
|
});
|
|
57
57
|
|
|
58
|
-
const semaphore = new PatchSemaphore(handle,
|
|
58
|
+
const semaphore = new PatchSemaphore(handle, state);
|
|
59
59
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
60
|
+
return [
|
|
61
|
+
Cursor.converter.of(cursorConverter(handle, path)),
|
|
62
|
+
// Reconcile external updates.
|
|
63
|
+
ViewPlugin.fromClass(
|
|
64
|
+
class {
|
|
65
|
+
constructor(private readonly _view: EditorView) {
|
|
66
|
+
invariant(this._view);
|
|
67
|
+
handle.addListener('change', this._handleChange.bind(this));
|
|
68
|
+
}
|
|
65
69
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
queueMicrotask(() => {
|
|
69
|
-
this._view.state.facet(semaphoreFacet).reconcile(this._view);
|
|
70
|
-
});
|
|
70
|
+
destroy() {
|
|
71
|
+
handle.removeListener('change', this._handleChange.bind(this));
|
|
71
72
|
}
|
|
72
|
-
}
|
|
73
73
|
|
|
74
|
-
|
|
75
|
-
|
|
74
|
+
_handleChange() {
|
|
75
|
+
semaphore.reconcile(this._view);
|
|
76
|
+
}
|
|
77
|
+
},
|
|
78
|
+
),
|
|
79
|
+
// Reconcile local updates.
|
|
80
|
+
EditorView.updateListener.of(({ view, changes }) => {
|
|
81
|
+
if (!changes.empty) {
|
|
82
|
+
// TODO(burdon): Loses cursor position if auto closing brackets. Call explicitly?
|
|
83
|
+
semaphore.reconcile(view);
|
|
76
84
|
}
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
};
|
|
81
|
-
},
|
|
82
|
-
);
|
|
83
|
-
|
|
84
|
-
return {
|
|
85
|
-
extension: [
|
|
86
|
-
Cursor.converter.of(cursorConverter(handle, path)),
|
|
87
|
-
semaphoreFacet.of(semaphore),
|
|
88
|
-
stateField,
|
|
89
|
-
viewPlugin,
|
|
90
|
-
],
|
|
91
|
-
};
|
|
85
|
+
}),
|
|
86
|
+
state,
|
|
87
|
+
];
|
|
92
88
|
};
|
|
@@ -4,15 +4,14 @@
|
|
|
4
4
|
// Ref: https://github.com/automerge/automerge-codemirror
|
|
5
5
|
//
|
|
6
6
|
|
|
7
|
-
import { Annotation, StateEffect, type StateField, type EditorState, type Transaction
|
|
7
|
+
import { Annotation, StateEffect, type StateField, type EditorState, type Transaction } from '@codemirror/state';
|
|
8
8
|
|
|
9
9
|
import { type ChangeFn, type ChangeOptions, type Doc, type Heads, type Prop } from '@dxos/automerge/automerge';
|
|
10
10
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
export type Value = {
|
|
14
|
-
lastHeads: Heads;
|
|
11
|
+
// TODO(burdon): Rename.
|
|
12
|
+
export type State = {
|
|
15
13
|
path: Prop[];
|
|
14
|
+
lastHeads: Heads;
|
|
16
15
|
unreconciledTransactions: Transaction[];
|
|
17
16
|
};
|
|
18
17
|
|
|
@@ -22,39 +21,15 @@ export type UpdateHeads = {
|
|
|
22
21
|
|
|
23
22
|
export const effectType = StateEffect.define<UpdateHeads>({});
|
|
24
23
|
|
|
25
|
-
export const updateHeads = (newHeads: Heads): StateEffect<UpdateHeads> => effectType.of({ newHeads });
|
|
26
|
-
|
|
27
|
-
export const getLastHeads = (state: EditorState, field: StateField<Value>): Heads => state.field(field).lastHeads;
|
|
28
|
-
|
|
29
|
-
export const getPath = (state: EditorState, field: StateField<Value>): Prop[] => state.field(field).path;
|
|
30
|
-
|
|
31
24
|
export const reconcileAnnotationType = Annotation.define<unknown>();
|
|
32
25
|
|
|
33
|
-
export const
|
|
26
|
+
export const getPath = (state: EditorState, field: StateField<State>): Prop[] => state.field(field).path;
|
|
34
27
|
|
|
35
|
-
|
|
36
|
-
export const semaphoreFacet = Facet.define<PatchSemaphore, PatchSemaphore>({
|
|
37
|
-
combine: (values) => values.at(-1)!, // Take last.
|
|
38
|
-
});
|
|
28
|
+
export const getLastHeads = (state: EditorState, field: StateField<State>): Heads => state.field(field).lastHeads;
|
|
39
29
|
|
|
40
|
-
|
|
41
|
-
export const makeReconcile = (tr: TransactionSpec) => {
|
|
42
|
-
if (tr.annotations != null) {
|
|
43
|
-
if (tr.annotations instanceof Array) {
|
|
44
|
-
tr.annotations = [...tr.annotations, reconcileAnnotationType.of({})];
|
|
45
|
-
} else {
|
|
46
|
-
tr.annotations = [tr.annotations, reconcileAnnotationType.of({})];
|
|
47
|
-
}
|
|
48
|
-
} else {
|
|
49
|
-
tr.annotations = [reconcileAnnotationType.of({})];
|
|
50
|
-
}
|
|
30
|
+
export const updateHeads = (newHeads: Heads): StateEffect<UpdateHeads> => effectType.of({ newHeads });
|
|
51
31
|
|
|
52
|
-
|
|
53
|
-
// ...tr,
|
|
54
|
-
// annotations: reconcileAnnotationType.of({})
|
|
55
|
-
// }
|
|
56
|
-
};
|
|
57
|
-
*/
|
|
32
|
+
export const isReconcileTx = (tr: Transaction): boolean => !!tr.annotation(reconcileAnnotationType);
|
|
58
33
|
|
|
59
34
|
export type IDocHandle<T = any> = {
|
|
60
35
|
docSync(): Doc<T> | undefined;
|
|
@@ -16,33 +16,38 @@ import {
|
|
|
16
16
|
reconcileAnnotationType,
|
|
17
17
|
updateHeads,
|
|
18
18
|
type IDocHandle,
|
|
19
|
-
type
|
|
19
|
+
type State,
|
|
20
20
|
} from './defs';
|
|
21
21
|
import { updateAutomerge } from './update-automerge';
|
|
22
22
|
import { updateCodeMirror } from './update-codemirror';
|
|
23
23
|
|
|
24
24
|
/**
|
|
25
|
-
* Implements three-way merge (on each mutation/
|
|
25
|
+
* Implements three-way merge (on each mutation/keystroke).
|
|
26
26
|
*/
|
|
27
27
|
export class PatchSemaphore {
|
|
28
|
-
_inReconcile = false;
|
|
28
|
+
private _inReconcile = false;
|
|
29
29
|
|
|
30
|
-
|
|
30
|
+
// prettier-ignore
|
|
31
|
+
constructor(
|
|
32
|
+
private readonly _handle: IDocHandle,
|
|
33
|
+
private readonly _state: StateField<State>
|
|
34
|
+
) {}
|
|
31
35
|
|
|
32
|
-
|
|
36
|
+
// NOTE: Cannot destruct view.state.
|
|
37
|
+
reconcile(view: EditorView) {
|
|
33
38
|
if (this._inReconcile) {
|
|
34
39
|
return;
|
|
35
40
|
}
|
|
36
41
|
this._inReconcile = true;
|
|
37
42
|
|
|
38
|
-
const path = getPath(view.state, this.
|
|
43
|
+
const path = getPath(view.state, this._state);
|
|
39
44
|
|
|
40
45
|
// Get the heads before the unreconciled transactions are applied.
|
|
41
|
-
const oldHeads = getLastHeads(view.state, this.
|
|
46
|
+
const oldHeads = getLastHeads(view.state, this._state);
|
|
42
47
|
let selection = view.state.selection;
|
|
43
48
|
|
|
44
49
|
// First undo all the unreconciled transactions.
|
|
45
|
-
const transactions = view.state.field(this.
|
|
50
|
+
const transactions = view.state.field(this._state).unreconciledTransactions.filter((tx) => !isReconcileTx(tx));
|
|
46
51
|
const toInvert = transactions.slice().reverse();
|
|
47
52
|
for (const tx of toInvert) {
|
|
48
53
|
const inverted = tx.changes.invert(tx.startState.doc);
|
|
@@ -54,11 +59,10 @@ export class PatchSemaphore {
|
|
|
54
59
|
}
|
|
55
60
|
|
|
56
61
|
// Apply the unreconciled transactions to the document.
|
|
57
|
-
let newHeads = updateAutomerge(this._field, this._handle, transactions, view.state);
|
|
58
|
-
|
|
59
62
|
// NOTE: null and undefined each come from automerge and repo respectively.
|
|
63
|
+
let newHeads = updateAutomerge(this._state, this._handle, transactions, view.state);
|
|
60
64
|
if (newHeads === null || newHeads === undefined) {
|
|
61
|
-
// TODO:
|
|
65
|
+
// TODO(alexjg): this is the call that's resetting the editor state on click.
|
|
62
66
|
newHeads = automerge.getHeads(this._handle.docSync()!);
|
|
63
67
|
}
|
|
64
68
|
|
|
@@ -68,11 +72,12 @@ export class PatchSemaphore {
|
|
|
68
72
|
: automerge.diff(this._handle.docSync()!, oldHeads, newHeads);
|
|
69
73
|
updateCodeMirror(view, selection, path, diff);
|
|
70
74
|
|
|
75
|
+
// Update automerge state.
|
|
71
76
|
view.dispatch({
|
|
72
77
|
effects: updateHeads(newHeads),
|
|
73
78
|
annotations: reconcileAnnotationType.of({}),
|
|
74
79
|
});
|
|
75
80
|
|
|
76
81
|
this._inReconcile = false;
|
|
77
|
-
}
|
|
82
|
+
}
|
|
78
83
|
}
|
|
@@ -8,10 +8,10 @@ import { type EditorState, type StateField, type Text, type Transaction } from '
|
|
|
8
8
|
|
|
9
9
|
import { next as am, type Heads } from '@dxos/automerge/automerge';
|
|
10
10
|
|
|
11
|
-
import { type IDocHandle, type
|
|
11
|
+
import { type IDocHandle, type State } from './defs';
|
|
12
12
|
|
|
13
13
|
export const updateAutomerge = (
|
|
14
|
-
field: StateField<
|
|
14
|
+
field: StateField<State>,
|
|
15
15
|
handle: IDocHandle,
|
|
16
16
|
transactions: Transaction[],
|
|
17
17
|
state: EditorState,
|
|
@@ -24,8 +24,8 @@ export interface AwarenessProvider {
|
|
|
24
24
|
open(): void;
|
|
25
25
|
close(): void;
|
|
26
26
|
|
|
27
|
-
update(position: AwarenessPosition | undefined): void;
|
|
28
27
|
getRemoteStates(): AwarenessState[];
|
|
28
|
+
update(position: AwarenessPosition | undefined): void;
|
|
29
29
|
}
|
|
30
30
|
|
|
31
31
|
const dummyProvider: AwarenessProvider = {
|
|
@@ -34,8 +34,8 @@ const dummyProvider: AwarenessProvider = {
|
|
|
34
34
|
open: () => {},
|
|
35
35
|
close: () => {},
|
|
36
36
|
|
|
37
|
-
update: () => {},
|
|
38
37
|
getRemoteStates: () => [],
|
|
38
|
+
update: () => {},
|
|
39
39
|
};
|
|
40
40
|
|
|
41
41
|
export const AwarenessProvider = Facet.define<AwarenessProvider, AwarenessProvider>({
|
|
@@ -66,7 +66,6 @@ export type AwarenessState = {
|
|
|
66
66
|
/**
|
|
67
67
|
* Extension provides presence information about other peers.
|
|
68
68
|
*/
|
|
69
|
-
// TODO(burdon): Why provide default?
|
|
70
69
|
export const awareness = (provider = dummyProvider): Extension => {
|
|
71
70
|
return [
|
|
72
71
|
AwarenessProvider.of(provider),
|
|
@@ -251,7 +250,6 @@ class RemoteCaretWidget extends WidgetType {
|
|
|
251
250
|
}
|
|
252
251
|
}
|
|
253
252
|
|
|
254
|
-
// TODO(burdon): Mixed case?
|
|
255
253
|
const styles = EditorView.baseTheme({
|
|
256
254
|
'.cm-collab-selection': {},
|
|
257
255
|
'.cm-collab-selectionLine': {
|
|
@@ -266,38 +264,40 @@ const styles = EditorView.baseTheme({
|
|
|
266
264
|
marginRight: '-1px',
|
|
267
265
|
boxSizing: 'border-box',
|
|
268
266
|
display: 'inline',
|
|
267
|
+
cursor: 'pointer',
|
|
269
268
|
},
|
|
270
269
|
'.cm-collab-selectionCaretDot': {
|
|
271
270
|
borderRadius: '50%',
|
|
272
271
|
position: 'absolute',
|
|
273
|
-
width: '.
|
|
274
|
-
height: '.
|
|
275
|
-
top: '-.
|
|
276
|
-
left: '-.
|
|
272
|
+
width: '.5em',
|
|
273
|
+
height: '.5em',
|
|
274
|
+
top: '-.25em',
|
|
275
|
+
left: '-.25em',
|
|
277
276
|
backgroundColor: 'inherit',
|
|
278
277
|
transition: 'transform .3s ease-in-out',
|
|
279
278
|
boxSizing: 'border-box',
|
|
280
279
|
},
|
|
281
280
|
'.cm-collab-selectionCaret:hover > .cm-collab-selectionCaretDot': {
|
|
282
|
-
transformOrigin: 'bottom center',
|
|
283
281
|
transform: 'scale(0)',
|
|
282
|
+
transformOrigin: 'center',
|
|
284
283
|
},
|
|
285
284
|
'.cm-collab-selectionInfo': {
|
|
286
285
|
position: 'absolute',
|
|
287
|
-
|
|
288
|
-
|
|
286
|
+
transform: 'translate(-50%, 0)',
|
|
287
|
+
top: '-20px',
|
|
288
|
+
left: 0,
|
|
289
289
|
fontSize: '.75em',
|
|
290
|
-
fontFamily: 'serif',
|
|
290
|
+
fontFamily: 'sans-serif',
|
|
291
291
|
fontStyle: 'normal',
|
|
292
292
|
fontWeight: 'normal',
|
|
293
293
|
lineHeight: 'normal',
|
|
294
294
|
userSelect: 'none',
|
|
295
295
|
color: 'white',
|
|
296
|
-
|
|
297
|
-
paddingRight: '2px',
|
|
296
|
+
padding: '2px',
|
|
298
297
|
zIndex: 101,
|
|
299
298
|
transition: 'opacity .3s ease-in-out',
|
|
300
299
|
backgroundColor: 'inherit',
|
|
300
|
+
borderRadius: '2px',
|
|
301
301
|
// These should be separate.
|
|
302
302
|
opacity: 0,
|
|
303
303
|
transitionDelay: '0s',
|
package/src/extensions/basic.ts
CHANGED
|
@@ -3,30 +3,35 @@
|
|
|
3
3
|
//
|
|
4
4
|
|
|
5
5
|
import { closeBrackets } from '@codemirror/autocomplete';
|
|
6
|
+
import { history } from '@codemirror/commands';
|
|
6
7
|
import { bracketMatching, defaultHighlightStyle, syntaxHighlighting } from '@codemirror/language';
|
|
7
8
|
import { type Extension } from '@codemirror/state';
|
|
8
9
|
import { oneDarkHighlightStyle } from '@codemirror/theme-one-dark';
|
|
9
|
-
import { drawSelection, EditorView, placeholder } from '@codemirror/view';
|
|
10
|
+
import { drawSelection, EditorView, highlightActiveLine, placeholder } from '@codemirror/view';
|
|
10
11
|
|
|
11
|
-
import type
|
|
12
|
+
import { type ThemeMode } from '@dxos/react-ui';
|
|
13
|
+
|
|
14
|
+
import { type TextEditorProps } from '../components';
|
|
12
15
|
|
|
13
16
|
export type BasicBundleOptions = {
|
|
14
|
-
readonly?: boolean;
|
|
15
17
|
themeMode?: ThemeMode;
|
|
16
|
-
|
|
17
|
-
};
|
|
18
|
+
} & Pick<TextEditorProps, 'placeholder' | 'lineWrapping'>;
|
|
18
19
|
|
|
19
|
-
export const basicBundle = ({
|
|
20
|
-
|
|
21
|
-
|
|
20
|
+
export const basicBundle = ({
|
|
21
|
+
themeMode,
|
|
22
|
+
placeholder: _placeholder,
|
|
23
|
+
lineWrapping = true,
|
|
24
|
+
}: BasicBundleOptions): Extension[] => [
|
|
25
|
+
lineWrapping ? EditorView.lineWrapping : [],
|
|
22
26
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
27
|
+
// https://codemirror.net/docs/ref/#codemirror.minimalSetup
|
|
28
|
+
bracketMatching(),
|
|
29
|
+
closeBrackets(),
|
|
30
|
+
drawSelection(),
|
|
31
|
+
highlightActiveLine(),
|
|
32
|
+
history(),
|
|
33
|
+
_placeholder ? placeholder(_placeholder) : [],
|
|
29
34
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
35
|
+
// https://github.com/codemirror/theme-one-dark
|
|
36
|
+
themeMode === 'dark' ? syntaxHighlighting(oneDarkHighlightStyle) : syntaxHighlighting(defaultHighlightStyle),
|
|
37
|
+
];
|