loro-codemirror
Version:
A CodeMirror plugin for loro
158 lines (157 loc) • 5.31 kB
JavaScript
import { EditorSelection, StateEffect, StateField, } from "@codemirror/state";
import { EditorView, ViewUpdate } from "@codemirror/view";
import { Cursor, LoroDoc, LoroText, UndoManager, } from "loro-crdt";
import { loroSyncAnnotation } from "./sync.js";
export const undoEffect = StateEffect.define();
export const redoEffect = StateEffect.define();
export const undoManagerStateField = StateField.define({
create(state) {
return undefined;
},
update(value, transaction) {
for (const effect of transaction.effects) {
if (effect.is(undoEffect)) {
if (value && value.canUndo()) {
value.undo();
}
}
else if (effect.is(redoEffect)) {
if (value && value.canRedo()) {
value.redo();
}
}
}
return value;
},
});
export class UndoPluginValue {
constructor(view, doc, undoManager, getTextFromDoc) {
this.view = view;
this.doc = doc;
this.undoManager = undoManager;
this.getTextFromDoc = getTextFromDoc;
this.lastSelection = {
anchor: undefined,
head: undefined,
};
this.sub = doc.subscribe((e) => {
if (e.origin !== "undo")
return;
let changes = [];
let pos = 0;
for (let { diff, target } of e.events) {
const text = this.getTextFromDoc(this.doc);
// Skip if the event is not a text event
if (diff.type !== "text")
return;
// Skip if the event is not for the current document
if (target !== text.id)
return;
const textDiff = diff.diff;
for (const delta of textDiff) {
if (delta.insert) {
changes.push({
from: pos,
to: pos,
insert: delta.insert,
});
}
else if (delta.delete) {
changes.push({
from: pos,
to: pos + delta.delete,
});
pos += delta.delete;
}
else if (delta.retain != null) {
pos += delta.retain;
}
}
this.view.dispatch({
changes,
annotations: [loroSyncAnnotation.of("undo")],
});
}
});
this.undoManager.setOnPop((isUndo, value, counterRange) => {
var _a, _b;
const anchor = (_a = value.cursors[0]) !== null && _a !== void 0 ? _a : undefined;
const head = (_b = value.cursors[1]) !== null && _b !== void 0 ? _b : undefined;
if (!anchor)
return;
setTimeout(() => {
const anchorPos = this.doc.getCursorPos(anchor).offset;
const headPos = head
? this.doc.getCursorPos(head).offset
: anchorPos;
const selection = EditorSelection.single(anchorPos, headPos);
this.view.dispatch({
selection,
effects: [EditorView.scrollIntoView(selection.ranges[0])],
});
}, 0);
});
this.undoManager.setOnPush((isUndo, counterRange) => {
const cursors = [];
let selection = this.lastSelection;
if (!isUndo) {
const stateSelection = this.view.state.selection.main;
selection.anchor = this.getTextFromDoc(this.doc).getCursor(stateSelection.anchor);
selection.head = this.getTextFromDoc(this.doc).getCursor(stateSelection.head);
}
if (selection.anchor) {
cursors.push(selection.anchor);
}
if (selection.head) {
cursors.push(selection.head);
}
return {
value: null,
cursors,
};
});
}
update(update) {
if (update.selectionSet) {
this.lastSelection = {
anchor: this.getTextFromDoc(this.doc).getCursor(update.state.selection.main.anchor),
head: this.getTextFromDoc(this.doc).getCursor(update.state.selection.main.head),
};
}
}
destroy() {
var _a;
(_a = this.sub) === null || _a === void 0 ? void 0 : _a.call(this);
this.sub = undefined;
}
}
export const undo = (view) => {
view.dispatch({
effects: [undoEffect.of(null)],
});
return true;
};
export const redo = (view) => {
view.dispatch({
effects: [redoEffect.of(null)],
});
return true;
};
export const undoKeyMap = [
{
key: "Mod-z",
run: undo,
preventDefault: true,
},
{
key: "Mod-y",
mac: "Mod-Shift-z",
run: redo,
preventDefault: true,
},
{
key: "Mod-Shift-z",
run: redo,
preventDefault: true,
},
];