|
1 | | -import { |
2 | | - type ContainerID, |
| 1 | +import type { |
| 2 | + ContainerID, |
| 3 | + Cursor, |
3 | 4 | LoroDoc, |
4 | | - type LoroEventBatch, |
| 5 | + LoroEventBatch, |
5 | 6 | LoroMap, |
6 | | - type Subscription, |
| 7 | + Subscription, |
7 | 8 | } from "loro-crdt"; |
8 | 9 | import { Fragment, Slice } from "prosemirror-model"; |
9 | 10 | import { |
10 | | - EditorState, |
| 11 | + type EditorState, |
11 | 12 | Plugin, |
12 | 13 | PluginKey, |
13 | 14 | type StateField, |
14 | | - TextSelection, |
15 | 15 | } from "prosemirror-state"; |
16 | | -import { EditorView } from "prosemirror-view"; |
| 16 | +import type { EditorView } from "prosemirror-view"; |
17 | 17 |
|
| 18 | +import { |
| 19 | + convertPmSelectionToCursors, |
| 20 | + cursorToAbsolutePosition, |
| 21 | +} from "./cursor-plugin"; |
18 | 22 | import { |
19 | 23 | clearChangedNodes, |
20 | 24 | createNodeFromLoroObj, |
21 | 25 | type LoroDocType, |
22 | 26 | type LoroNodeContainerType, |
23 | 27 | type LoroNodeMapping, |
24 | 28 | ROOT_DOC_KEY, |
| 29 | + safeSetSelection, |
25 | 30 | updateLoroToPmState, |
26 | 31 | } from "./lib"; |
27 | 32 | import { configLoroTextStyle } from "./text-style"; |
28 | | -import { |
29 | | - convertPmSelectionToCursors, |
30 | | - cursorToAbsolutePosition, |
31 | | -} from "./cursor-plugin"; |
32 | 33 | import { loroUndoPluginKey } from "./undo-plugin"; |
33 | 34 |
|
34 | 35 | export const loroSyncPluginKey = new PluginKey<LoroSyncPluginState>( |
@@ -227,25 +228,36 @@ function updateNodeOnLoroEvent(view: EditorView, event: LoroEventBatch) { |
227 | 228 | type: "non-local-updates", |
228 | 229 | }); |
229 | 230 | view.dispatch(tr); |
230 | | - Promise.resolve().then(() => { |
231 | | - if (anchor && focus) { |
232 | | - const state = loroSyncPluginKey.getState( |
233 | | - view.state, |
234 | | - ) as LoroSyncPluginState; |
235 | | - const anchorPos = cursorToAbsolutePosition( |
236 | | - anchor, |
237 | | - state.doc, |
238 | | - state.mapping, |
239 | | - )[0]; |
240 | | - const focusPos = |
241 | | - focus && cursorToAbsolutePosition(focus, state.doc, state.mapping)[0]; |
242 | | - const selection = TextSelection.create( |
243 | | - view.state.tr.doc, |
244 | | - anchorPos, |
245 | | - focusPos ?? undefined, |
246 | | - ); |
247 | | - const tr = view.state.tr.setSelection(selection); |
248 | | - view.dispatch(tr); |
249 | | - } |
| 231 | + |
| 232 | + if (anchor == null) { |
| 233 | + return; |
| 234 | + } |
| 235 | + setTimeout(() => { |
| 236 | + syncCursorsToPmSelection(view, anchor, focus); |
250 | 237 | }); |
251 | 238 | } |
| 239 | + |
| 240 | +/** |
| 241 | + * Update ProseMirror selection based on the given Loro cursors. |
| 242 | + */ |
| 243 | +export function syncCursorsToPmSelection( |
| 244 | + view: EditorView, |
| 245 | + anchor: Cursor, |
| 246 | + focus?: Cursor, |
| 247 | +) { |
| 248 | + const state = loroSyncPluginKey.getState(view.state); |
| 249 | + if (!state) { |
| 250 | + return; |
| 251 | + } |
| 252 | + |
| 253 | + const { doc, mapping } = state; |
| 254 | + const anchorPos = cursorToAbsolutePosition(anchor, doc, mapping)[0]; |
| 255 | + const focusPos = focus && cursorToAbsolutePosition(focus, doc, mapping)[0]; |
| 256 | + if (anchorPos == null) { |
| 257 | + return; |
| 258 | + } |
| 259 | + |
| 260 | + // If the cursors are synced faster than the document, then the cursors might |
| 261 | + // be out of bounds. Thus, we need to check if the cursors are out of bounds. |
| 262 | + safeSetSelection(view, anchorPos, focusPos); |
| 263 | +} |
0 commit comments