Modules (188)

TextRange

Description

Dependencies

Classes

Constructor

TextRange

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:

  • change -- When the range boundary line numbers change (due to a Document change)
  • contentChange -- When the actual content of the range changes. This might or might not be accompanied by a change in the boundary line numbers.
  • lostSync -- When the backing Document changes in such a way that the range can no longer accurately be maintained. Generally, occurs whenever an edit spans a range boundary. After this, startLine & endLine will be unusable (set to null). Also occurs when the document is deleted, though startLine & endLine won't be modified These events only ever occur in response to Document changes, so if you are already listening to the Document, you could ignore the TextRange events and just read its updated value in your own Document change handler.
document Document
startLine number
First line in range (0-based, inclusive)
endLine number
Last line in range (0-based, inclusive)
    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);

Properties

document

Containing document

Type
!Document
    TextRange.prototype.document = null;

endLine

Ending Line

Type
?number
    TextRange.prototype.endLine = null;

startLine

Starting Line

Type
?number
    TextRange.prototype.startLine = null;

Methods

Private

_applyChangesToRange

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");
    };
Private

_applySingleChangeToRange

Applies a single Document change object (out of the linked list of multiple such objects) to this range.

change Object
The CodeMirror change record.
Returns: {hasChanged: boolean,hasContentChanged: boolean}
Whether the range boundary and/or content has changed.
    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;
        }
    };

dispose

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);
    };

toString

(pretty toString(), to aid debugging)

    TextRange.prototype.toString = function () {
        return "[TextRange " + this.startLine + "-" + this.endLine + " in " + this.document + "]";
    };


    // Define public API
    exports.TextRange = TextRange;
});