Mercurial Hosting > nabble
comparison src/nabble/view/web/util/codemirror/js/editor.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 /* The Editor object manages the content of the editable frame. It | |
2 * catches events, colours nodes, and indents lines. This file also | |
3 * holds some functions for transforming arbitrary DOM structures into | |
4 * plain sequences of <span> and <br> elements | |
5 */ | |
6 | |
7 var internetExplorer = document.selection && window.ActiveXObject && /MSIE/.test(navigator.userAgent); | |
8 var webkit = /AppleWebKit/.test(navigator.userAgent); | |
9 var safari = /Apple Computer, Inc/.test(navigator.vendor); | |
10 var gecko = navigator.userAgent.match(/gecko\/(\d{8})/i); | |
11 if (gecko) gecko = Number(gecko[1]); | |
12 var mac = /Mac/.test(navigator.platform); | |
13 | |
14 // TODO this is related to the backspace-at-end-of-line bug. Remove | |
15 // this if Opera gets their act together, make the version check more | |
16 // broad if they don't. | |
17 var brokenOpera = window.opera && /Version\/10.[56]/.test(navigator.userAgent); | |
18 // TODO remove this once WebKit 533 becomes less common. | |
19 var slowWebkit = /AppleWebKit\/533/.test(navigator.userAgent); | |
20 | |
21 // Make sure a string does not contain two consecutive 'collapseable' | |
22 // whitespace characters. | |
23 function makeWhiteSpace(n) { | |
24 var buffer = [], nb = true; | |
25 for (; n > 0; n--) { | |
26 buffer.push((nb || n == 1) ? nbsp : " "); | |
27 nb ^= true; | |
28 } | |
29 return buffer.join(""); | |
30 } | |
31 | |
32 // Create a set of white-space characters that will not be collapsed | |
33 // by the browser, but will not break text-wrapping either. | |
34 function fixSpaces(string) { | |
35 if (string.charAt(0) == " ") string = nbsp + string.slice(1); | |
36 return string.replace(/\t/g, function() {return makeWhiteSpace(indentUnit);}) | |
37 .replace(/[ \u00a0]{2,}/g, function(s) {return makeWhiteSpace(s.length);}); | |
38 } | |
39 | |
40 function cleanText(text) { | |
41 return text.replace(/\u00a0/g, " ").replace(/\u200b/g, ""); | |
42 } | |
43 | |
44 // Create a SPAN node with the expected properties for document part | |
45 // spans. | |
46 function makePartSpan(value) { | |
47 var text = value; | |
48 if (value.nodeType == 3) text = value.nodeValue; | |
49 else value = document.createTextNode(text); | |
50 | |
51 var span = document.createElement("span"); | |
52 span.isPart = true; | |
53 span.appendChild(value); | |
54 span.currentText = text; | |
55 return span; | |
56 } | |
57 | |
58 function alwaysZero() {return 0;} | |
59 | |
60 // On webkit, when the last BR of the document does not have text | |
61 // behind it, the cursor can not be put on the line after it. This | |
62 // makes pressing enter at the end of the document occasionally do | |
63 // nothing (or at least seem to do nothing). To work around it, this | |
64 // function makes sure the document ends with a span containing a | |
65 // zero-width space character. The traverseDOM iterator filters such | |
66 // character out again, so that the parsers won't see them. This | |
67 // function is called from a few strategic places to make sure the | |
68 // zwsp is restored after the highlighting process eats it. | |
69 var webkitLastLineHack = webkit ? | |
70 function(container) { | |
71 var last = container.lastChild; | |
72 if (!last || !last.hackBR) { | |
73 var br = document.createElement("br"); | |
74 br.hackBR = true; | |
75 container.appendChild(br); | |
76 } | |
77 } : function() {}; | |
78 | |
79 function asEditorLines(string) { | |
80 var tab = makeWhiteSpace(indentUnit); | |
81 return map(string.replace(/\t/g, tab).replace(/\u00a0/g, " ").replace(/\r\n?/g, "\n").split("\n"), fixSpaces); | |
82 } | |
83 | |
84 var Editor = (function(){ | |
85 // The HTML elements whose content should be suffixed by a newline | |
86 // when converting them to flat text. | |
87 var newlineElements = {"P": true, "DIV": true, "LI": true}; | |
88 | |
89 // Helper function for traverseDOM. Flattens an arbitrary DOM node | |
90 // into an array of textnodes and <br> tags. | |
91 function simplifyDOM(root, atEnd) { | |
92 var result = []; | |
93 var leaving = true; | |
94 | |
95 function simplifyNode(node, top) { | |
96 if (node.nodeType == 3) { | |
97 var text = node.nodeValue = fixSpaces(node.nodeValue.replace(/[\r\u200b]/g, "").replace(/\n/g, " ")); | |
98 if (text.length) leaving = false; | |
99 result.push(node); | |
100 } | |
101 else if (isBR(node) && node.childNodes.length == 0) { | |
102 leaving = true; | |
103 result.push(node); | |
104 } | |
105 else { | |
106 for (var n = node.firstChild; n; n = n.nextSibling) simplifyNode(n); | |
107 if (!leaving && newlineElements.hasOwnProperty(node.nodeName.toUpperCase())) { | |
108 leaving = true; | |
109 if (!atEnd || !top) | |
110 result.push(document.createElement("br")); | |
111 } | |
112 } | |
113 } | |
114 | |
115 simplifyNode(root, true); | |
116 return result; | |
117 } | |
118 | |
119 // Creates a MochiKit-style iterator that goes over a series of DOM | |
120 // nodes. The values it yields are strings, the textual content of | |
121 // the nodes. It makes sure that all nodes up to and including the | |
122 // one whose text is being yielded have been 'normalized' to be just | |
123 // <span> and <br> elements. | |
124 function traverseDOM(start){ | |
125 var nodeQueue = []; | |
126 | |
127 // Create a function that can be used to insert nodes after the | |
128 // one given as argument. | |
129 function pointAt(node){ | |
130 var parent = node.parentNode; | |
131 var next = node.nextSibling; | |
132 return function(newnode) { | |
133 parent.insertBefore(newnode, next); | |
134 }; | |
135 } | |
136 var point = null; | |
137 | |
138 // This an Opera-specific hack -- always insert an empty span | |
139 // between two BRs, because Opera's cursor code gets terribly | |
140 // confused when the cursor is between two BRs. | |
141 var afterBR = true; | |
142 | |
143 // Insert a normalized node at the current point. If it is a text | |
144 // node, wrap it in a <span>, and give that span a currentText | |
145 // property -- this is used to cache the nodeValue, because | |
146 // directly accessing nodeValue is horribly slow on some browsers. | |
147 // The dirty property is used by the highlighter to determine | |
148 // which parts of the document have to be re-highlighted. | |
149 function insertPart(part){ | |
150 var text = "\n"; | |
151 if (part.nodeType == 3) { | |
152 select.snapshotChanged(); | |
153 part = makePartSpan(part); | |
154 text = part.currentText; | |
155 afterBR = false; | |
156 } | |
157 else { | |
158 if (afterBR && window.opera) | |
159 point(makePartSpan("")); | |
160 afterBR = true; | |
161 } | |
162 part.dirty = true; | |
163 nodeQueue.push(part); | |
164 point(part); | |
165 return text; | |
166 } | |
167 | |
168 // Extract the text and newlines from a DOM node, insert them into | |
169 // the document, and return the textual content. Used to replace | |
170 // non-normalized nodes. | |
171 function writeNode(node, end) { | |
172 var simplified = simplifyDOM(node, end); | |
173 for (var i = 0; i < simplified.length; i++) | |
174 simplified[i] = insertPart(simplified[i]); | |
175 return simplified.join(""); | |
176 } | |
177 | |
178 // Check whether a node is a normalized <span> element. | |
179 function partNode(node){ | |
180 if (node.isPart && node.childNodes.length == 1 && node.firstChild.nodeType == 3) { | |
181 var text = node.firstChild.nodeValue; | |
182 node.dirty = node.dirty || text != node.currentText; | |
183 node.currentText = text; | |
184 return !/[\n\t\r]/.test(node.currentText); | |
185 } | |
186 return false; | |
187 } | |
188 | |
189 // Advance to next node, return string for current node. | |
190 function next() { | |
191 if (!start) throw StopIteration; | |
192 var node = start; | |
193 start = node.nextSibling; | |
194 | |
195 if (partNode(node)){ | |
196 nodeQueue.push(node); | |
197 afterBR = false; | |
198 return node.currentText; | |
199 } | |
200 else if (isBR(node)) { | |
201 if (afterBR && window.opera) | |
202 node.parentNode.insertBefore(makePartSpan(""), node); | |
203 nodeQueue.push(node); | |
204 afterBR = true; | |
205 return "\n"; | |
206 } | |
207 else { | |
208 var end = !node.nextSibling; | |
209 point = pointAt(node); | |
210 removeElement(node); | |
211 return writeNode(node, end); | |
212 } | |
213 } | |
214 | |
215 // MochiKit iterators are objects with a next function that | |
216 // returns the next value or throws StopIteration when there are | |
217 // no more values. | |
218 return {next: next, nodes: nodeQueue}; | |
219 } | |
220 | |
221 // Determine the text size of a processed node. | |
222 function nodeSize(node) { | |
223 return isBR(node) ? 1 : node.currentText.length; | |
224 } | |
225 | |
226 // Search backwards through the top-level nodes until the next BR or | |
227 // the start of the frame. | |
228 function startOfLine(node) { | |
229 while (node && !isBR(node)) node = node.previousSibling; | |
230 return node; | |
231 } | |
232 function endOfLine(node, container) { | |
233 if (!node) node = container.firstChild; | |
234 else if (isBR(node)) node = node.nextSibling; | |
235 | |
236 while (node && !isBR(node)) node = node.nextSibling; | |
237 return node; | |
238 } | |
239 | |
240 function time() {return new Date().getTime();} | |
241 | |
242 // Client interface for searching the content of the editor. Create | |
243 // these by calling CodeMirror.getSearchCursor. To use, call | |
244 // findNext on the resulting object -- this returns a boolean | |
245 // indicating whether anything was found, and can be called again to | |
246 // skip to the next find. Use the select and replace methods to | |
247 // actually do something with the found locations. | |
248 function SearchCursor(editor, pattern, from, caseFold) { | |
249 this.editor = editor; | |
250 this.history = editor.history; | |
251 this.history.commit(); | |
252 this.valid = !!pattern; | |
253 this.atOccurrence = false; | |
254 if (caseFold == undefined) caseFold = typeof pattern == "string" && pattern == pattern.toLowerCase(); | |
255 | |
256 function getText(node){ | |
257 var line = cleanText(editor.history.textAfter(node)); | |
258 return (caseFold ? line.toLowerCase() : line); | |
259 } | |
260 | |
261 var topPos = {node: null, offset: 0}, self = this; | |
262 if (from && typeof from == "object" && typeof from.character == "number") { | |
263 editor.checkLine(from.line); | |
264 var pos = {node: from.line, offset: from.character}; | |
265 this.pos = {from: pos, to: pos}; | |
266 } | |
267 else if (from) { | |
268 this.pos = {from: select.cursorPos(editor.container, true) || topPos, | |
269 to: select.cursorPos(editor.container, false) || topPos}; | |
270 } | |
271 else { | |
272 this.pos = {from: topPos, to: topPos}; | |
273 } | |
274 | |
275 if (typeof pattern != "string") { // Regexp match | |
276 this.matches = function(reverse, node, offset) { | |
277 if (reverse) { | |
278 var line = getText(node).slice(0, offset), match = line.match(pattern), start = 0; | |
279 while (match) { | |
280 var ind = line.indexOf(match[0]); | |
281 start += ind; | |
282 line = line.slice(ind + 1); | |
283 var newmatch = line.match(pattern); | |
284 if (newmatch) match = newmatch; | |
285 else break; | |
286 } | |
287 } | |
288 else { | |
289 var line = getText(node).slice(offset), match = line.match(pattern), | |
290 start = match && offset + line.indexOf(match[0]); | |
291 } | |
292 if (match) { | |
293 self.currentMatch = match; | |
294 return {from: {node: node, offset: start}, | |
295 to: {node: node, offset: start + match[0].length}}; | |
296 } | |
297 }; | |
298 return; | |
299 } | |
300 | |
301 if (caseFold) pattern = pattern.toLowerCase(); | |
302 // Create a matcher function based on the kind of string we have. | |
303 var target = pattern.split("\n"); | |
304 this.matches = (target.length == 1) ? | |
305 // For one-line strings, searching can be done simply by calling | |
306 // indexOf or lastIndexOf on the current line. | |
307 function(reverse, node, offset) { | |
308 var line = getText(node), len = pattern.length, match; | |
309 if (reverse ? (offset >= len && (match = line.lastIndexOf(pattern, offset - len)) != -1) | |
310 : (match = line.indexOf(pattern, offset)) != -1) | |
311 return {from: {node: node, offset: match}, | |
312 to: {node: node, offset: match + len}}; | |
313 } : | |
314 // Multi-line strings require internal iteration over lines, and | |
315 // some clunky checks to make sure the first match ends at the | |
316 // end of the line and the last match starts at the start. | |
317 function(reverse, node, offset) { | |
318 var idx = (reverse ? target.length - 1 : 0), match = target[idx], line = getText(node); | |
319 var offsetA = (reverse ? line.indexOf(match) + match.length : line.lastIndexOf(match)); | |
320 if (reverse ? offsetA >= offset || offsetA != match.length | |
321 : offsetA <= offset || offsetA != line.length - match.length) | |
322 return; | |
323 | |
324 var pos = node; | |
325 while (true) { | |
326 if (reverse && !pos) return; | |
327 pos = (reverse ? this.history.nodeBefore(pos) : this.history.nodeAfter(pos) ); | |
328 if (!reverse && !pos) return; | |
329 | |
330 line = getText(pos); | |
331 match = target[reverse ? --idx : ++idx]; | |
332 | |
333 if (idx > 0 && idx < target.length - 1) { | |
334 if (line != match) return; | |
335 else continue; | |
336 } | |
337 var offsetB = (reverse ? line.lastIndexOf(match) : line.indexOf(match) + match.length); | |
338 if (reverse ? offsetB != line.length - match.length : offsetB != match.length) | |
339 return; | |
340 return {from: {node: reverse ? pos : node, offset: reverse ? offsetB : offsetA}, | |
341 to: {node: reverse ? node : pos, offset: reverse ? offsetA : offsetB}}; | |
342 } | |
343 }; | |
344 } | |
345 | |
346 SearchCursor.prototype = { | |
347 findNext: function() {return this.find(false);}, | |
348 findPrevious: function() {return this.find(true);}, | |
349 | |
350 find: function(reverse) { | |
351 if (!this.valid) return false; | |
352 | |
353 var self = this, pos = reverse ? this.pos.from : this.pos.to, | |
354 node = pos.node, offset = pos.offset; | |
355 // Reset the cursor if the current line is no longer in the DOM tree. | |
356 if (node && !node.parentNode) { | |
357 node = null; offset = 0; | |
358 } | |
359 function savePosAndFail() { | |
360 var pos = {node: node, offset: offset}; | |
361 self.pos = {from: pos, to: pos}; | |
362 self.atOccurrence = false; | |
363 return false; | |
364 } | |
365 | |
366 while (true) { | |
367 if (this.pos = this.matches(reverse, node, offset)) { | |
368 this.atOccurrence = true; | |
369 return true; | |
370 } | |
371 | |
372 if (reverse) { | |
373 if (!node) return savePosAndFail(); | |
374 node = this.history.nodeBefore(node); | |
375 offset = this.history.textAfter(node).length; | |
376 } | |
377 else { | |
378 var next = this.history.nodeAfter(node); | |
379 if (!next) { | |
380 offset = this.history.textAfter(node).length; | |
381 return savePosAndFail(); | |
382 } | |
383 node = next; | |
384 offset = 0; | |
385 } | |
386 } | |
387 }, | |
388 | |
389 select: function() { | |
390 if (this.atOccurrence) { | |
391 select.setCursorPos(this.editor.container, this.pos.from, this.pos.to); | |
392 select.scrollToCursor(this.editor.container); | |
393 } | |
394 }, | |
395 | |
396 replace: function(string) { | |
397 if (this.atOccurrence) { | |
398 var fragments = this.currentMatch; | |
399 if (fragments) | |
400 string = string.replace(/\\(\d)/, function(m, i){return fragments[i];}); | |
401 var end = this.editor.replaceRange(this.pos.from, this.pos.to, string); | |
402 this.pos.to = end; | |
403 this.atOccurrence = false; | |
404 } | |
405 }, | |
406 | |
407 position: function() { | |
408 if (this.atOccurrence) | |
409 return {line: this.pos.from.node, character: this.pos.from.offset}; | |
410 } | |
411 }; | |
412 | |
413 // The Editor object is the main inside-the-iframe interface. | |
414 function Editor(options) { | |
415 this.options = options; | |
416 window.indentUnit = options.indentUnit; | |
417 var container = this.container = document.body; | |
418 this.history = new UndoHistory(container, options.undoDepth, options.undoDelay, this); | |
419 var self = this; | |
420 | |
421 if (!Editor.Parser) | |
422 throw "No parser loaded."; | |
423 if (options.parserConfig && Editor.Parser.configure) | |
424 Editor.Parser.configure(options.parserConfig); | |
425 | |
426 if (!options.readOnly && !internetExplorer) | |
427 select.setCursorPos(container, {node: null, offset: 0}); | |
428 | |
429 this.dirty = []; | |
430 this.importCode(options.content || ""); | |
431 this.history.onChange = options.onChange; | |
432 | |
433 if (!options.readOnly) { | |
434 if (options.continuousScanning !== false) { | |
435 this.scanner = this.documentScanner(options.passTime); | |
436 this.delayScanning(); | |
437 } | |
438 | |
439 function setEditable() { | |
440 // Use contentEditable instead of designMode on IE, since designMode frames | |
441 // can not run any scripts. It would be nice if we could use contentEditable | |
442 // everywhere, but it is significantly flakier than designMode on every | |
443 // single non-IE browser. | |
444 if (document.body.contentEditable != undefined && internetExplorer) | |
445 document.body.contentEditable = "true"; | |
446 else | |
447 document.designMode = "on"; | |
448 | |
449 // Work around issue where you have to click on the actual | |
450 // body of the document to focus it in IE, making focusing | |
451 // hard when the document is small. | |
452 if (internetExplorer && options.height != "dynamic") | |
453 document.body.style.minHeight = ( | |
454 window.frameElement.clientHeight - 2 * document.body.offsetTop - 5) + "px"; | |
455 | |
456 document.documentElement.style.borderWidth = "0"; | |
457 if (!options.textWrapping) | |
458 container.style.whiteSpace = "nowrap"; | |
459 } | |
460 | |
461 // If setting the frame editable fails, try again when the user | |
462 // focus it (happens when the frame is not visible on | |
463 // initialisation, in Firefox). | |
464 try { | |
465 setEditable(); | |
466 } | |
467 catch(e) { | |
468 var focusEvent = addEventHandler(document, "focus", function() { | |
469 focusEvent(); | |
470 setEditable(); | |
471 }, true); | |
472 } | |
473 | |
474 addEventHandler(document, "keydown", method(this, "keyDown")); | |
475 addEventHandler(document, "keypress", method(this, "keyPress")); | |
476 addEventHandler(document, "keyup", method(this, "keyUp")); | |
477 | |
478 function cursorActivity() {self.cursorActivity(false);} | |
479 addEventHandler(internetExplorer ? document.body : window, "mouseup", cursorActivity); | |
480 addEventHandler(document.body, "cut", cursorActivity); | |
481 | |
482 // workaround for a gecko bug [?] where going forward and then | |
483 // back again breaks designmode (no more cursor) | |
484 if (gecko) | |
485 addEventHandler(window, "pagehide", function(){self.unloaded = true;}); | |
486 | |
487 addEventHandler(document.body, "paste", function(event) { | |
488 cursorActivity(); | |
489 var text = null; | |
490 try { | |
491 var clipboardData = event.clipboardData || window.clipboardData; | |
492 if (clipboardData) text = clipboardData.getData('Text'); | |
493 } | |
494 catch(e) {} | |
495 if (text !== null) { | |
496 event.stop(); | |
497 self.replaceSelection(text); | |
498 select.scrollToCursor(self.container); | |
499 } | |
500 }); | |
501 | |
502 if (this.options.autoMatchParens) | |
503 addEventHandler(document.body, "click", method(this, "scheduleParenHighlight")); | |
504 } | |
505 else if (!options.textWrapping) { | |
506 container.style.whiteSpace = "nowrap"; | |
507 } | |
508 } | |
509 | |
510 function isSafeKey(code) { | |
511 return (code >= 16 && code <= 18) || // shift, control, alt | |
512 (code >= 33 && code <= 40); // arrows, home, end | |
513 } | |
514 | |
515 Editor.prototype = { | |
516 // Import a piece of code into the editor. | |
517 importCode: function(code) { | |
518 var lines = asEditorLines(code), chunk = 1000; | |
519 if (!this.options.incrementalLoading || lines.length < chunk) { | |
520 this.history.push(null, null, lines); | |
521 this.history.reset(); | |
522 } | |
523 else { | |
524 var cur = 0, self = this; | |
525 function addChunk() { | |
526 var chunklines = lines.slice(cur, cur + chunk); | |
527 chunklines.push(""); | |
528 self.history.push(self.history.nodeBefore(null), null, chunklines); | |
529 self.history.reset(); | |
530 cur += chunk; | |
531 if (cur < lines.length) | |
532 parent.setTimeout(addChunk, 1000); | |
533 } | |
534 addChunk(); | |
535 } | |
536 }, | |
537 | |
538 // Extract the code from the editor. | |
539 getCode: function() { | |
540 if (!this.container.firstChild) | |
541 return ""; | |
542 | |
543 var accum = []; | |
544 select.markSelection(); | |
545 forEach(traverseDOM(this.container.firstChild), method(accum, "push")); | |
546 select.selectMarked(); | |
547 // On webkit, don't count last (empty) line if the webkitLastLineHack BR is present | |
548 if (webkit && this.container.lastChild.hackBR) | |
549 accum.pop(); | |
550 webkitLastLineHack(this.container); | |
551 return cleanText(accum.join("")); | |
552 }, | |
553 | |
554 checkLine: function(node) { | |
555 if (node === false || !(node == null || node.parentNode == this.container || node.hackBR)) | |
556 throw parent.CodeMirror.InvalidLineHandle; | |
557 }, | |
558 | |
559 cursorPosition: function(start) { | |
560 if (start == null) start = true; | |
561 var pos = select.cursorPos(this.container, start); | |
562 if (pos) return {line: pos.node, character: pos.offset}; | |
563 else return {line: null, character: 0}; | |
564 }, | |
565 | |
566 firstLine: function() { | |
567 return null; | |
568 }, | |
569 | |
570 lastLine: function() { | |
571 var last = this.container.lastChild; | |
572 if (last) last = startOfLine(last); | |
573 if (last && last.hackBR) last = startOfLine(last.previousSibling); | |
574 return last; | |
575 }, | |
576 | |
577 nextLine: function(line) { | |
578 this.checkLine(line); | |
579 var end = endOfLine(line, this.container); | |
580 if (!end || end.hackBR) return false; | |
581 else return end; | |
582 }, | |
583 | |
584 prevLine: function(line) { | |
585 this.checkLine(line); | |
586 if (line == null) return false; | |
587 return startOfLine(line.previousSibling); | |
588 }, | |
589 | |
590 visibleLineCount: function() { | |
591 var line = this.container.firstChild; | |
592 while (line && isBR(line)) line = line.nextSibling; // BR heights are unreliable | |
593 if (!line) return false; | |
594 var innerHeight = (window.innerHeight | |
595 || document.documentElement.clientHeight | |
596 || document.body.clientHeight); | |
597 return Math.floor(innerHeight / line.offsetHeight); | |
598 }, | |
599 | |
600 selectLines: function(startLine, startOffset, endLine, endOffset) { | |
601 this.checkLine(startLine); | |
602 var start = {node: startLine, offset: startOffset}, end = null; | |
603 if (endOffset !== undefined) { | |
604 this.checkLine(endLine); | |
605 end = {node: endLine, offset: endOffset}; | |
606 } | |
607 select.setCursorPos(this.container, start, end); | |
608 select.scrollToCursor(this.container); | |
609 }, | |
610 | |
611 lineContent: function(line) { | |
612 var accum = []; | |
613 for (line = line ? line.nextSibling : this.container.firstChild; | |
614 line && !isBR(line); line = line.nextSibling) | |
615 accum.push(nodeText(line)); | |
616 return cleanText(accum.join("")); | |
617 }, | |
618 | |
619 setLineContent: function(line, content) { | |
620 this.history.commit(); | |
621 this.replaceRange({node: line, offset: 0}, | |
622 {node: line, offset: this.history.textAfter(line).length}, | |
623 content); | |
624 this.addDirtyNode(line); | |
625 this.scheduleHighlight(); | |
626 }, | |
627 | |
628 removeLine: function(line) { | |
629 var node = line ? line.nextSibling : this.container.firstChild; | |
630 while (node) { | |
631 var next = node.nextSibling; | |
632 removeElement(node); | |
633 if (isBR(node)) break; | |
634 node = next; | |
635 } | |
636 this.addDirtyNode(line); | |
637 this.scheduleHighlight(); | |
638 }, | |
639 | |
640 insertIntoLine: function(line, position, content) { | |
641 var before = null; | |
642 if (position == "end") { | |
643 before = endOfLine(line, this.container); | |
644 } | |
645 else { | |
646 for (var cur = line ? line.nextSibling : this.container.firstChild; cur; cur = cur.nextSibling) { | |
647 if (position == 0) { | |
648 before = cur; | |
649 break; | |
650 } | |
651 var text = nodeText(cur); | |
652 if (text.length > position) { | |
653 before = cur.nextSibling; | |
654 content = text.slice(0, position) + content + text.slice(position); | |
655 removeElement(cur); | |
656 break; | |
657 } | |
658 position -= text.length; | |
659 } | |
660 } | |
661 | |
662 var lines = asEditorLines(content); | |
663 for (var i = 0; i < lines.length; i++) { | |
664 if (i > 0) this.container.insertBefore(document.createElement("BR"), before); | |
665 this.container.insertBefore(makePartSpan(lines[i]), before); | |
666 } | |
667 this.addDirtyNode(line); | |
668 this.scheduleHighlight(); | |
669 }, | |
670 | |
671 // Retrieve the selected text. | |
672 selectedText: function() { | |
673 var h = this.history; | |
674 h.commit(); | |
675 | |
676 var start = select.cursorPos(this.container, true), | |
677 end = select.cursorPos(this.container, false); | |
678 if (!start || !end) return ""; | |
679 | |
680 if (start.node == end.node) | |
681 return h.textAfter(start.node).slice(start.offset, end.offset); | |
682 | |
683 var text = [h.textAfter(start.node).slice(start.offset)]; | |
684 for (var pos = h.nodeAfter(start.node); pos != end.node; pos = h.nodeAfter(pos)) | |
685 text.push(h.textAfter(pos)); | |
686 text.push(h.textAfter(end.node).slice(0, end.offset)); | |
687 return cleanText(text.join("\n")); | |
688 }, | |
689 | |
690 // Replace the selection with another piece of text. | |
691 replaceSelection: function(text) { | |
692 this.history.commit(); | |
693 | |
694 var start = select.cursorPos(this.container, true), | |
695 end = select.cursorPos(this.container, false); | |
696 if (!start || !end) return; | |
697 | |
698 end = this.replaceRange(start, end, text); | |
699 select.setCursorPos(this.container, end); | |
700 webkitLastLineHack(this.container); | |
701 }, | |
702 | |
703 cursorCoords: function(start, internal) { | |
704 var sel = select.cursorPos(this.container, start); | |
705 if (!sel) return null; | |
706 var off = sel.offset, node = sel.node, self = this; | |
707 function measureFromNode(node, xOffset) { | |
708 var y = -(document.body.scrollTop || document.documentElement.scrollTop || 0), | |
709 x = -(document.body.scrollLeft || document.documentElement.scrollLeft || 0) + xOffset; | |
710 forEach([node, internal ? null : window.frameElement], function(n) { | |
711 while (n) {x += n.offsetLeft; y += n.offsetTop;n = n.offsetParent;} | |
712 }); | |
713 return {x: x, y: y, yBot: y + node.offsetHeight}; | |
714 } | |
715 function withTempNode(text, f) { | |
716 var node = document.createElement("SPAN"); | |
717 node.appendChild(document.createTextNode(text)); | |
718 try {return f(node);} | |
719 finally {if (node.parentNode) node.parentNode.removeChild(node);} | |
720 } | |
721 | |
722 while (off) { | |
723 node = node ? node.nextSibling : this.container.firstChild; | |
724 var txt = nodeText(node); | |
725 if (off < txt.length) | |
726 return withTempNode(txt.substr(0, off), function(tmp) { | |
727 tmp.style.position = "absolute"; tmp.style.visibility = "hidden"; | |
728 tmp.className = node.className; | |
729 self.container.appendChild(tmp); | |
730 return measureFromNode(node, tmp.offsetWidth); | |
731 }); | |
732 off -= txt.length; | |
733 } | |
734 if (node && isSpan(node)) | |
735 return measureFromNode(node, node.offsetWidth); | |
736 else if (node && node.nextSibling && isSpan(node.nextSibling)) | |
737 return measureFromNode(node.nextSibling, 0); | |
738 else | |
739 return withTempNode("\u200b", function(tmp) { | |
740 if (node) node.parentNode.insertBefore(tmp, node.nextSibling); | |
741 else self.container.insertBefore(tmp, self.container.firstChild); | |
742 return measureFromNode(tmp, 0); | |
743 }); | |
744 }, | |
745 | |
746 reroutePasteEvent: function() { | |
747 if (this.capturingPaste || window.opera || (gecko && gecko >= 20101026)) return; | |
748 this.capturingPaste = true; | |
749 var te = window.frameElement.CodeMirror.textareaHack; | |
750 var coords = this.cursorCoords(true, true); | |
751 te.style.top = coords.y + "px"; | |
752 if (internetExplorer) { | |
753 var snapshot = select.getBookmark(this.container); | |
754 if (snapshot) this.selectionSnapshot = snapshot; | |
755 } | |
756 parent.focus(); | |
757 te.value = ""; | |
758 te.focus(); | |
759 | |
760 var self = this; | |
761 parent.setTimeout(function() { | |
762 self.capturingPaste = false; | |
763 window.focus(); | |
764 if (self.selectionSnapshot) // IE hack | |
765 window.select.setBookmark(self.container, self.selectionSnapshot); | |
766 var text = te.value; | |
767 if (text) { | |
768 self.replaceSelection(text); | |
769 select.scrollToCursor(self.container); | |
770 } | |
771 }, 10); | |
772 }, | |
773 | |
774 replaceRange: function(from, to, text) { | |
775 var lines = asEditorLines(text); | |
776 lines[0] = this.history.textAfter(from.node).slice(0, from.offset) + lines[0]; | |
777 var lastLine = lines[lines.length - 1]; | |
778 lines[lines.length - 1] = lastLine + this.history.textAfter(to.node).slice(to.offset); | |
779 var end = this.history.nodeAfter(to.node); | |
780 this.history.push(from.node, end, lines); | |
781 return {node: this.history.nodeBefore(end), | |
782 offset: lastLine.length}; | |
783 }, | |
784 | |
785 getSearchCursor: function(string, fromCursor, caseFold) { | |
786 return new SearchCursor(this, string, fromCursor, caseFold); | |
787 }, | |
788 | |
789 // Re-indent the whole buffer | |
790 reindent: function() { | |
791 if (this.container.firstChild) | |
792 this.indentRegion(null, this.container.lastChild); | |
793 }, | |
794 | |
795 reindentSelection: function(direction) { | |
796 if (!select.somethingSelected()) { | |
797 this.indentAtCursor(direction); | |
798 } | |
799 else { | |
800 var start = select.selectionTopNode(this.container, true), | |
801 end = select.selectionTopNode(this.container, false); | |
802 if (start === false || end === false) return; | |
803 this.indentRegion(start, end, direction, true); | |
804 } | |
805 }, | |
806 | |
807 grabKeys: function(eventHandler, filter) { | |
808 this.frozen = eventHandler; | |
809 this.keyFilter = filter; | |
810 }, | |
811 ungrabKeys: function() { | |
812 this.frozen = "leave"; | |
813 }, | |
814 | |
815 setParser: function(name, parserConfig) { | |
816 Editor.Parser = window[name]; | |
817 parserConfig = parserConfig || this.options.parserConfig; | |
818 if (parserConfig && Editor.Parser.configure) | |
819 Editor.Parser.configure(parserConfig); | |
820 | |
821 if (this.container.firstChild) { | |
822 forEach(this.container.childNodes, function(n) { | |
823 if (n.nodeType != 3) n.dirty = true; | |
824 }); | |
825 this.addDirtyNode(this.firstChild); | |
826 this.scheduleHighlight(); | |
827 } | |
828 }, | |
829 | |
830 // Intercept enter and tab, and assign their new functions. | |
831 keyDown: function(event) { | |
832 if (this.frozen == "leave") {this.frozen = null; this.keyFilter = null;} | |
833 if (this.frozen && (!this.keyFilter || this.keyFilter(event.keyCode, event))) { | |
834 event.stop(); | |
835 this.frozen(event); | |
836 return; | |
837 } | |
838 | |
839 var code = event.keyCode; | |
840 // Don't scan when the user is typing. | |
841 this.delayScanning(); | |
842 // Schedule a paren-highlight event, if configured. | |
843 if (this.options.autoMatchParens) | |
844 this.scheduleParenHighlight(); | |
845 | |
846 // The various checks for !altKey are there because AltGr sets both | |
847 // ctrlKey and altKey to true, and should not be recognised as | |
848 // Control. | |
849 if (code == 13) { // enter | |
850 if (event.ctrlKey && !event.altKey) { | |
851 this.reparseBuffer(); | |
852 } | |
853 else { | |
854 select.insertNewlineAtCursor(); | |
855 var mode = this.options.enterMode; | |
856 if (mode != "flat") this.indentAtCursor(mode == "keep" ? "keep" : undefined); | |
857 select.scrollToCursor(this.container); | |
858 } | |
859 event.stop(); | |
860 } | |
861 else if (code == 9 && this.options.tabMode != "default" && !event.ctrlKey) { // tab | |
862 this.handleTab(!event.shiftKey); | |
863 event.stop(); | |
864 } | |
865 else if (code == 32 && event.shiftKey && this.options.tabMode == "default") { // space | |
866 this.handleTab(true); | |
867 event.stop(); | |
868 } | |
869 else if (code == 36 && !event.shiftKey && !event.ctrlKey) { // home | |
870 if (this.home()) event.stop(); | |
871 } | |
872 else if (code == 35 && !event.shiftKey && !event.ctrlKey) { // end | |
873 if (this.end()) event.stop(); | |
874 } | |
875 // Only in Firefox is the default behavior for PgUp/PgDn correct. | |
876 else if (code == 33 && !event.shiftKey && !event.ctrlKey && !gecko) { // PgUp | |
877 if (this.pageUp()) event.stop(); | |
878 } | |
879 else if (code == 34 && !event.shiftKey && !event.ctrlKey && !gecko) { // PgDn | |
880 if (this.pageDown()) event.stop(); | |
881 } | |
882 else if ((code == 219 || code == 221) && event.ctrlKey && !event.altKey) { // [, ] | |
883 this.highlightParens(event.shiftKey, true); | |
884 event.stop(); | |
885 } | |
886 else if (event.metaKey && !event.shiftKey && (code == 37 || code == 39)) { // Meta-left/right | |
887 var cursor = select.selectionTopNode(this.container); | |
888 if (cursor === false || !this.container.firstChild) return; | |
889 | |
890 if (code == 37) select.focusAfterNode(startOfLine(cursor), this.container); | |
891 else { | |
892 var end = endOfLine(cursor, this.container); | |
893 select.focusAfterNode(end ? end.previousSibling : this.container.lastChild, this.container); | |
894 } | |
895 event.stop(); | |
896 } | |
897 else if ((event.ctrlKey || event.metaKey) && !event.altKey) { | |
898 if ((event.shiftKey && code == 90) || code == 89) { // shift-Z, Y | |
899 select.scrollToNode(this.history.redo()); | |
900 event.stop(); | |
901 } | |
902 else if (code == 90 || (safari && code == 8)) { // Z, backspace | |
903 select.scrollToNode(this.history.undo()); | |
904 event.stop(); | |
905 } | |
906 else if (code == 83 && this.options.saveFunction) { // S | |
907 this.options.saveFunction(); | |
908 event.stop(); | |
909 } | |
910 else if (code == 86 && !mac) { // V | |
911 this.reroutePasteEvent(); | |
912 } | |
913 } | |
914 }, | |
915 | |
916 // Check for characters that should re-indent the current line, | |
917 // and prevent Opera from handling enter and tab anyway. | |
918 keyPress: function(event) { | |
919 var electric = this.options.electricChars && Editor.Parser.electricChars, self = this; | |
920 // Hack for Opera, and Firefox on OS X, in which stopping a | |
921 // keydown event does not prevent the associated keypress event | |
922 // from happening, so we have to cancel enter and tab again | |
923 // here. | |
924 if ((this.frozen && (!this.keyFilter || this.keyFilter(event.keyCode || event.code, event))) || | |
925 event.code == 13 || (event.code == 9 && this.options.tabMode != "default") || | |
926 (event.code == 32 && event.shiftKey && this.options.tabMode == "default")) | |
927 event.stop(); | |
928 else if (mac && (event.ctrlKey || event.metaKey) && event.character == "v") { | |
929 this.reroutePasteEvent(); | |
930 } | |
931 else if (electric && electric.indexOf(event.character) != -1) | |
932 parent.setTimeout(function(){self.indentAtCursor(null);}, 0); | |
933 // Work around a bug where pressing backspace at the end of a | |
934 // line, or delete at the start, often causes the cursor to jump | |
935 // to the start of the line in Opera 10.60. | |
936 else if (brokenOpera) { | |
937 if (event.code == 8) { // backspace | |
938 var sel = select.selectionTopNode(this.container), self = this, | |
939 next = sel ? sel.nextSibling : this.container.firstChild; | |
940 if (sel !== false && next && isBR(next)) | |
941 parent.setTimeout(function(){ | |
942 if (select.selectionTopNode(self.container) == next) | |
943 select.focusAfterNode(next.previousSibling, self.container); | |
944 }, 20); | |
945 } | |
946 else if (event.code == 46) { // delete | |
947 var sel = select.selectionTopNode(this.container), self = this; | |
948 if (sel && isBR(sel)) { | |
949 parent.setTimeout(function(){ | |
950 if (select.selectionTopNode(self.container) != sel) | |
951 select.focusAfterNode(sel, self.container); | |
952 }, 20); | |
953 } | |
954 } | |
955 } | |
956 // In 533.* WebKit versions, when the document is big, typing | |
957 // something at the end of a line causes the browser to do some | |
958 // kind of stupid heavy operation, creating delays of several | |
959 // seconds before the typed characters appear. This very crude | |
960 // hack inserts a temporary zero-width space after the cursor to | |
961 // make it not be at the end of the line. | |
962 else if (slowWebkit) { | |
963 var sel = select.selectionTopNode(this.container), | |
964 next = sel ? sel.nextSibling : this.container.firstChild; | |
965 // Doesn't work on empty lines, for some reason those always | |
966 // trigger the delay. | |
967 if (sel && next && isBR(next) && !isBR(sel)) { | |
968 var cheat = document.createTextNode("\u200b"); | |
969 this.container.insertBefore(cheat, next); | |
970 parent.setTimeout(function() { | |
971 if (cheat.nodeValue == "\u200b") removeElement(cheat); | |
972 else cheat.nodeValue = cheat.nodeValue.replace("\u200b", ""); | |
973 }, 20); | |
974 } | |
975 } | |
976 | |
977 // Magic incantation that works abound a webkit bug when you | |
978 // can't type on a blank line following a line that's wider than | |
979 // the window. | |
980 if (webkit && !this.options.textWrapping) | |
981 setTimeout(function () { | |
982 var node = select.selectionTopNode(self.container, true); | |
983 if (node && node.nodeType == 3 && node.previousSibling && isBR(node.previousSibling) | |
984 && node.nextSibling && isBR(node.nextSibling)) | |
985 node.parentNode.replaceChild(document.createElement("BR"), node.previousSibling); | |
986 }, 50); | |
987 }, | |
988 | |
989 // Mark the node at the cursor dirty when a non-safe key is | |
990 // released. | |
991 keyUp: function(event) { | |
992 this.cursorActivity(isSafeKey(event.keyCode)); | |
993 }, | |
994 | |
995 // Indent the line following a given <br>, or null for the first | |
996 // line. If given a <br> element, this must have been highlighted | |
997 // so that it has an indentation method. Returns the whitespace | |
998 // element that has been modified or created (if any). | |
999 indentLineAfter: function(start, direction) { | |
1000 function whiteSpaceAfter(node) { | |
1001 var ws = node ? node.nextSibling : self.container.firstChild; | |
1002 if (!ws || !hasClass(ws, "whitespace")) return null; | |
1003 return ws; | |
1004 } | |
1005 | |
1006 // whiteSpace is the whitespace span at the start of the line, | |
1007 // or null if there is no such node. | |
1008 var self = this, whiteSpace = whiteSpaceAfter(start); | |
1009 var newIndent = 0, curIndent = whiteSpace ? whiteSpace.currentText.length : 0; | |
1010 | |
1011 var firstText = whiteSpace ? whiteSpace.nextSibling : (start ? start.nextSibling : this.container.firstChild); | |
1012 if (direction == "keep") { | |
1013 if (start) { | |
1014 var prevWS = whiteSpaceAfter(startOfLine(start.previousSibling)) | |
1015 if (prevWS) newIndent = prevWS.currentText.length; | |
1016 } | |
1017 } | |
1018 else { | |
1019 // Sometimes the start of the line can influence the correct | |
1020 // indentation, so we retrieve it. | |
1021 var nextChars = (start && firstText && firstText.currentText) ? firstText.currentText : ""; | |
1022 | |
1023 // Ask the lexical context for the correct indentation, and | |
1024 // compute how much this differs from the current indentation. | |
1025 if (direction != null && this.options.tabMode != "indent") | |
1026 newIndent = direction ? curIndent + indentUnit : Math.max(0, curIndent - indentUnit) | |
1027 else if (start) | |
1028 newIndent = start.indentation(nextChars, curIndent, direction, firstText); | |
1029 else if (Editor.Parser.firstIndentation) | |
1030 newIndent = Editor.Parser.firstIndentation(nextChars, curIndent, direction, firstText); | |
1031 } | |
1032 | |
1033 var indentDiff = newIndent - curIndent; | |
1034 | |
1035 // If there is too much, this is just a matter of shrinking a span. | |
1036 if (indentDiff < 0) { | |
1037 if (newIndent == 0) { | |
1038 if (firstText) select.snapshotMove(whiteSpace.firstChild, firstText.firstChild || firstText, 0); | |
1039 removeElement(whiteSpace); | |
1040 whiteSpace = null; | |
1041 } | |
1042 else { | |
1043 select.snapshotMove(whiteSpace.firstChild, whiteSpace.firstChild, indentDiff, true); | |
1044 whiteSpace.currentText = makeWhiteSpace(newIndent); | |
1045 whiteSpace.firstChild.nodeValue = whiteSpace.currentText; | |
1046 } | |
1047 } | |
1048 // Not enough... | |
1049 else if (indentDiff > 0) { | |
1050 // If there is whitespace, we grow it. | |
1051 if (whiteSpace) { | |
1052 whiteSpace.currentText = makeWhiteSpace(newIndent); | |
1053 whiteSpace.firstChild.nodeValue = whiteSpace.currentText; | |
1054 select.snapshotMove(whiteSpace.firstChild, whiteSpace.firstChild, indentDiff, true); | |
1055 } | |
1056 // Otherwise, we have to add a new whitespace node. | |
1057 else { | |
1058 whiteSpace = makePartSpan(makeWhiteSpace(newIndent)); | |
1059 whiteSpace.className = "whitespace"; | |
1060 if (start) insertAfter(whiteSpace, start); | |
1061 else this.container.insertBefore(whiteSpace, this.container.firstChild); | |
1062 select.snapshotMove(firstText && (firstText.firstChild || firstText), | |
1063 whiteSpace.firstChild, newIndent, false, true); | |
1064 } | |
1065 } | |
1066 // Make sure cursor ends up after the whitespace | |
1067 else if (whiteSpace) { | |
1068 select.snapshotMove(whiteSpace.firstChild, whiteSpace.firstChild, newIndent, false); | |
1069 } | |
1070 if (indentDiff != 0) this.addDirtyNode(start); | |
1071 }, | |
1072 | |
1073 // Re-highlight the selected part of the document. | |
1074 highlightAtCursor: function() { | |
1075 var pos = select.selectionTopNode(this.container, true); | |
1076 var to = select.selectionTopNode(this.container, false); | |
1077 if (pos === false || to === false) return false; | |
1078 | |
1079 select.markSelection(); | |
1080 if (this.highlight(pos, endOfLine(to, this.container), true, 20) === false) | |
1081 return false; | |
1082 select.selectMarked(); | |
1083 return true; | |
1084 }, | |
1085 | |
1086 // When tab is pressed with text selected, the whole selection is | |
1087 // re-indented, when nothing is selected, the line with the cursor | |
1088 // is re-indented. | |
1089 handleTab: function(direction) { | |
1090 if (this.options.tabMode == "spaces" && !select.somethingSelected()) | |
1091 select.insertTabAtCursor(); | |
1092 else | |
1093 this.reindentSelection(direction); | |
1094 }, | |
1095 | |
1096 // Custom home behaviour that doesn't land the cursor in front of | |
1097 // leading whitespace unless pressed twice. | |
1098 home: function() { | |
1099 var cur = select.selectionTopNode(this.container, true), start = cur; | |
1100 if (cur === false || !(!cur || cur.isPart || isBR(cur)) || !this.container.firstChild) | |
1101 return false; | |
1102 | |
1103 while (cur && !isBR(cur)) cur = cur.previousSibling; | |
1104 var next = cur ? cur.nextSibling : this.container.firstChild; | |
1105 if (next && next != start && next.isPart && hasClass(next, "whitespace")) | |
1106 select.focusAfterNode(next, this.container); | |
1107 else | |
1108 select.focusAfterNode(cur, this.container); | |
1109 | |
1110 select.scrollToCursor(this.container); | |
1111 return true; | |
1112 }, | |
1113 | |
1114 // Some browsers (Opera) don't manage to handle the end key | |
1115 // properly in the face of vertical scrolling. | |
1116 end: function() { | |
1117 var cur = select.selectionTopNode(this.container, true); | |
1118 if (cur === false) return false; | |
1119 cur = endOfLine(cur, this.container); | |
1120 if (!cur) return false; | |
1121 select.focusAfterNode(cur.previousSibling, this.container); | |
1122 select.scrollToCursor(this.container); | |
1123 return true; | |
1124 }, | |
1125 | |
1126 pageUp: function() { | |
1127 var line = this.cursorPosition().line, scrollAmount = this.visibleLineCount(); | |
1128 if (line === false || scrollAmount === false) return false; | |
1129 // Try to keep one line on the screen. | |
1130 scrollAmount -= 2; | |
1131 for (var i = 0; i < scrollAmount; i++) { | |
1132 line = this.prevLine(line); | |
1133 if (line === false) break; | |
1134 } | |
1135 if (i == 0) return false; // Already at first line | |
1136 select.setCursorPos(this.container, {node: line, offset: 0}); | |
1137 select.scrollToCursor(this.container); | |
1138 return true; | |
1139 }, | |
1140 | |
1141 pageDown: function() { | |
1142 var line = this.cursorPosition().line, scrollAmount = this.visibleLineCount(); | |
1143 if (line === false || scrollAmount === false) return false; | |
1144 // Try to move to the last line of the current page. | |
1145 scrollAmount -= 2; | |
1146 for (var i = 0; i < scrollAmount; i++) { | |
1147 var nextLine = this.nextLine(line); | |
1148 if (nextLine === false) break; | |
1149 line = nextLine; | |
1150 } | |
1151 if (i == 0) return false; // Already at last line | |
1152 select.setCursorPos(this.container, {node: line, offset: 0}); | |
1153 select.scrollToCursor(this.container); | |
1154 return true; | |
1155 }, | |
1156 | |
1157 // Delay (or initiate) the next paren highlight event. | |
1158 scheduleParenHighlight: function() { | |
1159 if (this.parenEvent) parent.clearTimeout(this.parenEvent); | |
1160 var self = this; | |
1161 this.parenEvent = parent.setTimeout(function(){self.highlightParens();}, 300); | |
1162 }, | |
1163 | |
1164 // Take the token before the cursor. If it contains a character in | |
1165 // '()[]{}', search for the matching paren/brace/bracket, and | |
1166 // highlight them in green for a moment, or red if no proper match | |
1167 // was found. | |
1168 highlightParens: function(jump, fromKey) { | |
1169 var self = this, mark = this.options.markParen; | |
1170 if (typeof mark == "string") mark = [mark, mark]; | |
1171 // give the relevant nodes a colour. | |
1172 function highlight(node, ok) { | |
1173 if (!node) return; | |
1174 if (!mark) { | |
1175 node.style.fontWeight = "bold"; | |
1176 node.style.color = ok ? "#8F8" : "#F88"; | |
1177 } | |
1178 else if (mark.call) mark(node, ok); | |
1179 else node.className += " " + mark[ok ? 0 : 1]; | |
1180 } | |
1181 function unhighlight(node) { | |
1182 if (!node) return; | |
1183 if (mark && !mark.call) | |
1184 removeClass(removeClass(node, mark[0]), mark[1]); | |
1185 else if (self.options.unmarkParen) | |
1186 self.options.unmarkParen(node); | |
1187 else { | |
1188 node.style.fontWeight = ""; | |
1189 node.style.color = ""; | |
1190 } | |
1191 } | |
1192 if (!fromKey && self.highlighted) { | |
1193 unhighlight(self.highlighted[0]); | |
1194 unhighlight(self.highlighted[1]); | |
1195 } | |
1196 | |
1197 if (!window || !window.parent || !window.select) return; | |
1198 // Clear the event property. | |
1199 if (this.parenEvent) parent.clearTimeout(this.parenEvent); | |
1200 this.parenEvent = null; | |
1201 | |
1202 // Extract a 'paren' from a piece of text. | |
1203 function paren(node) { | |
1204 if (node.currentText) { | |
1205 var match = node.currentText.match(/^[\s\u00a0]*([\(\)\[\]{}])[\s\u00a0]*$/); | |
1206 return match && match[1]; | |
1207 } | |
1208 } | |
1209 // Determine the direction a paren is facing. | |
1210 function forward(ch) { | |
1211 return /[\(\[\{]/.test(ch); | |
1212 } | |
1213 | |
1214 var ch, cursor = select.selectionTopNode(this.container, true); | |
1215 if (!cursor || !this.highlightAtCursor()) return; | |
1216 cursor = select.selectionTopNode(this.container, true); | |
1217 if (!(cursor && ((ch = paren(cursor)) || (cursor = cursor.nextSibling) && (ch = paren(cursor))))) | |
1218 return; | |
1219 // We only look for tokens with the same className. | |
1220 var className = cursor.className, dir = forward(ch), match = matching[ch]; | |
1221 | |
1222 // Since parts of the document might not have been properly | |
1223 // highlighted, and it is hard to know in advance which part we | |
1224 // have to scan, we just try, and when we find dirty nodes we | |
1225 // abort, parse them, and re-try. | |
1226 function tryFindMatch() { | |
1227 var stack = [], ch, ok = true; | |
1228 for (var runner = cursor; runner; runner = dir ? runner.nextSibling : runner.previousSibling) { | |
1229 if (runner.className == className && isSpan(runner) && (ch = paren(runner))) { | |
1230 if (forward(ch) == dir) | |
1231 stack.push(ch); | |
1232 else if (!stack.length) | |
1233 ok = false; | |
1234 else if (stack.pop() != matching[ch]) | |
1235 ok = false; | |
1236 if (!stack.length) break; | |
1237 } | |
1238 else if (runner.dirty || !isSpan(runner) && !isBR(runner)) { | |
1239 return {node: runner, status: "dirty"}; | |
1240 } | |
1241 } | |
1242 return {node: runner, status: runner && ok}; | |
1243 } | |
1244 | |
1245 while (true) { | |
1246 var found = tryFindMatch(); | |
1247 if (found.status == "dirty") { | |
1248 this.highlight(found.node, endOfLine(found.node)); | |
1249 // Needed because in some corner cases a highlight does not | |
1250 // reach a node. | |
1251 found.node.dirty = false; | |
1252 continue; | |
1253 } | |
1254 else { | |
1255 highlight(cursor, found.status); | |
1256 highlight(found.node, found.status); | |
1257 if (fromKey) | |
1258 parent.setTimeout(function() {unhighlight(cursor); unhighlight(found.node);}, 500); | |
1259 else | |
1260 self.highlighted = [cursor, found.node]; | |
1261 if (jump && found.node) | |
1262 select.focusAfterNode(found.node.previousSibling, this.container); | |
1263 break; | |
1264 } | |
1265 } | |
1266 }, | |
1267 | |
1268 // Adjust the amount of whitespace at the start of the line that | |
1269 // the cursor is on so that it is indented properly. | |
1270 indentAtCursor: function(direction) { | |
1271 if (!this.container.firstChild) return; | |
1272 // The line has to have up-to-date lexical information, so we | |
1273 // highlight it first. | |
1274 if (!this.highlightAtCursor()) return; | |
1275 var cursor = select.selectionTopNode(this.container, false); | |
1276 // If we couldn't determine the place of the cursor, | |
1277 // there's nothing to indent. | |
1278 if (cursor === false) | |
1279 return; | |
1280 select.markSelection(); | |
1281 this.indentLineAfter(startOfLine(cursor), direction); | |
1282 select.selectMarked(); | |
1283 }, | |
1284 | |
1285 // Indent all lines whose start falls inside of the current | |
1286 // selection. | |
1287 indentRegion: function(start, end, direction, selectAfter) { | |
1288 var current = (start = startOfLine(start)), before = start && startOfLine(start.previousSibling); | |
1289 if (!isBR(end)) end = endOfLine(end, this.container); | |
1290 this.addDirtyNode(start); | |
1291 | |
1292 do { | |
1293 var next = endOfLine(current, this.container); | |
1294 if (current) this.highlight(before, next, true); | |
1295 this.indentLineAfter(current, direction); | |
1296 before = current; | |
1297 current = next; | |
1298 } while (current != end); | |
1299 if (selectAfter) | |
1300 select.setCursorPos(this.container, {node: start, offset: 0}, {node: end, offset: 0}); | |
1301 }, | |
1302 | |
1303 // Find the node that the cursor is in, mark it as dirty, and make | |
1304 // sure a highlight pass is scheduled. | |
1305 cursorActivity: function(safe) { | |
1306 // pagehide event hack above | |
1307 if (this.unloaded) { | |
1308 window.document.designMode = "off"; | |
1309 window.document.designMode = "on"; | |
1310 this.unloaded = false; | |
1311 } | |
1312 | |
1313 if (internetExplorer) { | |
1314 this.container.createTextRange().execCommand("unlink"); | |
1315 clearTimeout(this.saveSelectionSnapshot); | |
1316 var self = this; | |
1317 this.saveSelectionSnapshot = setTimeout(function() { | |
1318 var snapshot = select.getBookmark(self.container); | |
1319 if (snapshot) self.selectionSnapshot = snapshot; | |
1320 }, 200); | |
1321 } | |
1322 | |
1323 var activity = this.options.onCursorActivity; | |
1324 if (!safe || activity) { | |
1325 var cursor = select.selectionTopNode(this.container, false); | |
1326 if (cursor === false || !this.container.firstChild) return; | |
1327 cursor = cursor || this.container.firstChild; | |
1328 if (activity) activity(cursor); | |
1329 if (!safe) { | |
1330 this.scheduleHighlight(); | |
1331 this.addDirtyNode(cursor); | |
1332 } | |
1333 } | |
1334 }, | |
1335 | |
1336 reparseBuffer: function() { | |
1337 forEach(this.container.childNodes, function(node) {node.dirty = true;}); | |
1338 if (this.container.firstChild) | |
1339 this.addDirtyNode(this.container.firstChild); | |
1340 }, | |
1341 | |
1342 // Add a node to the set of dirty nodes, if it isn't already in | |
1343 // there. | |
1344 addDirtyNode: function(node) { | |
1345 node = node || this.container.firstChild; | |
1346 if (!node) return; | |
1347 | |
1348 for (var i = 0; i < this.dirty.length; i++) | |
1349 if (this.dirty[i] == node) return; | |
1350 | |
1351 if (node.nodeType != 3) | |
1352 node.dirty = true; | |
1353 this.dirty.push(node); | |
1354 }, | |
1355 | |
1356 allClean: function() { | |
1357 return !this.dirty.length; | |
1358 }, | |
1359 | |
1360 // Cause a highlight pass to happen in options.passDelay | |
1361 // milliseconds. Clear the existing timeout, if one exists. This | |
1362 // way, the passes do not happen while the user is typing, and | |
1363 // should as unobtrusive as possible. | |
1364 scheduleHighlight: function() { | |
1365 // Timeouts are routed through the parent window, because on | |
1366 // some browsers designMode windows do not fire timeouts. | |
1367 var self = this; | |
1368 parent.clearTimeout(this.highlightTimeout); | |
1369 this.highlightTimeout = parent.setTimeout(function(){self.highlightDirty();}, this.options.passDelay); | |
1370 }, | |
1371 | |
1372 // Fetch one dirty node, and remove it from the dirty set. | |
1373 getDirtyNode: function() { | |
1374 while (this.dirty.length > 0) { | |
1375 var found = this.dirty.pop(); | |
1376 // IE8 sometimes throws an unexplainable 'invalid argument' | |
1377 // exception for found.parentNode | |
1378 try { | |
1379 // If the node has been coloured in the meantime, or is no | |
1380 // longer in the document, it should not be returned. | |
1381 while (found && found.parentNode != this.container) | |
1382 found = found.parentNode; | |
1383 if (found && (found.dirty || found.nodeType == 3)) | |
1384 return found; | |
1385 } catch (e) {} | |
1386 } | |
1387 return null; | |
1388 }, | |
1389 | |
1390 // Pick dirty nodes, and highlight them, until options.passTime | |
1391 // milliseconds have gone by. The highlight method will continue | |
1392 // to next lines as long as it finds dirty nodes. It returns | |
1393 // information about the place where it stopped. If there are | |
1394 // dirty nodes left after this function has spent all its lines, | |
1395 // it shedules another highlight to finish the job. | |
1396 highlightDirty: function(force) { | |
1397 // Prevent FF from raising an error when it is firing timeouts | |
1398 // on a page that's no longer loaded. | |
1399 if (!window || !window.parent || !window.select) return false; | |
1400 | |
1401 if (!this.options.readOnly) select.markSelection(); | |
1402 var start, endTime = force ? null : time() + this.options.passTime; | |
1403 while ((time() < endTime || force) && (start = this.getDirtyNode())) { | |
1404 var result = this.highlight(start, endTime); | |
1405 if (result && result.node && result.dirty) | |
1406 this.addDirtyNode(result.node.nextSibling); | |
1407 } | |
1408 if (!this.options.readOnly) select.selectMarked(); | |
1409 if (start) this.scheduleHighlight(); | |
1410 return this.dirty.length == 0; | |
1411 }, | |
1412 | |
1413 // Creates a function that, when called through a timeout, will | |
1414 // continuously re-parse the document. | |
1415 documentScanner: function(passTime) { | |
1416 var self = this, pos = null; | |
1417 return function() { | |
1418 // FF timeout weirdness workaround. | |
1419 if (!window || !window.parent || !window.select) return; | |
1420 // If the current node is no longer in the document... oh | |
1421 // well, we start over. | |
1422 if (pos && pos.parentNode != self.container) | |
1423 pos = null; | |
1424 select.markSelection(); | |
1425 var result = self.highlight(pos, time() + passTime, true); | |
1426 select.selectMarked(); | |
1427 var newPos = result ? (result.node && result.node.nextSibling) : null; | |
1428 pos = (pos == newPos) ? null : newPos; | |
1429 self.delayScanning(); | |
1430 }; | |
1431 }, | |
1432 | |
1433 // Starts the continuous scanning process for this document after | |
1434 // a given interval. | |
1435 delayScanning: function() { | |
1436 if (this.scanner) { | |
1437 parent.clearTimeout(this.documentScan); | |
1438 this.documentScan = parent.setTimeout(this.scanner, this.options.continuousScanning); | |
1439 } | |
1440 }, | |
1441 | |
1442 // The function that does the actual highlighting/colouring (with | |
1443 // help from the parser and the DOM normalizer). Its interface is | |
1444 // rather overcomplicated, because it is used in different | |
1445 // situations: ensuring that a certain line is highlighted, or | |
1446 // highlighting up to X milliseconds starting from a certain | |
1447 // point. The 'from' argument gives the node at which it should | |
1448 // start. If this is null, it will start at the beginning of the | |
1449 // document. When a timestamp is given with the 'target' argument, | |
1450 // it will stop highlighting at that time. If this argument holds | |
1451 // a DOM node, it will highlight until it reaches that node. If at | |
1452 // any time it comes across two 'clean' lines (no dirty nodes), it | |
1453 // will stop, except when 'cleanLines' is true. maxBacktrack is | |
1454 // the maximum number of lines to backtrack to find an existing | |
1455 // parser instance. This is used to give up in situations where a | |
1456 // highlight would take too long and freeze the browser interface. | |
1457 highlight: function(from, target, cleanLines, maxBacktrack){ | |
1458 var container = this.container, self = this, active = this.options.activeTokens; | |
1459 var endTime = (typeof target == "number" ? target : null); | |
1460 | |
1461 if (!container.firstChild) | |
1462 return false; | |
1463 // Backtrack to the first node before from that has a partial | |
1464 // parse stored. | |
1465 while (from && (!from.parserFromHere || from.dirty)) { | |
1466 if (maxBacktrack != null && isBR(from) && (--maxBacktrack) < 0) | |
1467 return false; | |
1468 from = from.previousSibling; | |
1469 } | |
1470 // If we are at the end of the document, do nothing. | |
1471 if (from && !from.nextSibling) | |
1472 return false; | |
1473 | |
1474 // Check whether a part (<span> node) and the corresponding token | |
1475 // match. | |
1476 function correctPart(token, part){ | |
1477 return !part.reduced && part.currentText == token.value && part.className == token.style; | |
1478 } | |
1479 // Shorten the text associated with a part by chopping off | |
1480 // characters from the front. Note that only the currentText | |
1481 // property gets changed. For efficiency reasons, we leave the | |
1482 // nodeValue alone -- we set the reduced flag to indicate that | |
1483 // this part must be replaced. | |
1484 function shortenPart(part, minus){ | |
1485 part.currentText = part.currentText.substring(minus); | |
1486 part.reduced = true; | |
1487 } | |
1488 // Create a part corresponding to a given token. | |
1489 function tokenPart(token){ | |
1490 var part = makePartSpan(token.value); | |
1491 part.className = token.style; | |
1492 return part; | |
1493 } | |
1494 | |
1495 function maybeTouch(node) { | |
1496 if (node) { | |
1497 var old = node.oldNextSibling; | |
1498 if (lineDirty || old === undefined || node.nextSibling != old) | |
1499 self.history.touch(node); | |
1500 node.oldNextSibling = node.nextSibling; | |
1501 } | |
1502 else { | |
1503 var old = self.container.oldFirstChild; | |
1504 if (lineDirty || old === undefined || self.container.firstChild != old) | |
1505 self.history.touch(null); | |
1506 self.container.oldFirstChild = self.container.firstChild; | |
1507 } | |
1508 } | |
1509 | |
1510 // Get the token stream. If from is null, we start with a new | |
1511 // parser from the start of the frame, otherwise a partial parse | |
1512 // is resumed. | |
1513 var traversal = traverseDOM(from ? from.nextSibling : container.firstChild), | |
1514 stream = stringStream(traversal), | |
1515 parsed = from ? from.parserFromHere(stream) : Editor.Parser.make(stream); | |
1516 | |
1517 function surroundedByBRs(node) { | |
1518 return (node.previousSibling == null || isBR(node.previousSibling)) && | |
1519 (node.nextSibling == null || isBR(node.nextSibling)); | |
1520 } | |
1521 | |
1522 // parts is an interface to make it possible to 'delay' fetching | |
1523 // the next DOM node until we are completely done with the one | |
1524 // before it. This is necessary because often the next node is | |
1525 // not yet available when we want to proceed past the current | |
1526 // one. | |
1527 var parts = { | |
1528 current: null, | |
1529 // Fetch current node. | |
1530 get: function(){ | |
1531 if (!this.current) | |
1532 this.current = traversal.nodes.shift(); | |
1533 return this.current; | |
1534 }, | |
1535 // Advance to the next part (do not fetch it yet). | |
1536 next: function(){ | |
1537 this.current = null; | |
1538 }, | |
1539 // Remove the current part from the DOM tree, and move to the | |
1540 // next. | |
1541 remove: function(){ | |
1542 container.removeChild(this.get()); | |
1543 this.current = null; | |
1544 }, | |
1545 // Advance to the next part that is not empty, discarding empty | |
1546 // parts. | |
1547 getNonEmpty: function(){ | |
1548 var part = this.get(); | |
1549 // Allow empty nodes when they are alone on a line, needed | |
1550 // for the FF cursor bug workaround (see select.js, | |
1551 // insertNewlineAtCursor). | |
1552 while (part && isSpan(part) && part.currentText == "") { | |
1553 // Leave empty nodes that are alone on a line alone in | |
1554 // Opera, since that browsers doesn't deal well with | |
1555 // having 2 BRs in a row. | |
1556 if (window.opera && surroundedByBRs(part)) { | |
1557 this.next(); | |
1558 part = this.get(); | |
1559 } | |
1560 else { | |
1561 var old = part; | |
1562 this.remove(); | |
1563 part = this.get(); | |
1564 // Adjust selection information, if any. See select.js for details. | |
1565 select.snapshotMove(old.firstChild, part && (part.firstChild || part), 0); | |
1566 } | |
1567 } | |
1568 | |
1569 return part; | |
1570 } | |
1571 }; | |
1572 | |
1573 var lineDirty = false, prevLineDirty = true, lineNodes = 0; | |
1574 | |
1575 // This forEach loops over the tokens from the parsed stream, and | |
1576 // at the same time uses the parts object to proceed through the | |
1577 // corresponding DOM nodes. | |
1578 forEach(parsed, function(token){ | |
1579 var part = parts.getNonEmpty(); | |
1580 | |
1581 if (token.value == "\n"){ | |
1582 // The idea of the two streams actually staying synchronized | |
1583 // is such a long shot that we explicitly check. | |
1584 if (!isBR(part)) | |
1585 throw "Parser out of sync. Expected BR."; | |
1586 | |
1587 if (part.dirty || !part.indentation) lineDirty = true; | |
1588 maybeTouch(from); | |
1589 from = part; | |
1590 | |
1591 // Every <br> gets a copy of the parser state and a lexical | |
1592 // context assigned to it. The first is used to be able to | |
1593 // later resume parsing from this point, the second is used | |
1594 // for indentation. | |
1595 part.parserFromHere = parsed.copy(); | |
1596 part.indentation = token.indentation || alwaysZero; | |
1597 part.dirty = false; | |
1598 | |
1599 // If the target argument wasn't an integer, go at least | |
1600 // until that node. | |
1601 if (endTime == null && part == target) throw StopIteration; | |
1602 | |
1603 // A clean line with more than one node means we are done. | |
1604 // Throwing a StopIteration is the way to break out of a | |
1605 // MochiKit forEach loop. | |
1606 if ((endTime != null && time() >= endTime) || (!lineDirty && !prevLineDirty && lineNodes > 1 && !cleanLines)) | |
1607 throw StopIteration; | |
1608 prevLineDirty = lineDirty; lineDirty = false; lineNodes = 0; | |
1609 parts.next(); | |
1610 } | |
1611 else { | |
1612 if (!isSpan(part)) | |
1613 throw "Parser out of sync. Expected SPAN."; | |
1614 if (part.dirty) | |
1615 lineDirty = true; | |
1616 lineNodes++; | |
1617 | |
1618 // If the part matches the token, we can leave it alone. | |
1619 if (correctPart(token, part)){ | |
1620 if (active && part.dirty) active(part, token, self); | |
1621 part.dirty = false; | |
1622 parts.next(); | |
1623 } | |
1624 // Otherwise, we have to fix it. | |
1625 else { | |
1626 lineDirty = true; | |
1627 // Insert the correct part. | |
1628 var newPart = tokenPart(token); | |
1629 container.insertBefore(newPart, part); | |
1630 if (active) active(newPart, token, self); | |
1631 var tokensize = token.value.length; | |
1632 var offset = 0; | |
1633 // Eat up parts until the text for this token has been | |
1634 // removed, adjusting the stored selection info (see | |
1635 // select.js) in the process. | |
1636 while (tokensize > 0) { | |
1637 part = parts.get(); | |
1638 var partsize = part.currentText.length; | |
1639 select.snapshotReplaceNode(part.firstChild, newPart.firstChild, tokensize, offset); | |
1640 if (partsize > tokensize){ | |
1641 shortenPart(part, tokensize); | |
1642 tokensize = 0; | |
1643 } | |
1644 else { | |
1645 tokensize -= partsize; | |
1646 offset += partsize; | |
1647 parts.remove(); | |
1648 } | |
1649 } | |
1650 } | |
1651 } | |
1652 }); | |
1653 maybeTouch(from); | |
1654 webkitLastLineHack(this.container); | |
1655 | |
1656 // The function returns some status information that is used by | |
1657 // hightlightDirty to determine whether and where it has to | |
1658 // continue. | |
1659 return {node: parts.getNonEmpty(), | |
1660 dirty: lineDirty}; | |
1661 } | |
1662 }; | |
1663 | |
1664 return Editor; | |
1665 })(); | |
1666 | |
1667 addEventHandler(window, "load", function() { | |
1668 var CodeMirror = window.frameElement.CodeMirror; | |
1669 var e = CodeMirror.editor = new Editor(CodeMirror.options); | |
1670 parent.setTimeout(method(CodeMirror, "init"), 0); | |
1671 }); |