Skip to content

Commit

Permalink
Change AceEditorRGA to a classic OO style.
Browse files Browse the repository at this point in the history
  • Loading branch information
jorendorff committed Apr 9, 2016
1 parent d61a197 commit f44c2d6
Showing 1 changed file with 85 additions and 79 deletions.
164 changes: 85 additions & 79 deletions lib/rga.js
Original file line number Diff line number Diff line change
Expand Up @@ -428,125 +428,131 @@ RGA.AceEditorRGA = function AceEditorRga(id, editor, history, queue) {
RGA.call(this, id, history, queue);
this.editor = editor;

// `lastText` is the text that was in the editor, last we checked. When a
// `_lastText` is the text that was in the editor, last we checked. When a
// keypress happens and the text in the editor changes, Ace will notify us of
// the change, but not immediately: instead, it queues an event to fire
// asynchronously. In fact, at any given point in time we have no way of
// knowing whether we've been notified of all changes or something's still in
// flight. The solution is brute force (see takeUserEdits) and requires us to
// remember the last known in-sync state of the document, hence this
// variable.
var lastText = this.text();
this._lastText = this.text();

// The editor must start out in sync with the RGA.
// (The `-1` here means to place the editor cursor at the start of the document.)
editor.setValue(this._lastText, -1);

// The flow of operations is (unavoidably) bidirectional. First, when Ace
// notifies us of an edit, use _takeUserEdits to fold those changes into the
// RGA.
var self = this;
function assertInSync(infodump) {
let erText = self.text();
if (lastText != erText) {
infodump.lastText = lastText;
this._changeCallback = function () { self._takeUserEdits() };
editor.getSession().on("change", this._changeCallback);

// Now for the other direction. Here we replace the callback that
// receives changes from other RGAs.
this.downstream = function rgaToEditor(source, op) {
// Always check for new user edits *before* accepting ops from the internet.
// That way, _takeUserEdits() knows that all differences between
// `_lastText` and `editor.getValue()` are the result of new user input.
self._takeUserEdits();

// Since applyOpToEditor uses the RGA to look up the location of the
// inserted/deleted character in the document, and determine whether it has in fact
// already been inserted/deleted, we have to call that first,
// before modifying the RGA.
self._applyOpToEditor(op); // first update the editor
self._downstream.call(self, source, op); // then update the RGA
self._lastText = self.editor.getValue();

self._assertInSync({op: op});
};
this.downstream._id = id;
};

RGA.AceEditorRGA.prototype = Object.create(RGA.prototype);
Object.assign(RGA.AceEditorRGA.prototype, {
addRight: function () {
throw new Error("calling addRight on an AceEditorRGA is not supported");
},

remove: function () {
throw new Error("calling remove on an AceEditorRGA is not supported");
},

_assertInSync: function () {
let erText = this.text();
if (this._lastText != erText) {
infodump.lastText = this._lastText;
infodump.rgaText = erText;
console.error(self.id, "lastText and rga are out of sync", infodump);
console.error(this.id, "lastText and rga are out of sync", infodump);
throw new Error("editor and RGA data structure got out of sync");
}
}
},

// The editor must start out in sync with the RGA.
// (The `-1` here means to place the editor cursor at the start of the document.)
editor.setValue(lastText, -1);

// The flow of operations is (unavoidably) bidirectional.
// First, propagate user edits from Ace to the RGA.
function takeUserEdits() {
var currentText = editor.getValue();
//self._log("takeUserEdits!! <" + currentText + "> <" + lastText + ">");
if (currentText != lastText) {
assertInSync({currentEditorState: currentText});

var changes = RGA.diff(lastText, currentText);
//self._log(changes);
self.applyDelta(changes);
var savedLastText = lastText;
lastText = currentText;

assertInSync({before: savedLastText, patch: changes});
_takeUserEdits: function () {
var currentText = this.editor.getValue();
//this._log("_takeUserEdits: <" + currentText + "> <" + this._lastText + ">");
if (currentText != this._lastText) {
this._assertInSync({currentEditorState: currentText});

var changes = RGA.diff(this._lastText, currentText);
//this._log(changes);
this.applyDelta(changes);
var savedLastText = this._lastText;
this._lastText = currentText;

this._assertInSync({before: savedLastText, patch: changes});
}
}
var editorSession = editor.getSession();
editorSession.on("change", takeUserEdits);
function withEditorCallbacksDisabled(action) {
editorSession.off("change", takeUserEdits);
action();
editorSession.on("change", takeUserEdits);
}
},

// Now for the other direction: deliver ops from the RGA to the editor.
_withEditorCallbacksDisabled: function(action) {
this.editor.getSession().off("change", this._changeCallback);
action();
this.editor.getSession().on("change", this._changeCallback);
},

// Apply an RGA op to the Ace editor.
function applyOpToEditor(op) {
_applyOpToEditor: function (op) {
var editor = this.editor;
var session = editor.getSession();
switch (op.type) {
case "addRight":
if (self._index.has(op.w.timestamp)) {
if (this._index.has(op.w.timestamp)) {
// This character was already added.
throw new Error("bug - message delivered twice to " + self._id + ": ", JSON.stringify(op));
throw new Error("bug - message delivered twice to " + this._id + ": ", JSON.stringify(op));
}

var loc = self.getRowColumnAfter(op.t, op.w.timestamp);
//self._log("inserting character", op.w.atom, "at", loc);
withEditorCallbacksDisabled(function () {
editorSession.insert(loc, op.w.atom);
var loc = this.getRowColumnAfter(op.t, op.w.timestamp);
//this._log("inserting character", op.w.atom, "at", loc);
this._withEditorCallbacksDisabled(function () {
session.insert(loc, op.w.atom);
});
break;

case "remove":
//self._log("remove:", op.t, " from:", self);
if (self._index.get(op.t).removed) {
//this._log("remove:", op.t, " from:", this);
if (this._index.get(op.t).removed) {
// This character has already been removed. Nothing to do.
break;
}

var loc = self.getRowColumnBefore(op.t);
var removingNewline = editorSession.getDocument().getLine(loc.row).length === loc.column;
var loc = this.getRowColumnBefore(op.t);
var removingNewline = session.getDocument().getLine(loc.row).length === loc.column;
var whatToRemove = {
start: loc,
end: removingNewline
? {row: loc.row + 1, column: 0}
: {row: loc.row, column: loc.column + 1}
};
//self._log("removing from editor:", whatToRemove);
withEditorCallbacksDisabled(function () {
editorSession.remove(whatToRemove);
//this._log("removing from editor:", whatToRemove);
this._withEditorCallbacksDisabled(function () {
session.remove(whatToRemove);
});
break;
}
}

var _base_downstream = this.downstream;
this.downstream = function rgaToEditor(source, op) {
// Always check for new user edits *before* accepting ops from the internet.
// That way, takeUserEdits() knows that all differences between
// `lastText` and `editor.getValue()` are the result of new user input.
takeUserEdits();

//self._log("editorReplica.downstream: received op (from socket):", op);

// Since applyOpToEditor uses the RGA to look up the location of the
// inserted/deleted character in the document, and determine whether it has in fact
// already been inserted/deleted, we have to call that first,
// before modifying the RGA.
applyOpToEditor(op); // first update the editor
_base_downstream.call(this, source, op); // then update the RGA
lastText = self.editor.getValue();
assertInSync({op: op});
};
this.downstream._id = id;
};

RGA.AceEditorRGA.prototype = Object.create(RGA.prototype);
RGA.AceEditorRGA.prototype.addRight = function () {
throw new Error("calling addRight on an AceEditorRGA is not supported");
};
RGA.AceEditorRGA.prototype.remove = function () {
throw new Error("calling remove on an AceEditorRGA is not supported");
};
});

if (typeof module !== "undefined")
exports = module.exports = RGA;

0 comments on commit f44c2d6

Please sign in to comment.