Skip to content

Commit 9e4fa94

Browse files
committed
refactor: simplify undo impl based on 0.16.3
1 parent aa4785b commit 9e4fa94

File tree

3 files changed

+96
-130
lines changed

3 files changed

+96
-130
lines changed

examples/stories/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
"build-storybook": "storybook build"
1313
},
1414
"dependencies": {
15-
"loro-crdt": "^0.16.2",
15+
"loro-crdt": "^0.16.3",
1616
"loro-prosemirror": "link:../..",
1717
"prosemirror-commands": "^1.5.2",
1818
"prosemirror-example-setup": "^1.2.2",

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
"license": "ISC",
1515
"dependencies": {
1616
"lib0": "^0.2.42",
17-
"loro-crdt": "^0.16.2"
17+
"loro-crdt": "^0.16.3"
1818
},
1919
"peerDependencies": {
2020
"prosemirror-model": "^1.18.1",

src/undo-plugin.ts

Lines changed: 94 additions & 128 deletions
Original file line numberDiff line numberDiff line change
@@ -1,39 +1,51 @@
11
import { Cursor, Loro, UndoManager } from "loro-crdt";
2-
import { EditorState, Plugin, PluginKey, StateField, TextSelection, Transaction } from "prosemirror-state";
2+
import {
3+
EditorState,
4+
Plugin,
5+
PluginKey,
6+
StateField,
7+
TextSelection,
8+
Transaction,
9+
} from "prosemirror-state";
310
import { EditorView } from "prosemirror-view";
4-
import { convertPmSelectionToCursors, cursorToAbsolutePosition } from "./cursor-plugin";
11+
import {
12+
convertPmSelectionToCursors,
13+
cursorToAbsolutePosition,
14+
} from "./cursor-plugin";
515
import { loroSyncPluginKey } from "./sync-plugin";
616

717
export interface LoroUndoPluginProps {
818
doc: Loro;
9-
undoManager?: UndoManager
19+
undoManager?: UndoManager;
1020
}
1121

12-
export const loroUndoPluginKey = new PluginKey<LoroUndoPluginState>("loro-undo");
22+
export const loroUndoPluginKey = new PluginKey<LoroUndoPluginState>(
23+
"loro-undo",
24+
);
1325

1426
interface LoroUndoPluginState {
15-
undoManager: UndoManager,
16-
canUndo: boolean,
17-
canRedo: boolean
27+
undoManager: UndoManager;
28+
canUndo: boolean;
29+
canRedo: boolean;
1830
}
1931

20-
type Cursors = { anchor: Cursor | null, focus: Cursor | null };
32+
type Cursors = { anchor: Cursor | null; focus: Cursor | null };
2133
export const LoroUndoPlugin = (props: LoroUndoPluginProps): Plugin => {
2234
const undoManager = props.undoManager || new UndoManager(props.doc, {});
2335
let lastSelection: Cursors = {
2436
anchor: null,
25-
focus: null
26-
}
37+
focus: null,
38+
};
2739
return new Plugin({
2840
key: loroUndoPluginKey,
2941
state: {
3042
init: (config, editorState): LoroUndoPluginState => {
31-
undoManager.addExcludeOriginPrefix("sys:init")
43+
undoManager.addExcludeOriginPrefix("sys:init");
3244
return {
3345
undoManager,
3446
canUndo: undoManager.canUndo(),
3547
canRedo: undoManager.canRedo(),
36-
}
48+
};
3749
},
3850
apply: (tr, state, oldEditorState, newEditorState) => {
3951
const undoState = loroUndoPluginKey.getState(oldEditorState);
@@ -44,104 +56,55 @@ export const LoroUndoPlugin = (props: LoroUndoPluginProps): Plugin => {
4456

4557
const canUndo = undoState.undoManager.canUndo();
4658
const canRedo = undoState.undoManager.canRedo();
47-
const { anchor, focus } = convertPmSelectionToCursors(oldEditorState.doc, oldEditorState.selection, loroState);
48-
lastSelection = {
49-
anchor: anchor ?? null,
50-
focus: focus ?? null
59+
{
60+
const { anchor, focus } = convertPmSelectionToCursors(
61+
oldEditorState.doc,
62+
oldEditorState.selection,
63+
loroState,
64+
);
65+
lastSelection = {
66+
anchor: anchor ?? null,
67+
focus: focus ?? null,
68+
};
5169
}
5270
return {
5371
...state,
5472
canUndo,
5573
canRedo,
56-
}
74+
};
5775
},
5876
} as StateField<LoroUndoPluginState>,
5977

6078
view: (view: EditorView) => {
61-
// When in the undo/redo loop, the new undo/redo stack item should restore the selection
62-
// to the state it was in before the item that was popped two steps ago from the stack.
63-
//
64-
// ┌────────────┐
65-
// │Selection 1 │
66-
// └─────┬──────┘
67-
// │ Some
68-
// ▼ ops
69-
// ┌────────────┐
70-
// │Selection 2 │
71-
// └─────┬──────┘
72-
// │ Some
73-
// ▼ ops
74-
// ┌────────────┐
75-
// │Selection 3 │◁ ─ ─ ─ ─ ─ ─ ─ Restore ─ ─ ─
76-
// └─────┬──────┘ │
77-
// │
78-
// │ │
79-
// │ ┌ ─ ─ ─ ─ ─ ─ ─
80-
// Enter the │ Undo ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─▶ Push Redo │
81-
// undo/redo ─ ─ ─ ▶ ▼ └ ─ ─ ─ ─ ─ ─ ─
82-
// loop ┌────────────┐ │
83-
// │Selection 2 │◁─ ─ ─ Restore ─
84-
// └─────┬──────┘ │ │
85-
// │
86-
// │ │ │
87-
// │ ┌ ─ ─ ─ ─ ─ ─ ─
88-
// │ Undo ─ ─ ─ ─ ▶ Push Redo │ │
89-
// ▼ └ ─ ─ ─ ─ ─ ─ ─
90-
// ┌────────────┐ │ │
91-
// │Selection 1 │
92-
// └─────┬──────┘ │ │
93-
// │ Redo ◀ ─ ─ ─ ─ ─ ─ ─ ─
94-
// ▼ │
95-
// ┌────────────┐
96-
// ┌ Restore ─ ▷│Selection 2 │ │
97-
// └─────┬──────┘
98-
// │ │ │
99-
// ┌ ─ ─ ─ ─ ─ ─ ─ │
100-
// Push Undo │◀─ ─ ─ ─ ─ ─ ─ │ Redo ◀ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┘
101-
// └ ─ ─ ─ ─ ─ ─ ─ ▼
102-
// │ ┌────────────┐
103-
// │Selection 3 │
104-
// │ └─────┬──────┘
105-
// ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ▶ │ Undo
106-
// ▼
107-
// ┌────────────┐
108-
// │Selection 2 │
109-
// └────────────┘
110-
//
111-
// Because users may change the selections during the undo/redo loop, it's
112-
// more stable to keep the selection stored in the last stack item
113-
// rather than using the current selection directly.
114-
115-
let lastUndoRedoLoopSelection: Cursors | null = null;
116-
let justPopped = false;
117-
props.doc.subscribe(event => {
118-
if (event.by === "import") {
119-
lastUndoRedoLoopSelection = null;
120-
}
121-
});
122-
12379
undoManager.setOnPush((isUndo, _counterRange) => {
124-
if (!justPopped) {
125-
// A new op is being pushed to the undo stack, so it breaks the
126-
// undo/redo loop.
127-
console.assert(isUndo);
128-
lastUndoRedoLoopSelection = null;
129-
}
130-
13180
const loroState = loroSyncPluginKey.getState(view.state);
13281
if (loroState?.doc == null) {
13382
return {
13483
value: null,
135-
cursors: []
84+
cursors: [],
13685
};
13786
}
13887

13988
const cursors: Cursor[] = [];
140-
if (lastSelection.anchor) {
141-
cursors.push(lastSelection.anchor);
89+
let selection = lastSelection;
90+
if (!isUndo) {
91+
const loroState = loroSyncPluginKey.getState(view.state);
92+
if (loroState) {
93+
const { anchor, focus } = convertPmSelectionToCursors(
94+
view.state.doc,
95+
view.state.selection,
96+
loroState,
97+
);
98+
selection.anchor = anchor || null;
99+
selection.focus = focus || null;
100+
}
101+
}
102+
103+
if (selection.anchor) {
104+
cursors.push(selection.anchor);
142105
}
143-
if (lastSelection.focus) {
144-
cursors.push(lastSelection.focus);
106+
if (selection.focus) {
107+
cursors.push(selection.focus);
145108
}
146109

147110
return {
@@ -151,10 +114,10 @@ export const LoroUndoPlugin = (props: LoroUndoPluginProps): Plugin => {
151114
// the cursors to their new positions. Additionally, if containers are deleted
152115
// and recreated, they also need remapping. Remote changes to the document
153116
// should be considered in these transformations.
154-
cursors
155-
}
156-
})
157-
undoManager.setOnPop((isUndo, meta, counterRange) => {
117+
cursors,
118+
};
119+
});
120+
undoManager.setOnPop((_isUndo, meta, _counterRange) => {
158121
// After this call, the `onPush` will be called immediately.
159122
// The immediate `onPush` will contain the inverse operations that undone the effect caused by the current `onPop`
160123
const loroState = loroSyncPluginKey.getState(view.state);
@@ -163,44 +126,43 @@ export const LoroUndoPlugin = (props: LoroUndoPluginProps): Plugin => {
163126
}
164127

165128
const anchor = meta.cursors[0] ?? null;
166-
const focus = meta.cursors[1] ?? null
129+
const focus = meta.cursors[1] ?? null;
167130
if (anchor == null) {
168131
return;
169132
}
170133

171-
if (lastUndoRedoLoopSelection) {
172-
// We overwrite the lastSelection so that the corresponding `onPush`
173-
// will restore the selection to the state it was in before the
174-
// item that was popped two steps ago from the stack.
175-
lastSelection = lastUndoRedoLoopSelection;
176-
}
177-
178-
lastUndoRedoLoopSelection = {
179-
anchor,
180-
focus
181-
};
182-
183-
justPopped = true;
184134
setTimeout(() => {
185135
try {
186-
justPopped = false;
187-
const anchorPos = cursorToAbsolutePosition(anchor, loroState.doc, loroState.mapping)[0];
188-
const focusPos = focus && cursorToAbsolutePosition(focus, loroState.doc, loroState.mapping)[0];
189-
const selection = TextSelection.create(view.state.doc, anchorPos, focusPos ?? undefined)
136+
const anchorPos = cursorToAbsolutePosition(
137+
anchor,
138+
loroState.doc,
139+
loroState.mapping,
140+
)[0];
141+
const focusPos =
142+
focus &&
143+
cursorToAbsolutePosition(
144+
focus,
145+
loroState.doc,
146+
loroState.mapping,
147+
)[0];
148+
const selection = TextSelection.create(
149+
view.state.doc,
150+
anchorPos,
151+
focusPos ?? undefined,
152+
);
190153
view.dispatch(view.state.tr.setSelection(selection));
191154
} catch (e) {
192155
console.error(e);
193156
}
194-
}, 0)
157+
}, 0);
195158
});
196159
return {
197160
destroy: () => {
198161
undoManager.setOnPop();
199162
undoManager.setOnPush();
200-
}
201-
}
202-
203-
}
163+
},
164+
};
165+
},
204166
});
205167
};
206168

