UNPKG

8.75 kBJavaScriptView Raw
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 */
30define(function (require, exports, module) {
31 "use strict";
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});