Skip to content

Commit 9771d4d

Browse files
authored
Introduce Multi-User Undo/Redo for Collaborative Editing (#563)
Undo and redo operations are now recorded based on local edits and reconciled against remote changes, allowing text updates to be safely reverted and consistently reapplied in collaborative editing sessions.
1 parent ac289aa commit 9771d4d

File tree

3 files changed

+48
-7
lines changed

3 files changed

+48
-7
lines changed

frontend/src/components/editor/Editor.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@ function Editor(props: EditorProps) {
8383
extensions: [
8484
configStore.codeKey === CodeKeyType.VIM ? vim() : [],
8585
keymap.of(setKeymapConfig()),
86-
basicSetup({ highlightSelectionMatches: false }),
86+
basicSetup({ highlightSelectionMatches: false, history: false }),
8787
markdown(),
8888
themeMode === "light" ? xcodeLight : xcodeDark,
8989
EditorView.theme({ "&": { width: "100%" } }),

frontend/src/hooks/useFormatUtils.ts

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ export enum FormatType {
2020
}
2121

2222
export const useFormatUtils = () => {
23-
const { cmView } = useSelector(selectEditor);
23+
const { cmView, doc } = useSelector(selectEditor);
2424

2525
const getFormatMarker = useCallback((formatType: FormatType) => {
2626
switch (formatType) {
@@ -109,15 +109,34 @@ export const useFormatUtils = () => {
109109
[getFormatMarker, getFormatMarkerLength]
110110
);
111111

112+
const handleYorkieUndo = useCallback(() => {
113+
if (doc?.history.canUndo()) {
114+
doc.history.undo();
115+
}
116+
117+
return true;
118+
}, [doc]);
119+
120+
const handleYorkieRedo = useCallback(() => {
121+
if (doc?.history.canRedo()) {
122+
doc.history.redo();
123+
}
124+
125+
return true;
126+
}, [doc]);
127+
112128
const setKeymapConfig = useCallback(
113129
() => [
114130
indentWithTab,
115131
{ key: "Mod-b", run: applyFormat(FormatType.BOLD) },
116132
{ key: "Mod-i", run: applyFormat(FormatType.ITALIC) },
117133
{ key: "Mod-e", run: applyFormat(FormatType.CODE) },
118134
{ key: "Mod-Shift-x", run: applyFormat(FormatType.STRIKETHROUGH) },
135+
{ key: "Mod-z", run: handleYorkieUndo, preventDefault: true },
136+
{ key: "Mod-y", run: handleYorkieRedo, preventDefault: true },
137+
{ key: "Mod-Shift-z", run: handleYorkieRedo, preventDefault: true },
119138
],
120-
[applyFormat]
139+
[applyFormat, handleYorkieUndo, handleYorkieRedo]
121140
);
122141

123142
const toggleButtonChangeHandler = useCallback(
@@ -165,5 +184,7 @@ export const useFormatUtils = () => {
165184
setKeymapConfig,
166185
toggleButtonChangeHandler,
167186
checkAndAddFormat,
187+
handleYorkieUndo,
188+
handleYorkieRedo,
168189
};
169190
};

frontend/src/utils/yorkie/yorkieSync.ts

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -77,8 +77,14 @@ class YorkieSyncPluginValue implements cmView.PluginValue {
7777
});
7878

7979
this._doc.subscribe((event) => {
80-
if (event.type !== "remote-change") return;
80+
if (
81+
event.type !== "remote-change" &&
82+
!(event.type === "local-change" && event.source === "undoredo")
83+
) {
84+
return;
85+
}
8186

87+
const isUndoRedo = event.type === "local-change" && event.source === "undoredo";
8288
const { operations } = event.value;
8389

8490
// Check if content itself is replaced
@@ -106,11 +112,25 @@ class YorkieSyncPluginValue implements cmView.PluginValue {
106112
insert: op.value!.content,
107113
},
108114
];
109-
110-
view.dispatch({
115+
const transactionSpec: cmState.TransactionSpec = {
111116
changes,
112117
annotations: [cmState.Transaction.remote.of(true)],
113-
});
118+
};
119+
120+
if (isUndoRedo) {
121+
const newPos = op.from + (op.value?.content?.length || 0);
122+
const docLength =
123+
view.state.doc.length +
124+
((op.value?.content?.length || 0) - (op.to - op.from));
125+
const boundedPos = Math.min(Math.max(0, newPos), docLength);
126+
127+
transactionSpec.selection = {
128+
anchor: boundedPos,
129+
head: boundedPos,
130+
};
131+
}
132+
133+
view.dispatch(transactionSpec);
114134
}
115135
});
116136
});

0 commit comments

Comments
 (0)