1 | /*
|
2 | * Copyright (c) 2012 Adobe Systems Incorporated. All rights reserved.
|
3 | *
|
4 | * Permission is hereby granted, free of charge, to any person obtaining a
|
5 | * copy of this software and associated documentation files (the "Software"),
|
6 | * to deal in the Software without restriction, including without limitation
|
7 | * the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
8 | * and/or sell copies of the Software, and to permit persons to whom the
|
9 | * Software is furnished to do so, subject to the following conditions:
|
10 | *
|
11 | * The above copyright notice and this permission notice shall be included in
|
12 | * all copies or substantial portions of the Software.
|
13 | *
|
14 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
15 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
16 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
17 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
18 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
19 | * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
20 | * DEALINGS IN THE SOFTWARE.
|
21 | *
|
22 | */
|
23 |
|
24 |
|
25 | /*jslint vars: true, plusplus: true, devel: true, nomen: true, indent: 4, maxerr: 50 */
|
26 | /*global define, $, CodeMirror */
|
27 |
|
28 | /**
|
29 | */
|
30 | define(function (require, exports, module) {
|
31 | ;
|
32 |
|
33 | /**
|
34 | * @constructor
|
35 | *
|
36 | * Stores a range of lines that is automatically maintained as the Document changes. The range
|
37 | * MAY drop out of sync with the Document in certain edge cases; startLine & endLine will become
|
38 | * null when that happens.
|
39 | *
|
40 | * Important: you must dispose() a TextRange when you're done with it. Because TextRange addRef()s
|
41 | * the Document (in order to listen to it), you will leak Documents otherwise.
|
42 | *
|
43 | * TextRange dispatches two events:
|
44 | * - change -- When the range changes (due to a Document change)
|
45 | * - lostSync -- When the backing Document changes in such a way that the range can no longer
|
46 | * accurately be maintained. Generally, occurs whenever an edit spans a range boundary.
|
47 | * After this, startLine & endLine will be unusable (set to null).
|
48 | * Also occurs when the document is deleted, though startLine & endLine won't be modified
|
49 | * These events only ever occur in response to Document changes, so if you are already listening
|
50 | * to the Document, you could ignore the TextRange events and just read its updated value in your
|
51 | * own Document change handler.
|
52 | *
|
53 | * @param {!Document} document
|
54 | * @param {number} startLine First line in range (0-based, inclusive)
|
55 | * @param {number} endLine Last line in range (0-based, inclusive)
|
56 | */
|
57 | function TextRange(document, startLine, endLine) {
|
58 | this.startLine = startLine;
|
59 | this.endLine = endLine;
|
60 |
|
61 | this.document = document;
|
62 | document.addRef();
|
63 | // store this-bound versions of listeners so we can remove them later
|
64 | this._handleDocumentChange = this._handleDocumentChange.bind(this);
|
65 | this._handleDocumentDeleted = this._handleDocumentDeleted.bind(this);
|
66 | $(document).on("change", this._handleDocumentChange);
|
67 | $(document).on("deleted", this._handleDocumentDeleted);
|
68 | }
|
69 |
|
70 | /** Detaches from the Document. The TextRange will no longer update or send change events */
|
71 | TextRange.prototype.dispose = function (editor, change) {
|
72 | // Disconnect from Document
|
73 | this.document.releaseRef();
|
74 | $(this.document).off("change", this._handleDocumentChange);
|
75 | $(this.document).off("deleted", this._handleDocumentDeleted);
|
76 | };
|
77 |
|
78 |
|
79 | /** @type {!Document} */
|
80 | TextRange.prototype.document = null;
|
81 | /** @type {?number} Null after "lostSync" is dispatched */
|
82 | TextRange.prototype.startLine = null;
|
83 | /** @type {?number} Null after "lostSync" is dispatched */
|
84 | TextRange.prototype.endLine = null;
|
85 |
|
86 |
|
87 | /**
|
88 | * Applies a single Document change object (out of the linked list of multiple such objects)
|
89 | * to this range. Returns true if the range was changed as a result.
|
90 | */
|
91 | TextRange.prototype._applySingleChangeToRange = function (change) {
|
92 | // console.log(this + " applying change to (" +
|
93 | // (change.from && (change.from.line+","+change.from.ch)) + " - " +
|
94 | // (change.to && (change.to.line+","+change.to.ch)) + ")");
|
95 |
|
96 | // Special case: the range is no longer meaningful since the entire text was replaced
|
97 | if (!change.from || !change.to) {
|
98 | this.startLine = null;
|
99 | this.endLine = null;
|
100 | return true;
|
101 |
|
102 | // Special case: certain changes around the edges of the range are problematic, because
|
103 | // if they're undone, we'll be unable to determine how to fix up the range to include the
|
104 | // undone content. (The "undo" will just look like an insertion outside our bounds.) So
|
105 | // in those cases, we destroy the range instead of fixing it up incorrectly. The specific
|
106 | // cases are:
|
107 | // 1. Edit crosses the start boundary of the inline editor (defined as character 0
|
108 | // of the first line).
|
109 | // 2. Edit crosses the end boundary of the inline editor (defined as the newline at
|
110 | // the end of the last line).
|
111 | // Note: we also used to disallow edits that start at the beginning of the range (character 0
|
112 | // of the first line) if they crossed a newline. This was a vestige from before case #1
|
113 | // was added; now that edits crossing the top boundary (actually, undos of such edits) are
|
114 | // out of the picture, edits on the first line of the range unambiguously belong inside it.
|
115 | } else if ((change.from.line < this.startLine && change.to.line >= this.startLine) ||
|
116 | (change.from.line <= this.endLine && change.to.line > this.endLine)) {
|
117 | this.startLine = null;
|
118 | this.endLine = null;
|
119 | return true;
|
120 |
|
121 | // Normal case: update the range end points if any content was added before them. Note that
|
122 | // we don't rely on line handles for this since we want to gracefully handle cases where the
|
123 | // start or end line was deleted during a change.
|
124 | } else {
|
125 | var numAdded = change.text.length - (change.to.line - change.from.line + 1);
|
126 | var hasChanged = false;
|
127 |
|
128 | // This logic is so simple because we've already excluded all cases where the change
|
129 | // crosses the range boundaries
|
130 | if (change.to.line < this.startLine) {
|
131 | this.startLine += numAdded;
|
132 | hasChanged = true;
|
133 | }
|
134 | if (change.to.line <= this.endLine) {
|
135 | this.endLine += numAdded;
|
136 | hasChanged = true;
|
137 | }
|
138 |
|
139 | // console.log("Now " + this);
|
140 |
|
141 | return hasChanged;
|
142 | }
|
143 | };
|
144 |
|
145 | /**
|
146 | * Updates the range based on the changeList from a Document "change" event. Dispatches a
|
147 | * "change" event if the range was adjusted at all. Dispatches a "lostSync" event instead if the
|
148 | * range can no longer be accurately maintained.
|
149 | */
|
150 | TextRange.prototype._applyChangesToRange = function (changeList) {
|
151 | var hasChanged = false;
|
152 | var change;
|
153 | for (change = changeList; change; change = change.next) {
|
154 | // Apply this step of the change list
|
155 | var result = this._applySingleChangeToRange(change);
|
156 | hasChanged = hasChanged || result;
|
157 |
|
158 | // If we lost sync with the range, just bail now
|
159 | if (this.startLine === null || this.endLine === null) {
|
160 | $(this).triggerHandler("lostSync");
|
161 | break;
|
162 | }
|
163 | }
|
164 |
|
165 | if (hasChanged) {
|
166 | $(this).triggerHandler("change");
|
167 | }
|
168 | };
|
169 |
|
170 | TextRange.prototype._handleDocumentChange = function (event, doc, changeList) {
|
171 | this._applyChangesToRange(changeList);
|
172 | };
|
173 |
|
174 | TextRange.prototype._handleDocumentDeleted = function (event) {
|
175 | $(this).triggerHandler("lostSync");
|
176 | };
|
177 |
|
178 |
|
179 | /* (pretty toString(), to aid debugging) */
|
180 | TextRange.prototype.toString = function () {
|
181 | return "[TextRange " + this.startLine + "-" + this.endLine + " in " + this.document + "]";
|
182 | };
|
183 |
|
184 |
|
185 | // Define public API
|
186 | exports.TextRange = TextRange;
|
187 | });
|