@@ -214,34 +176,38 @@ export function canRedo(state: EditorState): boolean {
214176
return undoState?.undoManager.canRedo() || false;
215177
}
216178

217-
export function undo(state: EditorState, dispatch: (tr: Transaction) => void): boolean {
179+
export function undo(
180+
state: EditorState,
181+
dispatch: (tr: Transaction) => void,
182+
): boolean {
218183
const undoState = loroUndoPluginKey.getState(state);
219184
if (!undoState || !undoState.undoManager.canUndo()) {
220185
return false;
221186
}
222187

223188
if (!undoState.undoManager.undo()) {
224189
const emptyTr = state.tr;
225-
dispatch(emptyTr)
226-
return false
190+
dispatch(emptyTr);
191+
return false;
227192
}
228193

229-
return true
194+
return true;
230195
}
231196

232-
233-
export function redo(state: EditorState, dispatch: (tr: Transaction) => void): boolean {
197+
export function redo(
198+
state: EditorState,
199+
dispatch: (tr: Transaction) => void,
200+
): boolean {
234201
const undoState = loroUndoPluginKey.getState(state);
235202
if (!undoState || !undoState.undoManager.canRedo()) {
236203
return false;
237204
}
238205

239206
if (!undoState.undoManager.redo()) {
240207
const emptyTr = state.tr;
241-
dispatch(emptyTr)
242-
return false
208+
dispatch(emptyTr);
209+
return false;
243210
}
244211

245-
return true
212+
return true;
246213
}
247-

0 commit comments

Comments
 (0)