livebook/assets/js/editor/monaco_editor_adapter.js
Jonatan Kłosko 3e6a4adce2
Implement collaborative text editing (#10)
* Set up editor and client side delta handling

* Synchronize deltas on the server

* Document the client code, add more tests

* Implement delta on the server, use compact representation when transmitting changes

* Simplify transformation implementation and add documentation

* Add session and data tests

* Add more delta tests

* Clean up evaluator tests wait timeout
2021-01-21 13:11:45 +01:00

112 lines
2.5 KiB
JavaScript

import monaco from "./monaco";
import Delta, { isDelete, isInsert, isRetain } from "../lib/delta";
/**
* Encapsulates logic related to getting/applying changes to the editor.
*
* Uses the given Monaco editor instance.
*/
export default class MonacoEditorAdapter {
constructor(editor) {
this.editor = editor;
this._onDelta = null;
this.editor.onDidChangeModelContent((event) => {
if (this.ignoreChange) {
return;
}
const delta = this.__deltaFromEditorChange(event);
this._onDelta && this._onDelta(delta);
});
}
/**
* Registers a callback called whenever the user makes a change
* to the editor content. The change is represented by a delta object.
*/
onDelta(callback) {
this._onDelta = callback;
}
/**
* Applies the given delta to the editor content.
*/
applyDelta(delta) {
const operations = this.__deltaToEditorOperations(delta);
this.ignoreChange = true;
this.editor.getModel().pushEditOperations([], operations);
this.ignoreChange = false;
}
__deltaFromEditorChange(event) {
const deltas = event.changes.map((change) => {
const { rangeOffset, rangeLength, text } = change;
const delta = new Delta();
if (rangeOffset) {
delta.retain(rangeOffset);
}
if (rangeLength) {
delta.delete(rangeLength);
}
if (text) {
delta.insert(text);
}
return delta;
});
return deltas.reduce((delta1, delta2) => delta1.compose(delta2));
}
__deltaToEditorOperations(delta) {
const model = this.editor.getModel();
const operations = [];
let index = 0;
delta.ops.forEach((op) => {
if (isRetain(op)) {
index += op.retain;
}
if (isInsert(op)) {
const start = model.getPositionAt(index);
operations.push({
forceMoveMarkers: true,
range: new monaco.Range(
start.lineNumber,
start.column,
start.lineNumber,
start.column
),
text: op.insert,
});
}
if (isDelete(op)) {
const start = model.getPositionAt(index);
const end = model.getPositionAt(index + op.delete);
operations.push({
forceMoveMarkers: false,
range: new monaco.Range(
start.lineNumber,
start.column,
end.lineNumber,
end.column
),
text: null,
});
index += op.delete;
}
});
return operations;
}
}