-
-
Notifications
You must be signed in to change notification settings - Fork 194
Expand file tree
/
Copy pathTextRange.js
More file actions
217 lines (186 loc) · 8.96 KB
/
TextRange.js
File metadata and controls
217 lines (186 loc) · 8.96 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
/*
* GNU AGPL-3.0 License
*
* Copyright (c) 2021 - present core.ai . All rights reserved.
* Original work Copyright (c) 2012 - 2021 Adobe Systems Incorporated. All rights reserved.
*
* This program is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License
* for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see https://opensource.org/licenses/AGPL-3.0.
*
*/
// @INCLUDE_IN_API_DOCS
/**
*/
define(function (require, exports, module) {
var EventDispatcher = require("utils/EventDispatcher");
/**
* 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.
*
* @constructor
*
* @param {!Document} document
* @param {number} startLine First line in range (0-based, inclusive)
* @param {number} endLine 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);
/** 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);
};
/**
* Containing document
* @type {!Document}
*/
TextRange.prototype.document = null;
/**
* Starting Line
* @type {?number} Null after "lostSync" is dispatched
*/
TextRange.prototype.startLine = null;
/**
* Ending Line
* @type {?number} Null after "lostSync" is dispatched
*/
TextRange.prototype.endLine = null;
/**
* Applies a single Document change object (out of the linked list of multiple such objects)
* to this range.
* @private
* @param {Object} change The CodeMirror change record.
* @return {{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.
}
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;
};
/**
* 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.
* @private
*/
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");
};
/* (pretty toString(), to aid debugging) */
TextRange.prototype.toString = function () {
return "[TextRange " + this.startLine + "-" + this.endLine + " in " + this.document + "]";
};
// Define public API
exports.TextRange = TextRange;
});