@dxos/react-ui-editor 0.3.11-main.00a28cb → 0.3.11-main.08776d7
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 +258 -252
- package/dist/lib/browser/index.mjs.map +4 -4
- package/dist/lib/browser/meta.json +1 -1
- package/dist/types/src/components/TextEditor/TextEditor.d.ts +4 -2
- package/dist/types/src/components/TextEditor/TextEditor.d.ts.map +1 -1
- package/dist/types/src/components/TextEditor/comments.stories.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 +5 -7
- package/dist/types/src/extensions/automerge/defs.d.ts.map +1 -1
- package/dist/types/src/extensions/automerge/semaphore.d.ts +5 -4
- package/dist/types/src/extensions/automerge/semaphore.d.ts.map +1 -1
- package/dist/types/src/extensions/comments.d.ts +12 -12
- 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 -0
- package/dist/types/src/extensions/index.d.ts.map +1 -1
- package/dist/types/src/extensions/sync.d.ts +7 -0
- package/dist/types/src/extensions/sync.d.ts.map +1 -0
- package/dist/types/src/hooks/automerge.d.ts +2 -1
- package/dist/types/src/hooks/automerge.d.ts.map +1 -1
- package/dist/types/src/hooks/defs.d.ts +28 -0
- package/dist/types/src/hooks/defs.d.ts.map +1 -0
- package/dist/types/src/hooks/index.d.ts +1 -0
- package/dist/types/src/hooks/index.d.ts.map +1 -1
- 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/hooks/yjs.d.ts +2 -1
- package/dist/types/src/hooks/yjs.d.ts.map +1 -1
- package/package.json +23 -23
- package/src/components/TextEditor/MarkdownEditor.stories.tsx +5 -5
- package/src/components/TextEditor/TextEditor.tsx +46 -38
- package/src/components/TextEditor/comments.stories.tsx +35 -28
- package/src/extensions/automerge/automerge.ts +10 -39
- package/src/extensions/automerge/defs.ts +8 -13
- package/src/extensions/automerge/semaphore.ts +14 -7
- package/src/extensions/comments.ts +137 -126
- package/src/extensions/cursor.ts +7 -3
- package/src/extensions/index.ts +1 -0
- package/src/extensions/sync.ts +15 -0
- package/src/hooks/automerge.ts +2 -1
- package/src/hooks/defs.ts +47 -0
- package/src/hooks/index.ts +1 -0
- package/src/hooks/useTextModel.ts +2 -45
- package/src/hooks/yjs.ts +2 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dxos/react-ui-editor",
|
|
3
|
-
"version": "0.3.11-main.
|
|
3
|
+
"version": "0.3.11-main.08776d7",
|
|
4
4
|
"description": "Document editing experience within a DXOS shell.",
|
|
5
5
|
"homepage": "https://dxos.org",
|
|
6
6
|
"bugs": "https://github.com/dxos/dxos/issues",
|
|
@@ -38,20 +38,20 @@
|
|
|
38
38
|
"y-codemirror.next": "^0.3.2",
|
|
39
39
|
"y-protocols": "^1.0.5",
|
|
40
40
|
"yjs": "^13.6.10",
|
|
41
|
-
"@dxos/async": "0.3.11-main.
|
|
42
|
-
"@dxos/context": "0.3.11-main.
|
|
43
|
-
"@dxos/
|
|
44
|
-
"@dxos/
|
|
45
|
-
"@dxos/
|
|
46
|
-
"@dxos/
|
|
47
|
-
"@dxos/
|
|
48
|
-
"@dxos/log": "0.3.11-main.
|
|
49
|
-
"@dxos/protocols": "0.3.11-main.
|
|
50
|
-
"@dxos/react-async": "0.3.11-main.
|
|
51
|
-
"@dxos/
|
|
52
|
-
"@dxos/react-ui
|
|
53
|
-
"@dxos/
|
|
54
|
-
"@dxos/util": "0.3.11-main.
|
|
41
|
+
"@dxos/async": "0.3.11-main.08776d7",
|
|
42
|
+
"@dxos/context": "0.3.11-main.08776d7",
|
|
43
|
+
"@dxos/debug": "0.3.11-main.08776d7",
|
|
44
|
+
"@dxos/automerge": "0.3.11-main.08776d7",
|
|
45
|
+
"@dxos/display-name": "0.3.11-main.08776d7",
|
|
46
|
+
"@dxos/echo-schema": "0.3.11-main.08776d7",
|
|
47
|
+
"@dxos/invariant": "0.3.11-main.08776d7",
|
|
48
|
+
"@dxos/log": "0.3.11-main.08776d7",
|
|
49
|
+
"@dxos/protocols": "0.3.11-main.08776d7",
|
|
50
|
+
"@dxos/react-async": "0.3.11-main.08776d7",
|
|
51
|
+
"@dxos/text-model": "0.3.11-main.08776d7",
|
|
52
|
+
"@dxos/react-ui": "0.3.11-main.08776d7",
|
|
53
|
+
"@dxos/react-ui-theme": "0.3.11-main.08776d7",
|
|
54
|
+
"@dxos/util": "0.3.11-main.08776d7"
|
|
55
55
|
},
|
|
56
56
|
"devDependencies": {
|
|
57
57
|
"@automerge/automerge-repo-network-broadcastchannel": "^1.0.19",
|
|
@@ -67,19 +67,19 @@
|
|
|
67
67
|
"deepsignal": "1.4.0-shallow.0",
|
|
68
68
|
"react": "^18.2.0",
|
|
69
69
|
"react-dom": "^18.2.0",
|
|
70
|
-
"@dxos/config": "0.3.11-main.
|
|
71
|
-
"@dxos/echo-signals": "0.3.11-main.
|
|
72
|
-
"@dxos/echo-typegen": "0.3.11-main.
|
|
73
|
-
"@dxos/
|
|
74
|
-
"@dxos/react-client": "0.3.11-main.
|
|
75
|
-
"@dxos/
|
|
76
|
-
"@dxos/storybook-utils": "0.3.11-main.
|
|
70
|
+
"@dxos/config": "0.3.11-main.08776d7",
|
|
71
|
+
"@dxos/echo-signals": "0.3.11-main.08776d7",
|
|
72
|
+
"@dxos/echo-typegen": "0.3.11-main.08776d7",
|
|
73
|
+
"@dxos/react-ui": "0.3.11-main.08776d7",
|
|
74
|
+
"@dxos/react-client": "0.3.11-main.08776d7",
|
|
75
|
+
"@dxos/keyboard": "0.3.11-main.08776d7",
|
|
76
|
+
"@dxos/storybook-utils": "0.3.11-main.08776d7"
|
|
77
77
|
},
|
|
78
78
|
"peerDependencies": {
|
|
79
79
|
"@phosphor-icons/react": "^2.0.5",
|
|
80
80
|
"react": "^18.2.0",
|
|
81
81
|
"react-dom": "^18.2.0",
|
|
82
|
-
"@dxos/react-client": "0.3.11-main.
|
|
82
|
+
"@dxos/react-client": "0.3.11-main.08776d7"
|
|
83
83
|
},
|
|
84
84
|
"publishConfig": {
|
|
85
85
|
"access": "public"
|
|
@@ -35,7 +35,7 @@ import {
|
|
|
35
35
|
type LinkOptions,
|
|
36
36
|
outliner,
|
|
37
37
|
} from '../../extensions';
|
|
38
|
-
import { type
|
|
38
|
+
import { type Comment, useTextModel } from '../../hooks';
|
|
39
39
|
|
|
40
40
|
// Extensions:
|
|
41
41
|
// TODO(burdon): Table of contents.
|
|
@@ -347,7 +347,7 @@ export const Mention = {
|
|
|
347
347
|
|
|
348
348
|
export const Comments = {
|
|
349
349
|
render: () => {
|
|
350
|
-
const [commentRanges, setCommentRanges] = useState<
|
|
350
|
+
const [commentRanges, setCommentRanges] = useState<Comment[]>([]);
|
|
351
351
|
|
|
352
352
|
return (
|
|
353
353
|
<Story
|
|
@@ -367,9 +367,9 @@ export const Comments = {
|
|
|
367
367
|
console.log(
|
|
368
368
|
'update',
|
|
369
369
|
JSON.stringify({
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
370
|
+
comments: state.comments.length,
|
|
371
|
+
active: state.selection.active?.slice(0, 8),
|
|
372
|
+
closest: state.selection.closest?.slice(0, 8),
|
|
373
373
|
}),
|
|
374
374
|
);
|
|
375
375
|
}
|
|
@@ -22,8 +22,8 @@ import { generateName } from '@dxos/display-name';
|
|
|
22
22
|
import { useThemeContext } from '@dxos/react-ui';
|
|
23
23
|
import { getColorForValue, inputSurface, mx } from '@dxos/react-ui-theme';
|
|
24
24
|
|
|
25
|
-
import { basicBundle, markdownBundle,
|
|
26
|
-
import { type
|
|
25
|
+
import { basicBundle, markdownBundle, setComments, syncFacet } from '../../extensions';
|
|
26
|
+
import { type Comment, type EditorModel } from '../../hooks';
|
|
27
27
|
import { type ThemeStyles } from '../../styles';
|
|
28
28
|
import { defaultTheme, markdownTheme, textTheme } from '../../themes';
|
|
29
29
|
|
|
@@ -41,9 +41,6 @@ export type CursorInfo = {
|
|
|
41
41
|
|
|
42
42
|
export type TextEditorSlots = {
|
|
43
43
|
root?: Omit<ComponentProps<'div'>, 'ref'>;
|
|
44
|
-
// editor?: {
|
|
45
|
-
// className?: string;
|
|
46
|
-
// };
|
|
47
44
|
};
|
|
48
45
|
|
|
49
46
|
// TODO(burdon): Spellcheck?
|
|
@@ -52,7 +49,7 @@ export type TextEditorProps = {
|
|
|
52
49
|
autofocus?: boolean;
|
|
53
50
|
readonly?: boolean; // TODO(burdon): Move into model.
|
|
54
51
|
selection?: { anchor: number; head?: number };
|
|
55
|
-
comments?:
|
|
52
|
+
comments?: Comment[]; // TODO(burdon): Move into extension?
|
|
56
53
|
extensions?: Extension[];
|
|
57
54
|
editorMode?: EditorMode;
|
|
58
55
|
placeholder?: string;
|
|
@@ -60,6 +57,31 @@ export type TextEditorProps = {
|
|
|
60
57
|
slots?: TextEditorSlots;
|
|
61
58
|
};
|
|
62
59
|
|
|
60
|
+
// TODO(burdon): Factor out?
|
|
61
|
+
export const useComments = (view?: EditorView | null, comments?: Comment[]) => {
|
|
62
|
+
useEffect(() => {
|
|
63
|
+
if (view && comments !== undefined) {
|
|
64
|
+
view.dispatch({
|
|
65
|
+
effects: setComments.of(comments),
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
}, [view, comments]);
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
// TODO(burdon): Factor out?
|
|
72
|
+
export const useAwareness = ({ awareness, peer }: EditorModel) => {
|
|
73
|
+
const { themeMode } = useThemeContext();
|
|
74
|
+
useEffect(() => {
|
|
75
|
+
if (awareness && peer) {
|
|
76
|
+
awareness.setLocalStateField('user', {
|
|
77
|
+
name: peer.name ?? generateName(peer.id),
|
|
78
|
+
color: getColorForValue({ value: peer.id, type: 'color' }),
|
|
79
|
+
colorLight: getColorForValue({ value: peer.id, themeMode, type: 'highlight' }),
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
}, [awareness, peer, themeMode]);
|
|
83
|
+
};
|
|
84
|
+
|
|
63
85
|
/**
|
|
64
86
|
* Base text editor.
|
|
65
87
|
*/
|
|
@@ -77,32 +99,18 @@ export const BaseTextEditor = forwardRef<EditorView, TextEditorProps>(
|
|
|
77
99
|
// NOTE: This does not cause the parent to re-render, so the ref is not available immediately.
|
|
78
100
|
useImperativeHandle<EditorView | null, EditorView | null>(forwardedRef, () => view, [view]);
|
|
79
101
|
|
|
102
|
+
// Focus.
|
|
80
103
|
useEffect(() => {
|
|
81
104
|
if (autofocus) {
|
|
82
105
|
view?.focus();
|
|
83
106
|
}
|
|
84
|
-
}, [
|
|
107
|
+
}, [view, autofocus]);
|
|
85
108
|
|
|
86
109
|
// TODO(burdon): Factor out as extension/hook.
|
|
87
|
-
|
|
88
|
-
useEffect(() => {
|
|
89
|
-
if (awareness && peer) {
|
|
90
|
-
awareness.setLocalStateField('user', {
|
|
91
|
-
name: peer.name ?? generateName(peer.id),
|
|
92
|
-
color: getColorForValue({ value: peer.id, type: 'color' }),
|
|
93
|
-
colorLight: getColorForValue({ value: peer.id, themeMode, type: 'highlight' }),
|
|
94
|
-
});
|
|
95
|
-
}
|
|
96
|
-
}, [awareness, peer, themeMode]);
|
|
110
|
+
useAwareness(model);
|
|
97
111
|
|
|
98
112
|
// TODO(burdon): Factor out as extension/hook.
|
|
99
|
-
|
|
100
|
-
if (view && comments !== undefined) {
|
|
101
|
-
view.dispatch({
|
|
102
|
-
effects: setCommentRange.of({ model, comments }),
|
|
103
|
-
});
|
|
104
|
-
}
|
|
105
|
-
}, [view, comments]);
|
|
113
|
+
useComments(view, comments);
|
|
106
114
|
|
|
107
115
|
useEffect(() => {
|
|
108
116
|
if (!model || !rootRef.current) {
|
|
@@ -117,16 +125,16 @@ export const BaseTextEditor = forwardRef<EditorView, TextEditorProps>(
|
|
|
117
125
|
extensions: [
|
|
118
126
|
readonly && EditorState.readOnly.of(readonly),
|
|
119
127
|
|
|
120
|
-
// TODO(burdon): Factor out VIM mode? (manage via MarkdownPlugin).
|
|
121
|
-
editorMode === 'vim' && vim(),
|
|
122
|
-
|
|
123
128
|
// Theme.
|
|
124
129
|
// TODO(burdon): Make theme configurable.
|
|
125
130
|
EditorView.baseTheme(defaultTheme),
|
|
126
131
|
EditorView.theme(theme ?? {}),
|
|
127
132
|
EditorView.darkTheme.of(themeMode === 'dark'),
|
|
128
133
|
|
|
129
|
-
//
|
|
134
|
+
// TODO(burdon): Factor out VIM mode? (manage via MarkdownPlugin).
|
|
135
|
+
editorMode === 'vim' && vim(),
|
|
136
|
+
|
|
137
|
+
// Storage and replication (NOTE: must come before other extensions).
|
|
130
138
|
model.extension,
|
|
131
139
|
|
|
132
140
|
// Custom.
|
|
@@ -143,9 +151,12 @@ export const BaseTextEditor = forwardRef<EditorView, TextEditorProps>(
|
|
|
143
151
|
state,
|
|
144
152
|
// NOTE: Uncomment to spy on all transactions.
|
|
145
153
|
// https://codemirror.net/docs/ref/#view.EditorView.dispatch
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
154
|
+
dispatch: (transaction, view) => {
|
|
155
|
+
view.update([transaction]);
|
|
156
|
+
|
|
157
|
+
// TODO(burdon): Is there a place to register this?
|
|
158
|
+
view.state.facet(syncFacet)?.reconcile(view);
|
|
159
|
+
},
|
|
149
160
|
});
|
|
150
161
|
|
|
151
162
|
setView(newView);
|
|
@@ -155,18 +166,15 @@ export const BaseTextEditor = forwardRef<EditorView, TextEditorProps>(
|
|
|
155
166
|
};
|
|
156
167
|
}, [rootRef, model, readonly, editorMode, themeMode]);
|
|
157
168
|
|
|
169
|
+
// TODO(burdon): Replace with keymap?
|
|
158
170
|
const handleKeyUp = useCallback(
|
|
159
171
|
(event: KeyboardEvent) => {
|
|
160
172
|
const { key, altKey, shiftKey, metaKey, ctrlKey } = event;
|
|
161
173
|
switch (key) {
|
|
162
|
-
// TODO(burdon): Is this required (for vim mode?)
|
|
163
|
-
// case 'Enter': {
|
|
164
|
-
// view?.contentDOM.focus();
|
|
165
|
-
// break;
|
|
166
|
-
// }
|
|
167
|
-
|
|
168
174
|
case 'Escape': {
|
|
169
|
-
editorMode === 'vim' && (altKey || shiftKey || metaKey || ctrlKey)
|
|
175
|
+
if (editorMode === 'vim' && (altKey || shiftKey || metaKey || ctrlKey)) {
|
|
176
|
+
rootRef.current?.focus();
|
|
177
|
+
}
|
|
170
178
|
break;
|
|
171
179
|
}
|
|
172
180
|
}
|
|
@@ -18,7 +18,7 @@ import { withTheme } from '@dxos/storybook-utils';
|
|
|
18
18
|
|
|
19
19
|
import { MarkdownEditor, TextEditor } from './TextEditor';
|
|
20
20
|
import { comments, type CommentsOptions, Cursor } from '../../extensions';
|
|
21
|
-
import { type
|
|
21
|
+
import { type Comment, type Range, useTextModel } from '../../hooks';
|
|
22
22
|
|
|
23
23
|
faker.seed(101);
|
|
24
24
|
|
|
@@ -28,31 +28,39 @@ 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
47
|
const editorRef = useRef<EditorView>(null);
|
|
40
48
|
const [selected, setSelected] = useState<string>();
|
|
41
49
|
useEffect(() => {
|
|
42
|
-
if (!editorRef.current?.hasFocus &&
|
|
43
|
-
const thread = commentRanges.find((range) => range.id ===
|
|
50
|
+
if (!editorRef.current?.hasFocus && selectedValue !== selected) {
|
|
51
|
+
const thread = commentRanges.find((range) => range.id === selectedValue);
|
|
44
52
|
if (thread) {
|
|
45
53
|
const { cursor } = thread;
|
|
46
|
-
const range = Cursor.getRangeFromCursor(editorRef.current!.state
|
|
54
|
+
const range = cursor && Cursor.getRangeFromCursor(editorRef.current!.state, cursor);
|
|
47
55
|
if (range) {
|
|
48
56
|
// TODO(burdon): Scroll selection to center of screen?
|
|
49
57
|
editorRef.current?.dispatch({ selection: { anchor: range.from }, scrollIntoView: true });
|
|
50
58
|
}
|
|
51
59
|
}
|
|
52
60
|
|
|
53
|
-
setSelected(
|
|
61
|
+
setSelected(selectedValue);
|
|
54
62
|
}
|
|
55
|
-
}, [selected, commentRanges,
|
|
63
|
+
}, [selected, commentRanges, selectedValue]);
|
|
56
64
|
|
|
57
65
|
const extensions = useMemo(() => {
|
|
58
66
|
return [
|
|
@@ -83,11 +91,10 @@ const Editor: FC<{
|
|
|
83
91
|
|
|
84
92
|
type CommentThread = {
|
|
85
93
|
id: string;
|
|
86
|
-
|
|
94
|
+
cursor?: string;
|
|
95
|
+
range?: Range;
|
|
87
96
|
yPos?: number;
|
|
88
|
-
selection?: Range;
|
|
89
97
|
messages: TextObject[];
|
|
90
|
-
deleted?: boolean;
|
|
91
98
|
};
|
|
92
99
|
|
|
93
100
|
const Thread: FC<{
|
|
@@ -129,16 +136,16 @@ const Thread: FC<{
|
|
|
129
136
|
className={mx(
|
|
130
137
|
'flex flex-col m-1 rounded shadow divide-y bg-white',
|
|
131
138
|
selected && 'ring',
|
|
132
|
-
thread.
|
|
139
|
+
!thread.cursor && 'opacity-50',
|
|
133
140
|
)}
|
|
134
141
|
>
|
|
135
142
|
<div className='flex p-2 gap-2 items-center text-xs font-mono text-neutral-500 font-thin'>
|
|
136
143
|
<span>id:{thread.id.slice(0, 4)}</span>
|
|
137
|
-
<span>from:{thread.
|
|
138
|
-
<span>to:{thread.
|
|
144
|
+
<span>from:{thread.range?.from}</span>
|
|
145
|
+
<span>to:{thread.range?.to}</span>
|
|
139
146
|
<span>y:{thread.yPos}</span>
|
|
140
147
|
<span className='grow' />
|
|
141
|
-
{thread.
|
|
148
|
+
{!thread.cursor && <Trash />}
|
|
142
149
|
</div>
|
|
143
150
|
|
|
144
151
|
{thread.messages.map((message, i) => (
|
|
@@ -218,9 +225,10 @@ type StoryProps = {
|
|
|
218
225
|
const Story = ({ text, autoCreate }: StoryProps) => {
|
|
219
226
|
const [item] = useState({ text: new TextObject(text) });
|
|
220
227
|
const [threads, setThreads] = useState<CommentThread[]>([]);
|
|
221
|
-
const commentRanges = useMemo(() => threads.map((thread) => thread.range), [threads]);
|
|
222
228
|
const [selected, setSelected] = useState<string>();
|
|
223
229
|
|
|
230
|
+
const comments = useMemo<Comment[]>(() => threads.map(({ id, cursor }) => ({ id, cursor })), [threads]);
|
|
231
|
+
|
|
224
232
|
// Filter by visibility.
|
|
225
233
|
const visibleThreads = useMemo(() => threads.filter((thread) => thread.yPos !== undefined), [threads]);
|
|
226
234
|
|
|
@@ -231,7 +239,7 @@ const Story = ({ text, autoCreate }: StoryProps) => {
|
|
|
231
239
|
...threads,
|
|
232
240
|
{
|
|
233
241
|
id,
|
|
234
|
-
|
|
242
|
+
cursor,
|
|
235
243
|
yPos: location ? Math.floor(location.top) : undefined,
|
|
236
244
|
messages: autoCreate
|
|
237
245
|
? faker.helpers.multiple(() => new TextObject(faker.lorem.sentence()), { count: { min: 1, max: 3 } })
|
|
@@ -247,7 +255,7 @@ const Story = ({ text, autoCreate }: StoryProps) => {
|
|
|
247
255
|
setThreads((threads) =>
|
|
248
256
|
threads.map((thread) => {
|
|
249
257
|
if (thread.id === id) {
|
|
250
|
-
thread.
|
|
258
|
+
thread.cursor = undefined;
|
|
251
259
|
}
|
|
252
260
|
|
|
253
261
|
return thread;
|
|
@@ -262,8 +270,7 @@ const Story = ({ text, autoCreate }: StoryProps) => {
|
|
|
262
270
|
setThreads((threads) =>
|
|
263
271
|
threads.map((thread) => {
|
|
264
272
|
if (thread.id === id) {
|
|
265
|
-
thread.
|
|
266
|
-
delete thread.deleted;
|
|
273
|
+
thread.cursor = cursor;
|
|
267
274
|
}
|
|
268
275
|
|
|
269
276
|
return thread;
|
|
@@ -271,14 +278,14 @@ const Story = ({ text, autoCreate }: StoryProps) => {
|
|
|
271
278
|
);
|
|
272
279
|
};
|
|
273
280
|
|
|
274
|
-
const handleSelectComment: CommentsOptions['onSelect'] = ({ active, closest
|
|
281
|
+
const handleSelectComment: CommentsOptions['onSelect'] = ({ comments, selection: { active, closest } }) => {
|
|
275
282
|
log.info('select', { active: active?.slice(0, 4), closest: closest?.slice(0, 4) });
|
|
276
283
|
setThreads((threads) =>
|
|
277
284
|
threads.map((thread) => {
|
|
278
|
-
const
|
|
279
|
-
if (
|
|
280
|
-
thread.yPos =
|
|
281
|
-
thread.
|
|
285
|
+
const comment = comments.find(({ comment }) => comment.id === thread.id);
|
|
286
|
+
if (comment) {
|
|
287
|
+
thread.yPos = comment.location ? Math.floor(comment.location.top) : undefined;
|
|
288
|
+
thread.range = { from: comment.range.from, to: comment.range.to };
|
|
282
289
|
}
|
|
283
290
|
|
|
284
291
|
return thread;
|
|
@@ -303,8 +310,8 @@ const Story = ({ text, autoCreate }: StoryProps) => {
|
|
|
303
310
|
<div className='flex flex-col h-full w-[600px]'>
|
|
304
311
|
<Editor
|
|
305
312
|
item={item}
|
|
306
|
-
|
|
307
|
-
|
|
313
|
+
selected={selected}
|
|
314
|
+
comments={comments}
|
|
308
315
|
onCreateComment={handleCreateComment}
|
|
309
316
|
onDeleteComment={handleDeleteComment}
|
|
310
317
|
onUpdateComment={handleUpdateComment}
|
|
@@ -4,16 +4,15 @@
|
|
|
4
4
|
// Ref: https://github.com/automerge/automerge-codemirror
|
|
5
5
|
//
|
|
6
6
|
|
|
7
|
-
import { StateField, type Extension
|
|
8
|
-
import { ViewPlugin, type EditorView, type PluginValue, type ViewUpdate } from '@codemirror/view';
|
|
7
|
+
import { StateField, type Extension } from '@codemirror/state';
|
|
9
8
|
|
|
10
|
-
import { next as A } from '@dxos/automerge/automerge';
|
|
11
|
-
import { type Prop } from '@dxos/automerge/automerge';
|
|
9
|
+
import { type Prop, next as A } from '@dxos/automerge/automerge';
|
|
12
10
|
|
|
13
11
|
import { cursorConverter } from './cursor';
|
|
14
|
-
import { effectType, type IDocHandle, isReconcileTx,
|
|
12
|
+
import { effectType, type IDocHandle, isReconcileTx, type Value } from './defs';
|
|
15
13
|
import { PatchSemaphore } from './semaphore';
|
|
16
14
|
import { Cursor } from '../cursor';
|
|
15
|
+
import { syncFacet } from '../sync';
|
|
17
16
|
|
|
18
17
|
export type AutomergeOptions = {
|
|
19
18
|
handle: IDocHandle;
|
|
@@ -21,18 +20,19 @@ export type AutomergeOptions = {
|
|
|
21
20
|
};
|
|
22
21
|
|
|
23
22
|
export const automerge = ({ handle, path }: AutomergeOptions): Extension => {
|
|
24
|
-
|
|
23
|
+
// TODO(burdon): Rename and comment.
|
|
24
|
+
const stateField = StateField.define<Value>({
|
|
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
|
|
31
|
+
update: (value, tr) => {
|
|
32
32
|
const result: Value = {
|
|
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;
|
|
@@ -57,36 +57,7 @@ export const automerge = ({ handle, path }: AutomergeOptions): Extension => {
|
|
|
57
57
|
|
|
58
58
|
const semaphore = new PatchSemaphore(handle, stateField);
|
|
59
59
|
|
|
60
|
-
const viewPlugin = ViewPlugin.fromClass(
|
|
61
|
-
class AutomergeViewPlugin implements PluginValue {
|
|
62
|
-
constructor(private readonly _view: EditorView) {
|
|
63
|
-
handle.addListener('change', this._handleChange);
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
update(update: ViewUpdate) {
|
|
67
|
-
if (update.transactions.length > 0 && update.transactions.some((tr) => !isReconcileTx(tr))) {
|
|
68
|
-
queueMicrotask(() => {
|
|
69
|
-
this._view.state.facet(semaphoreFacet).reconcile(this._view);
|
|
70
|
-
});
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
destroy() {
|
|
75
|
-
handle.addListener('change', this._handleChange);
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
private _handleChange = () => {
|
|
79
|
-
this._view.state.facet(semaphoreFacet).reconcile(this._view);
|
|
80
|
-
};
|
|
81
|
-
},
|
|
82
|
-
);
|
|
83
|
-
|
|
84
60
|
return {
|
|
85
|
-
extension: [
|
|
86
|
-
Cursor.converter.of(cursorConverter(handle, path)),
|
|
87
|
-
semaphoreFacet.of(semaphore),
|
|
88
|
-
stateField,
|
|
89
|
-
viewPlugin,
|
|
90
|
-
],
|
|
61
|
+
extension: [Cursor.converter.of(cursorConverter(handle, path)), syncFacet.of(semaphore), stateField],
|
|
91
62
|
};
|
|
92
63
|
};
|
|
@@ -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
|
-
|
|
11
|
+
// TODO(burdon): Rename and document.
|
|
13
12
|
export type Value = {
|
|
14
|
-
lastHeads: Heads;
|
|
15
13
|
path: Prop[];
|
|
14
|
+
lastHeads: Heads;
|
|
16
15
|
unreconciledTransactions: Transaction[];
|
|
17
16
|
};
|
|
18
17
|
|
|
@@ -22,21 +21,17 @@ export type UpdateHeads = {
|
|
|
22
21
|
|
|
23
22
|
export const effectType = StateEffect.define<UpdateHeads>({});
|
|
24
23
|
|
|
25
|
-
export const
|
|
26
|
-
|
|
27
|
-
export const getLastHeads = (state: EditorState, field: StateField<Value>): Heads => state.field(field).lastHeads;
|
|
24
|
+
export const reconcileAnnotationType = Annotation.define<unknown>();
|
|
28
25
|
|
|
29
26
|
export const getPath = (state: EditorState, field: StateField<Value>): Prop[] => state.field(field).path;
|
|
30
27
|
|
|
31
|
-
export const
|
|
28
|
+
export const getLastHeads = (state: EditorState, field: StateField<Value>): Heads => state.field(field).lastHeads;
|
|
32
29
|
|
|
33
|
-
export const
|
|
30
|
+
export const updateHeads = (newHeads: Heads): StateEffect<UpdateHeads> => effectType.of({ newHeads });
|
|
34
31
|
|
|
35
|
-
|
|
36
|
-
export const semaphoreFacet = Facet.define<PatchSemaphore, PatchSemaphore>({
|
|
37
|
-
combine: (values) => values.at(-1)!, // Take last.
|
|
38
|
-
});
|
|
32
|
+
export const isReconcileTx = (tr: Transaction): boolean => !!tr.annotation(reconcileAnnotationType);
|
|
39
33
|
|
|
34
|
+
// TODO(burdon): Remove?
|
|
40
35
|
/*
|
|
41
36
|
export const makeReconcile = (tr: TransactionSpec) => {
|
|
42
37
|
if (tr.annotations != null) {
|
|
@@ -20,16 +20,23 @@ import {
|
|
|
20
20
|
} from './defs';
|
|
21
21
|
import { updateAutomerge } from './update-automerge';
|
|
22
22
|
import { updateCodeMirror } from './update-codemirror';
|
|
23
|
+
import { type Syncer } from '../sync';
|
|
23
24
|
|
|
24
25
|
/**
|
|
25
|
-
* Implements three-way merge (on each mutation/
|
|
26
|
+
* Implements three-way merge (on each mutation/keystroke).
|
|
26
27
|
*/
|
|
27
|
-
export class PatchSemaphore {
|
|
28
|
-
_inReconcile = false;
|
|
28
|
+
export class PatchSemaphore implements Syncer {
|
|
29
|
+
private _inReconcile = false;
|
|
29
30
|
|
|
30
|
-
|
|
31
|
+
// prettier-ignore
|
|
32
|
+
constructor(
|
|
33
|
+
private readonly _handle: IDocHandle,
|
|
34
|
+
// TODO(burdon): Rename.
|
|
35
|
+
private readonly _field: StateField<Value>
|
|
36
|
+
) {}
|
|
31
37
|
|
|
32
|
-
|
|
38
|
+
// NOTE: Cannot destruct view.state.
|
|
39
|
+
reconcile(view: EditorView) {
|
|
33
40
|
if (this._inReconcile) {
|
|
34
41
|
return;
|
|
35
42
|
}
|
|
@@ -58,7 +65,7 @@ export class PatchSemaphore {
|
|
|
58
65
|
|
|
59
66
|
// NOTE: null and undefined each come from automerge and repo respectively.
|
|
60
67
|
if (newHeads === null || newHeads === undefined) {
|
|
61
|
-
// TODO:
|
|
68
|
+
// TODO(alexjg): this is the call that's resetting the editor state on click.
|
|
62
69
|
newHeads = automerge.getHeads(this._handle.docSync()!);
|
|
63
70
|
}
|
|
64
71
|
|
|
@@ -74,5 +81,5 @@ export class PatchSemaphore {
|
|
|
74
81
|
});
|
|
75
82
|
|
|
76
83
|
this._inReconcile = false;
|
|
77
|
-
}
|
|
84
|
+
}
|
|
78
85
|
}
|