Mercurial Hosting > nabble
comparison src/nabble/view/web/util/codemirror/js/undo.js @ 0:7ecd1a4ef557
add content
author | Franklin Schmidt <fschmidt@gmail.com> |
---|---|
date | Thu, 21 Mar 2019 19:15:52 -0600 |
parents | |
children |
comparison
equal
deleted
inserted
replaced
-1:000000000000 | 0:7ecd1a4ef557 |
---|---|
1 /** | |
2 * Storage and control for undo information within a CodeMirror | |
3 * editor. 'Why on earth is such a complicated mess required for | |
4 * that?', I hear you ask. The goal, in implementing this, was to make | |
5 * the complexity of storing and reverting undo information depend | |
6 * only on the size of the edited or restored content, not on the size | |
7 * of the whole document. This makes it necessary to use a kind of | |
8 * 'diff' system, which, when applied to a DOM tree, causes some | |
9 * complexity and hackery. | |
10 * | |
11 * In short, the editor 'touches' BR elements as it parses them, and | |
12 * the UndoHistory stores these. When nothing is touched in commitDelay | |
13 * milliseconds, the changes are committed: It goes over all touched | |
14 * nodes, throws out the ones that did not change since last commit or | |
15 * are no longer in the document, and assembles the rest into zero or | |
16 * more 'chains' -- arrays of adjacent lines. Links back to these | |
17 * chains are added to the BR nodes, while the chain that previously | |
18 * spanned these nodes is added to the undo history. Undoing a change | |
19 * means taking such a chain off the undo history, restoring its | |
20 * content (text is saved per line) and linking it back into the | |
21 * document. | |
22 */ | |
23 | |
24 // A history object needs to know about the DOM container holding the | |
25 // document, the maximum amount of undo levels it should store, the | |
26 // delay (of no input) after which it commits a set of changes, and, | |
27 // unfortunately, the 'parent' window -- a window that is not in | |
28 // designMode, and on which setTimeout works in every browser. | |
29 function UndoHistory(container, maxDepth, commitDelay, editor) { | |
30 this.container = container; | |
31 this.maxDepth = maxDepth; this.commitDelay = commitDelay; | |
32 this.editor = editor; | |
33 // This line object represents the initial, empty editor. | |
34 var initial = {text: "", from: null, to: null}; | |
35 // As the borders between lines are represented by BR elements, the | |
36 // start of the first line and the end of the last one are | |
37 // represented by null. Since you can not store any properties | |
38 // (links to line objects) in null, these properties are used in | |
39 // those cases. | |
40 this.first = initial; this.last = initial; | |
41 // Similarly, a 'historyTouched' property is added to the BR in | |
42 // front of lines that have already been touched, and 'firstTouched' | |
43 // is used for the first line. | |
44 this.firstTouched = false; | |
45 // History is the set of committed changes, touched is the set of | |
46 // nodes touched since the last commit. | |
47 this.history = []; this.redoHistory = []; this.touched = []; this.lostundo = 0; | |
48 } | |
49 | |
50 UndoHistory.prototype = { | |
51 // Schedule a commit (if no other touches come in for commitDelay | |
52 // milliseconds). | |
53 scheduleCommit: function() { | |
54 var self = this; | |
55 parent.clearTimeout(this.commitTimeout); | |
56 this.commitTimeout = parent.setTimeout(function(){self.tryCommit();}, this.commitDelay); | |
57 }, | |
58 | |
59 // Mark a node as touched. Null is a valid argument. | |
60 touch: function(node) { | |
61 this.setTouched(node); | |
62 this.scheduleCommit(); | |
63 }, | |
64 | |
65 // Undo the last change. | |
66 undo: function() { | |
67 // Make sure pending changes have been committed. | |
68 this.commit(); | |
69 | |
70 if (this.history.length) { | |
71 // Take the top diff from the history, apply it, and store its | |
72 // shadow in the redo history. | |
73 var item = this.history.pop(); | |
74 this.redoHistory.push(this.updateTo(item, "applyChain")); | |
75 this.notifyEnvironment(); | |
76 return this.chainNode(item); | |
77 } | |
78 }, | |
79 | |
80 // Redo the last undone change. | |
81 redo: function() { | |
82 this.commit(); | |
83 if (this.redoHistory.length) { | |
84 // The inverse of undo, basically. | |
85 var item = this.redoHistory.pop(); | |
86 this.addUndoLevel(this.updateTo(item, "applyChain")); | |
87 this.notifyEnvironment(); | |
88 return this.chainNode(item); | |
89 } | |
90 }, | |
91 | |
92 clear: function() { | |
93 this.history = []; | |
94 this.redoHistory = []; | |
95 this.lostundo = 0; | |
96 }, | |
97 | |
98 // Ask for the size of the un/redo histories. | |
99 historySize: function() { | |
100 return {undo: this.history.length, redo: this.redoHistory.length, lostundo: this.lostundo}; | |
101 }, | |
102 | |
103 // Push a changeset into the document. | |
104 push: function(from, to, lines) { | |
105 var chain = []; | |
106 for (var i = 0; i < lines.length; i++) { | |
107 var end = (i == lines.length - 1) ? to : document.createElement("br"); | |
108 chain.push({from: from, to: end, text: cleanText(lines[i])}); | |
109 from = end; | |
110 } | |
111 this.pushChains([chain], from == null && to == null); | |
112 this.notifyEnvironment(); | |
113 }, | |
114 | |
115 pushChains: function(chains, doNotHighlight) { | |
116 this.commit(doNotHighlight); | |
117 this.addUndoLevel(this.updateTo(chains, "applyChain")); | |
118 this.redoHistory = []; | |
119 }, | |
120 | |
121 // Retrieve a DOM node from a chain (for scrolling to it after undo/redo). | |
122 chainNode: function(chains) { | |
123 for (var i = 0; i < chains.length; i++) { | |
124 var start = chains[i][0], node = start && (start.from || start.to); | |
125 if (node) return node; | |
126 } | |
127 }, | |
128 | |
129 // Clear the undo history, make the current document the start | |
130 // position. | |
131 reset: function() { | |
132 this.history = []; this.redoHistory = []; this.lostundo = 0; | |
133 }, | |
134 | |
135 textAfter: function(br) { | |
136 return this.after(br).text; | |
137 }, | |
138 | |
139 nodeAfter: function(br) { | |
140 return this.after(br).to; | |
141 }, | |
142 | |
143 nodeBefore: function(br) { | |
144 return this.before(br).from; | |
145 }, | |
146 | |
147 // Commit unless there are pending dirty nodes. | |
148 tryCommit: function() { | |
149 if (!window || !window.parent || !window.UndoHistory) return; // Stop when frame has been unloaded | |
150 if (this.editor.highlightDirty()) this.commit(true); | |
151 else this.scheduleCommit(); | |
152 }, | |
153 | |
154 // Check whether the touched nodes hold any changes, if so, commit | |
155 // them. | |
156 commit: function(doNotHighlight) { | |
157 parent.clearTimeout(this.commitTimeout); | |
158 // Make sure there are no pending dirty nodes. | |
159 if (!doNotHighlight) this.editor.highlightDirty(true); | |
160 // Build set of chains. | |
161 var chains = this.touchedChains(), self = this; | |
162 | |
163 if (chains.length) { | |
164 this.addUndoLevel(this.updateTo(chains, "linkChain")); | |
165 this.redoHistory = []; | |
166 this.notifyEnvironment(); | |
167 } | |
168 }, | |
169 | |
170 // [ end of public interface ] | |
171 | |
172 // Update the document with a given set of chains, return its | |
173 // shadow. updateFunc should be "applyChain" or "linkChain". In the | |
174 // second case, the chains are taken to correspond the the current | |
175 // document, and only the state of the line data is updated. In the | |
176 // first case, the content of the chains is also pushed iinto the | |
177 // document. | |
178 updateTo: function(chains, updateFunc) { | |
179 var shadows = [], dirty = []; | |
180 for (var i = 0; i < chains.length; i++) { | |
181 shadows.push(this.shadowChain(chains[i])); | |
182 dirty.push(this[updateFunc](chains[i])); | |
183 } | |
184 if (updateFunc == "applyChain") | |
185 this.notifyDirty(dirty); | |
186 return shadows; | |
187 }, | |
188 | |
189 // Notify the editor that some nodes have changed. | |
190 notifyDirty: function(nodes) { | |
191 forEach(nodes, method(this.editor, "addDirtyNode")) | |
192 this.editor.scheduleHighlight(); | |
193 }, | |
194 | |
195 notifyEnvironment: function() { | |
196 if (this.onChange) this.onChange(this.editor); | |
197 // Used by the line-wrapping line-numbering code. | |
198 if (window.frameElement && window.frameElement.CodeMirror.updateNumbers) | |
199 window.frameElement.CodeMirror.updateNumbers(); | |
200 }, | |
201 | |
202 // Link a chain into the DOM nodes (or the first/last links for null | |
203 // nodes). | |
204 linkChain: function(chain) { | |
205 for (var i = 0; i < chain.length; i++) { | |
206 var line = chain[i]; | |
207 if (line.from) line.from.historyAfter = line; | |
208 else this.first = line; | |
209 if (line.to) line.to.historyBefore = line; | |
210 else this.last = line; | |
211 } | |
212 }, | |
213 | |
214 // Get the line object after/before a given node. | |
215 after: function(node) { | |
216 return node ? node.historyAfter : this.first; | |
217 }, | |
218 before: function(node) { | |
219 return node ? node.historyBefore : this.last; | |
220 }, | |
221 | |
222 // Mark a node as touched if it has not already been marked. | |
223 setTouched: function(node) { | |
224 if (node) { | |
225 if (!node.historyTouched) { | |
226 this.touched.push(node); | |
227 node.historyTouched = true; | |
228 } | |
229 } | |
230 else { | |
231 this.firstTouched = true; | |
232 } | |
233 }, | |
234 | |
235 // Store a new set of undo info, throw away info if there is more of | |
236 // it than allowed. | |
237 addUndoLevel: function(diffs) { | |
238 this.history.push(diffs); | |
239 if (this.history.length > this.maxDepth) { | |
240 this.history.shift(); | |
241 this.lostundo += 1; | |
242 } | |
243 }, | |
244 | |
245 // Build chains from a set of touched nodes. | |
246 touchedChains: function() { | |
247 var self = this; | |
248 | |
249 // The temp system is a crummy hack to speed up determining | |
250 // whether a (currently touched) node has a line object associated | |
251 // with it. nullTemp is used to store the object for the first | |
252 // line, other nodes get it stored in their historyTemp property. | |
253 var nullTemp = null; | |
254 function temp(node) {return node ? node.historyTemp : nullTemp;} | |
255 function setTemp(node, line) { | |
256 if (node) node.historyTemp = line; | |
257 else nullTemp = line; | |
258 } | |
259 | |
260 function buildLine(node) { | |
261 var text = []; | |
262 for (var cur = node ? node.nextSibling : self.container.firstChild; | |
263 cur && (!isBR(cur) || cur.hackBR); cur = cur.nextSibling) | |
264 if (!cur.hackBR && cur.currentText) text.push(cur.currentText); | |
265 return {from: node, to: cur, text: cleanText(text.join(""))}; | |
266 } | |
267 | |
268 // Filter out unchanged lines and nodes that are no longer in the | |
269 // document. Build up line objects for remaining nodes. | |
270 var lines = []; | |
271 if (self.firstTouched) self.touched.push(null); | |
272 forEach(self.touched, function(node) { | |
273 if (node && (node.parentNode != self.container || node.hackBR)) return; | |
274 | |
275 if (node) node.historyTouched = false; | |
276 else self.firstTouched = false; | |
277 | |
278 var line = buildLine(node), shadow = self.after(node); | |
279 if (!shadow || shadow.text != line.text || shadow.to != line.to) { | |
280 lines.push(line); | |
281 setTemp(node, line); | |
282 } | |
283 }); | |
284 | |
285 // Get the BR element after/before the given node. | |
286 function nextBR(node, dir) { | |
287 var link = dir + "Sibling", search = node[link]; | |
288 while (search && !isBR(search)) | |
289 search = search[link]; | |
290 return search; | |
291 } | |
292 | |
293 // Assemble line objects into chains by scanning the DOM tree | |
294 // around them. | |
295 var chains = []; self.touched = []; | |
296 forEach(lines, function(line) { | |
297 // Note that this makes the loop skip line objects that have | |
298 // been pulled into chains by lines before them. | |
299 if (!temp(line.from)) return; | |
300 | |
301 var chain = [], curNode = line.from, safe = true; | |
302 // Put any line objects (referred to by temp info) before this | |
303 // one on the front of the array. | |
304 while (true) { | |
305 var curLine = temp(curNode); | |
306 if (!curLine) { | |
307 if (safe) break; | |
308 else curLine = buildLine(curNode); | |
309 } | |
310 chain.unshift(curLine); | |
311 setTemp(curNode, null); | |
312 if (!curNode) break; | |
313 safe = self.after(curNode); | |
314 curNode = nextBR(curNode, "previous"); | |
315 } | |
316 curNode = line.to; safe = self.before(line.from); | |
317 // Add lines after this one at end of array. | |
318 while (true) { | |
319 if (!curNode) break; | |
320 var curLine = temp(curNode); | |
321 if (!curLine) { | |
322 if (safe) break; | |
323 else curLine = buildLine(curNode); | |
324 } | |
325 chain.push(curLine); | |
326 setTemp(curNode, null); | |
327 safe = self.before(curNode); | |
328 curNode = nextBR(curNode, "next"); | |
329 } | |
330 chains.push(chain); | |
331 }); | |
332 | |
333 return chains; | |
334 }, | |
335 | |
336 // Find the 'shadow' of a given chain by following the links in the | |
337 // DOM nodes at its start and end. | |
338 shadowChain: function(chain) { | |
339 var shadows = [], next = this.after(chain[0].from), end = chain[chain.length - 1].to; | |
340 while (true) { | |
341 shadows.push(next); | |
342 var nextNode = next.to; | |
343 if (!nextNode || nextNode == end) | |
344 break; | |
345 else | |
346 next = nextNode.historyAfter || this.before(end); | |
347 // (The this.before(end) is a hack -- FF sometimes removes | |
348 // properties from BR nodes, in which case the best we can hope | |
349 // for is to not break.) | |
350 } | |
351 return shadows; | |
352 }, | |
353 | |
354 // Update the DOM tree to contain the lines specified in a given | |
355 // chain, link this chain into the DOM nodes. | |
356 applyChain: function(chain) { | |
357 // Some attempt is made to prevent the cursor from jumping | |
358 // randomly when an undo or redo happens. It still behaves a bit | |
359 // strange sometimes. | |
360 var cursor = select.cursorPos(this.container, false), self = this; | |
361 | |
362 // Remove all nodes in the DOM tree between from and to (null for | |
363 // start/end of container). | |
364 function removeRange(from, to) { | |
365 var pos = from ? from.nextSibling : self.container.firstChild; | |
366 while (pos != to) { | |
367 var temp = pos.nextSibling; | |
368 removeElement(pos); | |
369 pos = temp; | |
370 } | |
371 } | |
372 | |
373 var start = chain[0].from, end = chain[chain.length - 1].to; | |
374 // Clear the space where this change has to be made. | |
375 removeRange(start, end); | |
376 | |
377 // Insert the content specified by the chain into the DOM tree. | |
378 for (var i = 0; i < chain.length; i++) { | |
379 var line = chain[i]; | |
380 // The start and end of the space are already correct, but BR | |
381 // tags inside it have to be put back. | |
382 if (i > 0) | |
383 self.container.insertBefore(line.from, end); | |
384 | |
385 // Add the text. | |
386 var node = makePartSpan(fixSpaces(line.text)); | |
387 self.container.insertBefore(node, end); | |
388 // See if the cursor was on this line. Put it back, adjusting | |
389 // for changed line length, if it was. | |
390 if (cursor && cursor.node == line.from) { | |
391 var cursordiff = 0; | |
392 var prev = this.after(line.from); | |
393 if (prev && i == chain.length - 1) { | |
394 // Only adjust if the cursor is after the unchanged part of | |
395 // the line. | |
396 for (var match = 0; match < cursor.offset && | |
397 line.text.charAt(match) == prev.text.charAt(match); match++){} | |
398 if (cursor.offset > match) | |
399 cursordiff = line.text.length - prev.text.length; | |
400 } | |
401 select.setCursorPos(this.container, {node: line.from, offset: Math.max(0, cursor.offset + cursordiff)}); | |
402 } | |
403 // Cursor was in removed line, this is last new line. | |
404 else if (cursor && (i == chain.length - 1) && cursor.node && cursor.node.parentNode != this.container) { | |
405 select.setCursorPos(this.container, {node: line.from, offset: line.text.length}); | |
406 } | |
407 } | |
408 | |
409 // Anchor the chain in the DOM tree. | |
410 this.linkChain(chain); | |
411 return start; | |
412 } | |
413 }; |