Stores a range of lines that is automatically maintained as the Document changes. The range MAY drop out of sync with the Document in certain edge cases; startLine & endLine will become null when that happens.
Important: you must dispose() a TextRange when you're done with it. Because TextRange addRef()s the Document (in order to listen to it), you will leak Documents otherwise.
TextRange dispatches these events:
function TextRange(document, startLine, endLine) {
this.startLine = startLine;
this.endLine = endLine;
this.document = document;
document.addRef();
// store this-bound versions of listeners so we can remove them later
this._handleDocumentChange = this._handleDocumentChange.bind(this);
this._handleDocumentDeleted = this._handleDocumentDeleted.bind(this);
document.on("change", this._handleDocumentChange);
document.on("deleted", this._handleDocumentDeleted);
}
EventDispatcher.makeEventDispatcher(TextRange.prototype);
Updates the range based on the changeList from a Document "change" event. Dispatches a "change" event if the range was adjusted at all. Dispatches a "lostSync" event instead if the range can no longer be accurately maintained.
TextRange.prototype._applyChangesToRange = function (changeList) {
var hasChanged = false, hasContentChanged = false;
var i;
for (i = 0; i < changeList.length; i++) {
// Apply this step of the change list
var result = this._applySingleChangeToRange(changeList[i]);
hasChanged = hasChanged || result.hasChanged;
hasContentChanged = hasContentChanged || result.hasContentChanged;
// If we lost sync with the range, just bail now
if (this.startLine === null || this.endLine === null) {
this.trigger("lostSync");
break;
}
}
if (hasChanged) {
this.trigger("change");
}
if (hasContentChanged) {
this.trigger("contentChange");
}
};
TextRange.prototype._handleDocumentChange = function (event, doc, changeList) {
this._applyChangesToRange(changeList);
};
TextRange.prototype._handleDocumentDeleted = function (event) {
this.trigger("lostSync");
};
Applies a single Document change object (out of the linked list of multiple such objects) to this range.
TextRange.prototype._applySingleChangeToRange = function (change) {
// console.log(this + " applying change to (" +
// (change.from && (change.from.line+","+change.from.ch)) + " - " +
// (change.to && (change.to.line+","+change.to.ch)) + ")");
// Special case: the range is no longer meaningful since the entire text was replaced
if (!change.from || !change.to) {
this.startLine = null;
this.endLine = null;
return {hasChanged: true, hasContentChanged: true};
// Special case: certain changes around the edges of the range are problematic, because
// if they're undone, we'll be unable to determine how to fix up the range to include the
// undone content. (The "undo" will just look like an insertion outside our bounds.) So
// in those cases, we destroy the range instead of fixing it up incorrectly. The specific
// cases are:
// 1. Edit crosses the start boundary of the inline editor (defined as character 0
// of the first line).
// 2. Edit crosses the end boundary of the inline editor (defined as the newline at
// the end of the last line).
// Note: we also used to disallow edits that start at the beginning of the range (character 0
// of the first line) if they crossed a newline. This was a vestige from before case #1
// was added; now that edits crossing the top boundary (actually, undos of such edits) are
// out of the picture, edits on the first line of the range unambiguously belong inside it.
} else if ((change.from.line < this.startLine && change.to.line >= this.startLine) ||
(change.from.line <= this.endLine && change.to.line > this.endLine)) {
this.startLine = null;
this.endLine = null;
return {hasChanged: true, hasContentChanged: true};
// Normal case: update the range end points if any content was added before them. Note that
// we don't rely on line handles for this since we want to gracefully handle cases where the
// start or end line was deleted during a change.
} else {
var numAdded = change.text.length - (change.to.line - change.from.line + 1);
var result = {hasChanged: false, hasContentChanged: false};
// This logic is so simple because we've already excluded all cases where the change
// crosses the range boundaries
if (numAdded !== 0) {
if (change.to.line < this.startLine) {
this.startLine += numAdded;
result.hasChanged = true;
}
if (change.to.line <= this.endLine) {
this.endLine += numAdded;
result.hasChanged = true;
}
}
if (change.from.line >= this.startLine && change.from.line <= this.endLine) {
// Since we know the change doesn't cross the range boundary, as long as the
// start of the change is within the range, we know the content changed.
result.hasContentChanged = true;
}
// console.log("Now " + this);
return result;
}
};
Detaches from the Document. The TextRange will no longer update or send change events
TextRange.prototype.dispose = function (editor, change) {
// Disconnect from Document
this.document.releaseRef();
this.document.off("change", this._handleDocumentChange);
this.document.off("deleted", this._handleDocumentDeleted);
};