3d7a20538ec6e04244f847aaa9a4d2cbf9ee4fa7
[redakcja.git] / platforma / static / js / lib / codemirror / editor.js
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 // Make sure a string does not contain two consecutive 'collapseable'
8 // whitespace characters.
9 function makeWhiteSpace(n) {
10   var buffer = [], nb = true;
11   for (; n > 0; n--) {
12     buffer.push((nb || n == 1) ? nbsp : " ");
13     nb = !nb;
14   }
15   return buffer.join("");
16 }
17
18 // Create a set of white-space characters that will not be collapsed
19 // by the browser, but will not break text-wrapping either.
20 function fixSpaces(string) {
21   if (string.charAt(0) == " ") string = nbsp + string.slice(1);
22   return string.replace(/\t/g, function(){return makeWhiteSpace(indentUnit);})
23     .replace(/[ \u00a0]{2,}/g, function(s) {return makeWhiteSpace(s.length);});
24 }
25
26 function cleanText(text) {
27   return text.replace(/\u00a0/g, " ").replace(/\u200b/g, "");
28 }
29
30 // Create a SPAN node with the expected properties for document part
31 // spans.
32 function makePartSpan(value, doc) {
33   var text = value;
34   if (value.nodeType == 3) text = value.nodeValue;
35   else value = doc.createTextNode(text);
36
37   var span = doc.createElement("SPAN");
38   span.isPart = true;
39   span.appendChild(value);
40   span.currentText = text;
41   return span;
42 }
43
44 // On webkit, when the last BR of the document does not have text
45 // behind it, the cursor can not be put on the line after it. This
46 // makes pressing enter at the end of the document occasionally do
47 // nothing (or at least seem to do nothing). To work around it, this
48 // function makes sure the document ends with a span containing a
49 // zero-width space character. The traverseDOM iterator filters such
50 // character out again, so that the parsers won't see them. This
51 // function is called from a few strategic places to make sure the
52 // zwsp is restored after the highlighting process eats it.
53 var webkitLastLineHack = webkit ?
54   function(container) {
55     var last = container.lastChild;
56     if (!last || !last.isPart || last.textContent != "\u200b")
57       container.appendChild(makePartSpan("\u200b", container.ownerDocument));
58   } : function() {};
59
60 var Editor = (function(){
61   // The HTML elements whose content should be suffixed by a newline
62   // when converting them to flat text.
63   var newlineElements = {"P": true, "DIV": true, "LI": true};
64
65   function asEditorLines(string) {
66     var tab = makeWhiteSpace(indentUnit);
67     return map(string.replace(/\t/g, tab).replace(/\u00a0/g, " ").replace(/\r\n?/g, "\n").split("\n"), fixSpaces);
68   }
69
70   // Helper function for traverseDOM. Flattens an arbitrary DOM node
71   // into an array of textnodes and <br> tags.
72   function simplifyDOM(root, atEnd) {
73     var doc = root.ownerDocument;
74     var result = [];
75     var leaving = true;
76
77     function simplifyNode(node, top) {
78       if (node.nodeType == 3) {
79         var text = node.nodeValue = fixSpaces(node.nodeValue.replace(/[\r\u200b]/g, "").replace(/\n/g, " "));
80         if (text.length) leaving = false;
81         result.push(node);
82       }
83       else if (isBR(node) && node.childNodes.length == 0) {
84         leaving = true;
85         result.push(node);
86       }
87       else {
88         forEach(node.childNodes, simplifyNode);
89         if (!leaving && newlineElements.hasOwnProperty(node.nodeName.toUpperCase())) {
90           leaving = true;
91           if (!atEnd || !top)
92             result.push(doc.createElement("BR"));
93         }
94       }
95     }
96
97     simplifyNode(root, true);
98     return result;
99   }
100
101   // Creates a MochiKit-style iterator that goes over a series of DOM
102   // nodes. The values it yields are strings, the textual content of
103   // the nodes. It makes sure that all nodes up to and including the
104   // one whose text is being yielded have been 'normalized' to be just
105   // <span> and <br> elements.
106   // See the story.html file for some short remarks about the use of
107   // continuation-passing style in this iterator.
108   function traverseDOM(start){
109     function yield(value, c){cc = c; return value;}
110     function push(fun, arg, c){return function(){return fun(arg, c);};}
111     function stop(){cc = stop; throw StopIteration;};
112     var cc = push(scanNode, start, stop);
113     var owner = start.ownerDocument;
114     var nodeQueue = [];
115
116     // Create a function that can be used to insert nodes after the
117     // one given as argument.
118     function pointAt(node){
119       var parent = node.parentNode;
120       var next = node.nextSibling;
121       return function(newnode) {
122         parent.insertBefore(newnode, next);
123       };
124     }
125     var point = null;
126
127     // Insert a normalized node at the current point. If it is a text
128     // node, wrap it in a <span>, and give that span a currentText
129     // property -- this is used to cache the nodeValue, because
130     // directly accessing nodeValue is horribly slow on some browsers.
131     // The dirty property is used by the highlighter to determine
132     // which parts of the document have to be re-highlighted.
133     function insertPart(part){
134       var text = "\n";
135       if (part.nodeType == 3) {
136         select.snapshotChanged();
137         part = makePartSpan(part, owner);
138         text = part.currentText;
139       }
140       part.dirty = true;
141       nodeQueue.push(part);
142       point(part);
143       return text;
144     }
145
146     // Extract the text and newlines from a DOM node, insert them into
147     // the document, and yield the textual content. Used to replace
148     // non-normalized nodes.
149     function writeNode(node, c, end) {
150       var toYield = [];
151       forEach(simplifyDOM(node, end), function(part) {
152         toYield.push(insertPart(part));
153       });
154       return yield(toYield.join(""), c);
155     }
156
157     // Check whether a node is a normalized <span> element.
158     function partNode(node){
159       if (node.isPart && node.childNodes.length == 1 && node.firstChild.nodeType == 3) {
160         node.currentText = node.firstChild.nodeValue;
161         return !/[\n\t\r]/.test(node.currentText);
162       }
163       return false;
164     }
165
166     // Handle a node. Add its successor to the continuation if there
167     // is one, find out whether the node is normalized. If it is,
168     // yield its content, otherwise, normalize it (writeNode will take
169     // care of yielding).
170     function scanNode(node, c){
171       if (node.nextSibling)
172         c = push(scanNode, node.nextSibling, c);
173
174       if (partNode(node)){
175         nodeQueue.push(node);
176         return yield(node.currentText, c);
177       }
178       else if (isBR(node)) {
179         nodeQueue.push(node);
180         return yield("\n", c);
181       }
182       else {
183         var end = !node.nextSibling;
184         point = pointAt(node);
185         removeElement(node);
186         return writeNode(node, c, end);
187       }
188     }
189
190     // MochiKit iterators are objects with a next function that
191     // returns the next value or throws StopIteration when there are
192     // no more values.
193     return {next: function(){return cc();}, nodes: nodeQueue};
194   }
195
196   // Determine the text size of a processed node.
197   function nodeSize(node) {
198     return isBR(node) ? 1 : node.currentText.length;
199   }
200
201   // Search backwards through the top-level nodes until the next BR or
202   // the start of the frame.
203   function startOfLine(node) {
204     while (node && !isBR(node)) node = node.previousSibling;
205     return node;
206   }
207   function endOfLine(node, container) {
208     if (!node) node = container.firstChild;
209     else if (isBR(node)) node = node.nextSibling;
210
211     while (node && !isBR(node)) node = node.nextSibling;
212     return node;
213   }
214
215   function time() {return new Date().getTime();}
216
217   // Client interface for searching the content of the editor. Create
218   // these by calling CodeMirror.getSearchCursor. To use, call
219   // findNext on the resulting object -- this returns a boolean
220   // indicating whether anything was found, and can be called again to
221   // skip to the next find. Use the select and replace methods to
222   // actually do something with the found locations.
223   function SearchCursor(editor, string, fromCursor) {
224     this.editor = editor;
225     this.history = editor.history;
226     this.history.commit();
227
228     // Are we currently at an occurrence of the search string?
229     this.atOccurrence = false;
230     // The object stores a set of nodes coming after its current
231     // position, so that when the current point is taken out of the
232     // DOM tree, we can still try to continue.
233     this.fallbackSize = 15;
234     var cursor;
235     // Start from the cursor when specified and a cursor can be found.
236     if (fromCursor && (cursor = select.cursorPos(this.editor.container))) {
237       this.line = cursor.node;
238       this.offset = cursor.offset;
239     }
240     else {
241       this.line = null;
242       this.offset = 0;
243     }
244     this.valid = !!string;
245
246     // Create a matcher function based on the kind of string we have.
247     var target = string.split("\n"), self = this;
248     this.matches = (target.length == 1) ?
249       // For one-line strings, searching can be done simply by calling
250       // indexOf on the current line.
251       function() {
252         var match = cleanText(self.history.textAfter(self.line).slice(self.offset)).indexOf(string);
253         if (match > -1)
254           return {from: {node: self.line, offset: self.offset + match},
255                   to: {node: self.line, offset: self.offset + match + string.length}};
256       } :
257       // Multi-line strings require internal iteration over lines, and
258       // some clunky checks to make sure the first match ends at the
259       // end of the line and the last match starts at the start.
260       function() {
261         var firstLine = cleanText(self.history.textAfter(self.line).slice(self.offset));
262         var match = firstLine.lastIndexOf(target[0]);
263         if (match == -1 || match != firstLine.length - target[0].length)
264           return false;
265         var startOffset = self.offset + match;
266
267         var line = self.history.nodeAfter(self.line);
268         for (var i = 1; i < target.length - 1; i++) {
269           if (cleanText(self.history.textAfter(line)) != target[i])
270             return false;
271           line = self.history.nodeAfter(line);
272         }
273
274         if (cleanText(self.history.textAfter(line)).indexOf(target[target.length - 1]) != 0)
275           return false;
276
277         return {from: {node: self.line, offset: startOffset},
278                 to: {node: line, offset: target[target.length - 1].length}};
279       };
280   }
281
282   SearchCursor.prototype = {
283     findNext: function() {
284       if (!this.valid) return false;
285       this.atOccurrence = false;
286       var self = this;
287
288       // Go back to the start of the document if the current line is
289       // no longer in the DOM tree.
290       if (this.line && !this.line.parentNode) {
291         this.line = null;
292         this.offset = 0;
293       }
294
295       // Set the cursor's position one character after the given
296       // position.
297       function saveAfter(pos) {
298         if (self.history.textAfter(pos.node).length > pos.offset) {
299           self.line = pos.node;
300           self.offset = pos.offset + 1;
301         }
302         else {
303           self.line = self.history.nodeAfter(pos.node);
304           self.offset = 0;
305         }
306       }
307
308       while (true) {
309         var match = this.matches();
310         // Found the search string.
311         if (match) {
312           this.atOccurrence = match;
313           saveAfter(match.from);
314           return true;
315         }
316         this.line = this.history.nodeAfter(this.line);
317         this.offset = 0;
318         // End of document.
319         if (!this.line) {
320           this.valid = false;
321           return false;
322         }
323       }
324     },
325
326     select: function() {
327       if (this.atOccurrence) {
328         select.setCursorPos(this.editor.container, this.atOccurrence.from, this.atOccurrence.to);
329         select.scrollToCursor(this.editor.container);
330       }
331     },
332
333     replace: function(string) {
334       if (this.atOccurrence) {
335         var end = this.editor.replaceRange(this.atOccurrence.from, this.atOccurrence.to, string);
336         this.line = end.node;
337         this.offset = end.offset;
338         this.atOccurrence = false;
339       }
340     }
341   };
342
343   // The Editor object is the main inside-the-iframe interface.
344   function Editor(options) {
345     this.options = options;
346     window.indentUnit = options.indentUnit;
347     this.parent = parent;
348     this.doc = document;
349     var container = this.container = this.doc.body;
350     this.win = window;
351     this.history = new History(container, options.undoDepth, options.undoDelay,
352                                this, options.onChange);
353     var self = this;
354
355     if (!Editor.Parser)
356       throw "No parser loaded.";
357     if (options.parserConfig && Editor.Parser.configure)
358       Editor.Parser.configure(options.parserConfig);
359
360     if (!options.readOnly)
361       select.setCursorPos(container, {node: null, offset: 0});
362
363     this.dirty = [];
364     if (options.content)
365       this.importCode(options.content);
366
367     if (!options.readOnly) {
368       if (options.continuousScanning !== false) {
369         this.scanner = this.documentScanner(options.passTime);
370         this.delayScanning();
371       }
372
373       function setEditable() {
374         // In IE, designMode frames can not run any scripts, so we use
375         // contentEditable instead.
376         if (document.body.contentEditable != undefined && internetExplorer)
377           document.body.contentEditable = "true";
378         else
379           document.designMode = "on";
380
381         document.documentElement.style.borderWidth = "0";
382         if (!options.textWrapping)
383           container.style.whiteSpace = "nowrap";
384       }
385
386       // If setting the frame editable fails, try again when the user
387       // focus it (happens when the frame is not visible on
388       // initialisation, in Firefox).
389       try {
390         setEditable();
391       }
392       catch(e) {
393         var focusEvent = addEventHandler(document, "focus", function() {
394           focusEvent();
395           setEditable();
396         }, true);
397       }
398
399       addEventHandler(document, "keydown", method(this, "keyDown"));
400       addEventHandler(document, "keypress", method(this, "keyPress"));
401       addEventHandler(document, "keyup", method(this, "keyUp"));
402
403       function cursorActivity() {self.cursorActivity(false);}
404       addEventHandler(document.body, "mouseup", cursorActivity);
405       addEventHandler(document.body, "cut", cursorActivity);
406
407       addEventHandler(document.body, "paste", function(event) {
408         cursorActivity();
409         var text = null;
410         try {
411           var clipboardData = event.clipboardData || window.clipboardData;
412           if (clipboardData) text = clipboardData.getData('Text');
413         }
414         catch(e) {}
415         if (text !== null) {
416           self.replaceSelection(text);
417           event.stop();
418         }
419       });
420
421       addEventHandler(document.body, "beforepaste", method(this, "reroutePasteEvent"));
422
423       if (this.options.autoMatchParens)
424         addEventHandler(document.body, "click", method(this, "scheduleParenBlink"));
425     }
426     else if (!options.textWrapping) {
427       container.style.whiteSpace = "nowrap";
428     }
429   }
430
431   function isSafeKey(code) {
432     return (code >= 16 && code <= 18) || // shift, control, alt
433            (code >= 33 && code <= 40); // arrows, home, end
434   }
435
436   Editor.prototype = {
437     // Import a piece of code into the editor.
438     importCode: function(code) {
439       this.history.push(null, null, asEditorLines(code));
440       this.history.reset();
441     },
442
443     // Extract the code from the editor.
444     getCode: function() {
445       if (!this.container.firstChild)
446         return "";
447
448       var accum = [];
449       select.markSelection(this.win);
450       forEach(traverseDOM(this.container.firstChild), method(accum, "push"));
451       webkitLastLineHack(this.container);
452       select.selectMarked();
453       return cleanText(accum.join(""));
454     },
455
456     checkLine: function(node) {
457       if (node === false || !(node == null || node.parentNode == this.container))
458         throw parent.CodeMirror.InvalidLineHandle;
459     },
460
461     cursorPosition: function(start) {
462       if (start == null) start = true;
463       var pos = select.cursorPos(this.container, start);
464       if (pos) return {line: pos.node, character: pos.offset};
465       else return {line: null, character: 0};
466     },
467
468     firstLine: function() {
469       return null;
470     },
471
472     lastLine: function() {
473       if (this.container.lastChild) return startOfLine(this.container.lastChild);
474       else return null;
475     },
476
477     nextLine: function(line) {
478       this.checkLine(line);
479       var end = endOfLine(line, this.container);
480       return end || false;
481     },
482
483     prevLine: function(line) {
484       this.checkLine(line);
485       if (line == null) return false;
486       return startOfLine(line.previousSibling);
487     },
488
489     selectLines: function(startLine, startOffset, endLine, endOffset) {
490       this.checkLine(startLine);
491       var start = {node: startLine, offset: startOffset}, end = null;
492       if (endOffset !== undefined) {
493         this.checkLine(endLine);
494         end = {node: endLine, offset: endOffset};
495       }
496       select.setCursorPos(this.container, start, end);
497       select.scrollToCursor(this.container);
498     },
499
500     lineContent: function(line) {
501       this.checkLine(line);
502       var accum = [];
503       for (line = line ? line.nextSibling : this.container.firstChild;
504            line && !isBR(line); line = line.nextSibling)
505         accum.push(nodeText(line));
506       return cleanText(accum.join(""));
507     },
508
509     setLineContent: function(line, content) {
510       this.history.commit();
511       this.replaceRange({node: line, offset: 0},
512                         {node: line, offset: this.history.textAfter(line).length},
513                         content);
514       this.addDirtyNode(line);
515       this.scheduleHighlight();
516     },
517
518     insertIntoLine: function(line, position, content) {
519       var before = null;
520       if (position == "end") {
521         before = endOfLine(line, this.container);
522       }
523       else {
524         for (var cur = line ? line.nextSibling : this.container.firstChild; cur; cur = cur.nextSibling) {
525           if (position == 0) {
526             before = cur;
527             break;
528           }
529           var text = nodeText(cur);
530           if (text.length > position) {
531             before = cur.nextSibling;
532             content = text.slice(0, position) + content + text.slice(position);
533             removeElement(cur);
534             break;
535           }
536           position -= text.length;
537         }
538       }
539
540       var lines = asEditorLines(content), doc = this.container.ownerDocument;
541       for (var i = 0; i < lines.length; i++) {
542         if (i > 0) this.container.insertBefore(doc.createElement("BR"), before);
543         this.container.insertBefore(makePartSpan(lines[i], doc), before);
544       }
545       this.addDirtyNode(line);
546       this.scheduleHighlight();
547     },
548
549     // Retrieve the selected text.
550     selectedText: function() {
551       var h = this.history;
552       h.commit();
553
554       var start = select.cursorPos(this.container, true),
555           end = select.cursorPos(this.container, false);
556       if (!start || !end) return "";
557
558       if (start.node == end.node)
559         return h.textAfter(start.node).slice(start.offset, end.offset);
560
561       var text = [h.textAfter(start.node).slice(start.offset)];
562       for (var pos = h.nodeAfter(start.node); pos != end.node; pos = h.nodeAfter(pos))
563         text.push(h.textAfter(pos));
564       text.push(h.textAfter(end.node).slice(0, end.offset));
565       return cleanText(text.join("\n"));
566     },
567
568     // Replace the selection with another piece of text.
569     replaceSelection: function(text) {
570       this.history.commit();
571
572       var start = select.cursorPos(this.container, true),
573           end = select.cursorPos(this.container, false);
574       if (!start || !end) return;
575
576       end = this.replaceRange(start, end, text);
577       select.setCursorPos(this.container, end);
578       webkitLastLineHack(this.container);
579     },
580
581     reroutePasteEvent: function() {
582       if (this.capturingPaste || window.opera) return;
583       this.capturingPaste = true;
584       var te = parent.document.createElement("TEXTAREA");
585       te.style.position = "absolute";
586       te.style.left = "-500px";
587       te.style.width = "10px";
588       te.style.top = nodeTop(frameElement) + "px";
589       parent.document.body.appendChild(te);
590       parent.focus();
591       te.focus();
592
593       var self = this;
594       this.parent.setTimeout(function() {
595         self.capturingPaste = false;
596         self.win.focus();
597         if (self.selectionSnapshot) // IE hack
598           self.win.select.selectCoords(self.win, self.selectionSnapshot);
599         var text = te.value;
600         if (text) self.replaceSelection(text);
601         removeElement(te);
602       }, 10);
603     },
604
605     replaceRange: function(from, to, text) {
606       var lines = asEditorLines(text);
607       lines[0] = this.history.textAfter(from.node).slice(0, from.offset) + lines[0];
608       var lastLine = lines[lines.length - 1];
609       lines[lines.length - 1] = lastLine + this.history.textAfter(to.node).slice(to.offset);
610       var end = this.history.nodeAfter(to.node);
611       this.history.push(from.node, end, lines);
612       return {node: this.history.nodeBefore(end),
613               offset: lastLine.length};
614     },
615
616     getSearchCursor: function(string, fromCursor) {
617       return new SearchCursor(this, string, fromCursor);
618     },
619
620     // Re-indent the whole buffer
621     reindent: function() {
622       if (this.container.firstChild)
623         this.indentRegion(null, this.container.lastChild);
624     },
625
626     reindentSelection: function(direction) {
627       if (!select.somethingSelected(this.win)) {
628         this.indentAtCursor(direction);
629       }
630       else {
631         var start = select.selectionTopNode(this.container, true),
632             end = select.selectionTopNode(this.container, false);
633         if (start === false || end === false) return;
634         this.indentRegion(start, end, direction);
635       }
636     },
637
638     grabKeys: function(eventHandler, filter) {
639       this.frozen = eventHandler;
640       this.keyFilter = filter;
641     },
642     ungrabKeys: function() {
643       this.frozen = "leave";
644       this.keyFilter = null;
645     },
646
647     setParser: function(name) {
648       Editor.Parser = window[name];
649       if (this.container.firstChild) {
650         forEach(this.container.childNodes, function(n) {
651           if (n.nodeType != 3) n.dirty = true;
652         });
653         this.addDirtyNode(this.firstChild);
654         this.scheduleHighlight();
655       }
656     },
657
658     // Intercept enter and tab, and assign their new functions.
659     keyDown: function(event) {
660       if (this.frozen == "leave") this.frozen = null;
661       if (this.frozen && (!this.keyFilter || this.keyFilter(event))) {
662         event.stop();
663         this.frozen(event);
664         return;
665       }
666
667       var code = event.keyCode;
668       // Don't scan when the user is typing.
669       this.delayScanning();
670       // Schedule a paren-highlight event, if configured.
671       if (this.options.autoMatchParens)
672         this.scheduleParenBlink();
673
674       // The various checks for !altKey are there because AltGr sets both
675       // ctrlKey and altKey to true, and should not be recognised as
676       // Control.
677       if (code == 13) { // enter
678         if (event.ctrlKey && !event.altKey) {
679           this.reparseBuffer();
680         }
681         else {
682           select.insertNewlineAtCursor(this.win);
683           this.indentAtCursor();
684           select.scrollToCursor(this.container);
685         }
686         event.stop();
687       }
688       else if (code == 9 && this.options.tabMode != "default") { // tab
689         this.handleTab(!event.ctrlKey && !event.shiftKey);
690         event.stop();
691       }
692       else if (code == 32 && event.shiftKey && this.options.tabMode == "default") { // space
693         this.handleTab(true);
694         event.stop();
695       }
696       else if (code == 36 && !event.shiftKey && !event.ctrlKey) { // home
697         if (this.home())
698           event.stop();
699       }
700       else if ((code == 219 || code == 221) && event.ctrlKey && !event.altKey) { // [, ]
701         this.blinkParens(event.shiftKey);
702         event.stop();
703       }
704       else if (event.metaKey && !event.shiftKey && (code == 37 || code == 39)) { // Meta-left/right
705         var cursor = select.selectionTopNode(this.container);
706         if (cursor === false || !this.container.firstChild) return;
707
708         if (code == 37) select.focusAfterNode(startOfLine(cursor), this.container);
709         else {
710           var end = endOfLine(cursor, this.container);
711           select.focusAfterNode(end ? end.previousSibling : this.container.lastChild, this.container);
712         }
713         event.stop();
714       }
715       else if ((event.ctrlKey || event.metaKey) && !event.altKey) {
716         if ((event.shiftKey && code == 90) || code == 89) { // shift-Z, Y
717           select.scrollToNode(this.history.redo());
718           event.stop();
719         }
720         else if (code == 90 || (safari && code == 8)) { // Z, backspace
721           select.scrollToNode(this.history.undo());
722           event.stop();
723         }
724         else if (code == 83 && this.options.saveFunction) { // S
725           this.options.saveFunction();
726           event.stop();
727         }
728       }
729     },
730
731     // Check for characters that should re-indent the current line,
732     // and prevent Opera from handling enter and tab anyway.
733     keyPress: function(event) {
734       var electric = Editor.Parser.electricChars, self = this;
735       // Hack for Opera, and Firefox on OS X, in which stopping a
736       // keydown event does not prevent the associated keypress event
737       // from happening, so we have to cancel enter and tab again
738       // here.
739       if ((this.frozen && (!this.keyFilter || this.keyFilter(event.keyCode))) ||
740           event.code == 13 || (event.code == 9 && this.options.tabMode != "default") ||
741           (event.keyCode == 32 && event.shiftKey && this.options.tabMode == "default"))
742         event.stop();
743       else if (electric && electric.indexOf(event.character) != -1)
744         this.parent.setTimeout(function(){self.indentAtCursor(null);}, 0);
745       else if ((event.character == "v" || event.character == "V")
746                && (event.ctrlKey || event.metaKey) && !event.altKey) // ctrl-V
747         this.reroutePasteEvent();
748     },
749
750     // Mark the node at the cursor dirty when a non-safe key is
751     // released.
752     keyUp: function(event) {
753       this.cursorActivity(isSafeKey(event.keyCode));
754     },
755
756     // Indent the line following a given <br>, or null for the first
757     // line. If given a <br> element, this must have been highlighted
758     // so that it has an indentation method. Returns the whitespace
759     // element that has been modified or created (if any).
760     indentLineAfter: function(start, direction) {
761       // whiteSpace is the whitespace span at the start of the line,
762       // or null if there is no such node.
763       var whiteSpace = start ? start.nextSibling : this.container.firstChild;
764       if (whiteSpace && !hasClass(whiteSpace, "whitespace"))
765         whiteSpace = null;
766
767       // Sometimes the start of the line can influence the correct
768       // indentation, so we retrieve it.
769       var firstText = whiteSpace ? whiteSpace.nextSibling : (start ? start.nextSibling : this.container.firstChild);
770       var nextChars = (start && firstText && firstText.currentText) ? firstText.currentText : "";
771
772       // Ask the lexical context for the correct indentation, and
773       // compute how much this differs from the current indentation.
774       var newIndent = 0, curIndent = whiteSpace ? whiteSpace.currentText.length : 0;
775       if (direction != null && this.options.tabMode == "shift")
776         newIndent = direction ? curIndent + indentUnit : Math.max(0, curIndent - indentUnit)
777       else if (start)
778         newIndent = start.indentation(nextChars, curIndent, direction);
779       else if (Editor.Parser.firstIndentation)
780         newIndent = Editor.Parser.firstIndentation(nextChars, curIndent, direction);
781       var indentDiff = newIndent - curIndent;
782
783       // If there is too much, this is just a matter of shrinking a span.
784       if (indentDiff < 0) {
785         if (newIndent == 0) {
786           if (firstText) select.snapshotMove(whiteSpace.firstChild, firstText.firstChild, 0);
787           removeElement(whiteSpace);
788           whiteSpace = null;
789         }
790         else {
791           select.snapshotMove(whiteSpace.firstChild, whiteSpace.firstChild, indentDiff, true);
792           whiteSpace.currentText = makeWhiteSpace(newIndent);
793           whiteSpace.firstChild.nodeValue = whiteSpace.currentText;
794         }
795       }
796       // Not enough...
797       else if (indentDiff > 0) {
798         // If there is whitespace, we grow it.
799         if (whiteSpace) {
800           whiteSpace.currentText = makeWhiteSpace(newIndent);
801           whiteSpace.firstChild.nodeValue = whiteSpace.currentText;
802         }
803         // Otherwise, we have to add a new whitespace node.
804         else {
805           whiteSpace = makePartSpan(makeWhiteSpace(newIndent), this.doc);
806           whiteSpace.className = "whitespace";
807           if (start) insertAfter(whiteSpace, start);
808           else this.container.insertBefore(whiteSpace, this.container.firstChild);
809         }
810         if (firstText) select.snapshotMove(firstText.firstChild, whiteSpace.firstChild, curIndent, false, true);
811       }
812       if (indentDiff != 0) this.addDirtyNode(start);
813       return whiteSpace;
814     },
815
816     // Re-highlight the selected part of the document.
817     highlightAtCursor: function() {
818       var pos = select.selectionTopNode(this.container, true);
819       var to = select.selectionTopNode(this.container, false);
820       if (pos === false || to === false) return;
821
822       select.markSelection(this.win);
823       if (this.highlight(pos, endOfLine(to, this.container), true, 20) === false)
824         return false;
825       select.selectMarked();
826       return true;
827     },
828
829     // When tab is pressed with text selected, the whole selection is
830     // re-indented, when nothing is selected, the line with the cursor
831     // is re-indented.
832     handleTab: function(direction) {
833       if (this.options.tabMode == "spaces")
834         select.insertTabAtCursor(this.win);
835       else
836         this.reindentSelection(direction);
837     },
838
839     home: function() {
840       var cur = select.selectionTopNode(this.container, true), start = cur;
841       if (cur === false || !(!cur || cur.isPart || isBR(cur)) || !this.container.firstChild)
842         return false;
843
844       while (cur && !isBR(cur)) cur = cur.previousSibling;
845       var next = cur ? cur.nextSibling : this.container.firstChild;
846       if (next && next != start && next.isPart && hasClass(next, "whitespace"))
847         select.focusAfterNode(next, this.container);
848       else
849         select.focusAfterNode(cur, this.container);
850
851       select.scrollToCursor(this.container);
852       return true;
853     },
854
855     // Delay (or initiate) the next paren blink event.
856     scheduleParenBlink: function() {
857       if (this.parenEvent) this.parent.clearTimeout(this.parenEvent);
858       var self = this;
859       this.parenEvent = this.parent.setTimeout(function(){self.blinkParens();}, 300);
860     },
861
862     // Take the token before the cursor. If it contains a character in
863     // '()[]{}', search for the matching paren/brace/bracket, and
864     // highlight them in green for a moment, or red if no proper match
865     // was found.
866     blinkParens: function(jump) {
867       if (!window.select) return;
868       // Clear the event property.
869       if (this.parenEvent) this.parent.clearTimeout(this.parenEvent);
870       this.parenEvent = null;
871
872       // Extract a 'paren' from a piece of text.
873       function paren(node) {
874         if (node.currentText) {
875           var match = node.currentText.match(/^[\s\u00a0]*([\(\)\[\]{}])[\s\u00a0]*$/);
876           return match && match[1];
877         }
878       }
879       // Determine the direction a paren is facing.
880       function forward(ch) {
881         return /[\(\[\{]/.test(ch);
882       }
883
884       var ch, self = this, cursor = select.selectionTopNode(this.container, true);
885       if (!cursor || !this.highlightAtCursor()) return;
886       cursor = select.selectionTopNode(this.container, true);
887       if (!(cursor && ((ch = paren(cursor)) || (cursor = cursor.nextSibling) && (ch = paren(cursor)))))
888         return;
889       // We only look for tokens with the same className.
890       var className = cursor.className, dir = forward(ch), match = matching[ch];
891
892       // Since parts of the document might not have been properly
893       // highlighted, and it is hard to know in advance which part we
894       // have to scan, we just try, and when we find dirty nodes we
895       // abort, parse them, and re-try.
896       function tryFindMatch() {
897         var stack = [], ch, ok = true;;
898         for (var runner = cursor; runner; runner = dir ? runner.nextSibling : runner.previousSibling) {
899           if (runner.className == className && isSpan(runner) && (ch = paren(runner))) {
900             if (forward(ch) == dir)
901               stack.push(ch);
902             else if (!stack.length)
903               ok = false;
904             else if (stack.pop() != matching[ch])
905               ok = false;
906             if (!stack.length) break;
907           }
908           else if (runner.dirty || !isSpan(runner) && !isBR(runner)) {
909             return {node: runner, status: "dirty"};
910           }
911         }
912         return {node: runner, status: runner && ok};
913       }
914       // Temporarily give the relevant nodes a colour.
915       function blink(node, ok) {
916         node.style.fontWeight = "bold";
917         node.style.color = ok ? "#8F8" : "#F88";
918         self.parent.setTimeout(function() {node.style.fontWeight = ""; node.style.color = "";}, 500);
919       }
920
921       while (true) {
922         var found = tryFindMatch();
923         if (found.status == "dirty") {
924           this.highlight(found.node, endOfLine(found.node));
925           // Needed because in some corner cases a highlight does not
926           // reach a node.
927           found.node.dirty = false;
928           continue;
929         }
930         else {
931           blink(cursor, found.status);
932           if (found.node) {
933             blink(found.node, found.status);
934             if (jump) select.focusAfterNode(found.node.previousSibling, this.container);
935           }
936           break;
937         }
938       }
939     },
940
941     // Adjust the amount of whitespace at the start of the line that
942     // the cursor is on so that it is indented properly.
943     indentAtCursor: function(direction) {
944       if (!this.container.firstChild) return;
945       // The line has to have up-to-date lexical information, so we
946       // highlight it first.
947       if (!this.highlightAtCursor()) return;
948       var cursor = select.selectionTopNode(this.container, false);
949       // If we couldn't determine the place of the cursor,
950       // there's nothing to indent.
951       if (cursor === false)
952         return;
953       var lineStart = startOfLine(cursor);
954       var whiteSpace = this.indentLineAfter(lineStart, direction);
955       if (cursor == lineStart && whiteSpace)
956           cursor = whiteSpace;
957       // This means the indentation has probably messed up the cursor.
958       if (cursor == whiteSpace)
959         select.focusAfterNode(cursor, this.container);
960     },
961
962     // Indent all lines whose start falls inside of the current
963     // selection.
964     indentRegion: function(start, end, direction) {
965       var current = (start = startOfLine(start)), before = start && startOfLine(start.previousSibling);
966       if (!isBR(end)) end = endOfLine(end, this.container);
967
968       do {
969         var next = endOfLine(current, this.container);
970         if (current) this.highlight(before, next, true);
971         this.indentLineAfter(current, direction);
972         before = current;
973         current = next;
974       } while (current != end);
975       select.setCursorPos(this.container, {node: start, offset: 0}, {node: end, offset: 0});
976     },
977
978     // Find the node that the cursor is in, mark it as dirty, and make
979     // sure a highlight pass is scheduled.
980     cursorActivity: function(safe) {
981       if (internetExplorer) {
982         this.container.createTextRange().execCommand("unlink");
983         this.selectionSnapshot = select.selectionCoords(this.win);
984       }
985
986       var activity = this.options.cursorActivity;
987       if (!safe || activity) {
988         var cursor = select.selectionTopNode(this.container, false);
989         if (cursor === false || !this.container.firstChild) return;
990         cursor = cursor || this.container.firstChild;
991         if (activity) activity(cursor);
992         if (!safe) {
993           this.scheduleHighlight();
994           this.addDirtyNode(cursor);
995         }
996       }
997     },
998
999     reparseBuffer: function() {
1000       forEach(this.container.childNodes, function(node) {node.dirty = true;});
1001       if (this.container.firstChild)
1002         this.addDirtyNode(this.container.firstChild);
1003     },
1004
1005     // Add a node to the set of dirty nodes, if it isn't already in
1006     // there.
1007     addDirtyNode: function(node) {
1008       node = node || this.container.firstChild;
1009       if (!node) return;
1010
1011       for (var i = 0; i < this.dirty.length; i++)
1012         if (this.dirty[i] == node) return;
1013
1014       if (node.nodeType != 3)
1015         node.dirty = true;
1016       this.dirty.push(node);
1017     },
1018
1019     // Cause a highlight pass to happen in options.passDelay
1020     // milliseconds. Clear the existing timeout, if one exists. This
1021     // way, the passes do not happen while the user is typing, and
1022     // should as unobtrusive as possible.
1023     scheduleHighlight: function() {
1024       // Timeouts are routed through the parent window, because on
1025       // some browsers designMode windows do not fire timeouts.
1026       var self = this;
1027       this.parent.clearTimeout(this.highlightTimeout);
1028       this.highlightTimeout = this.parent.setTimeout(function(){self.highlightDirty();}, this.options.passDelay);
1029     },
1030
1031     // Fetch one dirty node, and remove it from the dirty set.
1032     getDirtyNode: function() {
1033       while (this.dirty.length > 0) {
1034         var found = this.dirty.pop();
1035         // IE8 sometimes throws an unexplainable 'invalid argument'
1036         // exception for found.parentNode
1037         try {
1038           // If the node has been coloured in the meantime, or is no
1039           // longer in the document, it should not be returned.
1040           while (found && found.parentNode != this.container)
1041             found = found.parentNode
1042           if (found && (found.dirty || found.nodeType == 3))
1043             return found;
1044         } catch (e) {}
1045       }
1046       return null;
1047     },
1048
1049     // Pick dirty nodes, and highlight them, until options.passTime
1050     // milliseconds have gone by. The highlight method will continue
1051     // to next lines as long as it finds dirty nodes. It returns
1052     // information about the place where it stopped. If there are
1053     // dirty nodes left after this function has spent all its lines,
1054     // it shedules another highlight to finish the job.
1055     highlightDirty: function(force) {
1056       // Prevent FF from raising an error when it is firing timeouts
1057       // on a page that's no longer loaded.
1058       if (!window.select) return;
1059
1060       if (!this.options.readOnly) select.markSelection(this.win);
1061       var start, endTime = force ? null : time() + this.options.passTime;
1062       while ((time() < endTime || force) && (start = this.getDirtyNode())) {
1063         var result = this.highlight(start, endTime);
1064         if (result && result.node && result.dirty)
1065           this.addDirtyNode(result.node);
1066       }
1067       if (!this.options.readOnly) select.selectMarked();
1068       if (start) this.scheduleHighlight();
1069       return this.dirty.length == 0;
1070     },
1071
1072     // Creates a function that, when called through a timeout, will
1073     // continuously re-parse the document.
1074     documentScanner: function(passTime) {
1075       var self = this, pos = null;
1076       return function() {
1077         // FF timeout weirdness workaround.
1078         if (!window.select) return;
1079         // If the current node is no longer in the document... oh
1080         // well, we start over.
1081         if (pos && pos.parentNode != self.container)
1082           pos = null;
1083         select.markSelection(self.win);
1084         var result = self.highlight(pos, time() + passTime, true);
1085         select.selectMarked();
1086         var newPos = result ? (result.node && result.node.nextSibling) : null;
1087         pos = (pos == newPos) ? null : newPos;
1088         self.delayScanning();
1089       };
1090     },
1091
1092     // Starts the continuous scanning process for this document after
1093     // a given interval.
1094     delayScanning: function() {
1095       if (this.scanner) {
1096         this.parent.clearTimeout(this.documentScan);
1097         this.documentScan = this.parent.setTimeout(this.scanner, this.options.continuousScanning);
1098       }
1099     },
1100
1101     // The function that does the actual highlighting/colouring (with
1102     // help from the parser and the DOM normalizer). Its interface is
1103     // rather overcomplicated, because it is used in different
1104     // situations: ensuring that a certain line is highlighted, or
1105     // highlighting up to X milliseconds starting from a certain
1106     // point. The 'from' argument gives the node at which it should
1107     // start. If this is null, it will start at the beginning of the
1108     // document. When a timestamp is given with the 'target' argument,
1109     // it will stop highlighting at that time. If this argument holds
1110     // a DOM node, it will highlight until it reaches that node. If at
1111     // any time it comes across two 'clean' lines (no dirty nodes), it
1112     // will stop, except when 'cleanLines' is true. maxBacktrack is
1113     // the maximum number of lines to backtrack to find an existing
1114     // parser instance. This is used to give up in situations where a
1115     // highlight would take too long and freeze the browser interface.
1116     highlight: function(from, target, cleanLines, maxBacktrack){
1117       var container = this.container, self = this, active = this.options.activeTokens;
1118       var endTime = (typeof target == "number" ? target : null);
1119
1120       if (!container.firstChild)
1121         return;
1122       // Backtrack to the first node before from that has a partial
1123       // parse stored.
1124       while (from && (!from.parserFromHere || from.dirty)) {
1125         if (maxBacktrack != null && isBR(from) && (--maxBacktrack) < 0)
1126           return false;
1127         from = from.previousSibling;
1128       }
1129       // If we are at the end of the document, do nothing.
1130       if (from && !from.nextSibling)
1131         return;
1132
1133       // Check whether a part (<span> node) and the corresponding token
1134       // match.
1135       function correctPart(token, part){
1136         return !part.reduced && part.currentText == token.value && part.className == token.style;
1137       }
1138       // Shorten the text associated with a part by chopping off
1139       // characters from the front. Note that only the currentText
1140       // property gets changed. For efficiency reasons, we leave the
1141       // nodeValue alone -- we set the reduced flag to indicate that
1142       // this part must be replaced.
1143       function shortenPart(part, minus){
1144         part.currentText = part.currentText.substring(minus);
1145         part.reduced = true;
1146       }
1147       // Create a part corresponding to a given token.
1148       function tokenPart(token){
1149         var part = makePartSpan(token.value, self.doc);     
1150         part.className = token.style;
1151         return part;
1152       }
1153
1154       function maybeTouch(node) {
1155         if (node) {
1156           var old = node.oldNextSibling;
1157           if (lineDirty || old === undefined || node.nextSibling != old)
1158             self.history.touch(node);
1159           node.oldNextSibling = node.nextSibling;
1160         }
1161         else {
1162           var old = self.container.oldFirstChild;
1163           if (lineDirty || old === undefined || self.container.firstChild != old)
1164             self.history.touch(null);
1165           self.container.oldFirstChild = self.container.firstChild;
1166         }
1167       }
1168
1169       // Get the token stream. If from is null, we start with a new
1170       // parser from the start of the frame, otherwise a partial parse
1171       // is resumed.
1172       var traversal = traverseDOM(from ? from.nextSibling : container.firstChild),
1173           stream = stringStream(traversal),
1174           parsed = from ? from.parserFromHere(stream) : Editor.Parser.make(stream);
1175
1176       // parts is an interface to make it possible to 'delay' fetching
1177       // the next DOM node until we are completely done with the one
1178       // before it. This is necessary because often the next node is
1179       // not yet available when we want to proceed past the current
1180       // one.
1181       var parts = {
1182         current: null,
1183         // Fetch current node.
1184         get: function(){
1185           if (!this.current)
1186             this.current = traversal.nodes.shift();
1187           return this.current;
1188         },
1189         // Advance to the next part (do not fetch it yet).
1190         next: function(){
1191           this.current = null;
1192         },
1193         // Remove the current part from the DOM tree, and move to the
1194         // next.
1195         remove: function(){
1196           container.removeChild(this.get());
1197           this.current = null;
1198         },
1199         // Advance to the next part that is not empty, discarding empty
1200         // parts.
1201         getNonEmpty: function(){
1202           var part = this.get();
1203           // Allow empty nodes when they are alone on a line, needed
1204           // for the FF cursor bug workaround (see select.js,
1205           // insertNewlineAtCursor).
1206           while (part && isSpan(part) && part.currentText == "") {
1207             var old = part;
1208             this.remove();
1209             part = this.get();
1210             // Adjust selection information, if any. See select.js for details.
1211             select.snapshotMove(old.firstChild, part && (part.firstChild || part), 0);
1212           }
1213           return part;
1214         }
1215       };
1216
1217       var lineDirty = false, prevLineDirty = true, lineNodes = 0;
1218
1219       // This forEach loops over the tokens from the parsed stream, and
1220       // at the same time uses the parts object to proceed through the
1221       // corresponding DOM nodes.
1222       forEach(parsed, function(token){
1223         var part = parts.getNonEmpty();
1224
1225         if (token.value == "\n"){
1226           // The idea of the two streams actually staying synchronized
1227           // is such a long shot that we explicitly check.
1228           if (!isBR(part))
1229             throw "Parser out of sync. Expected BR.";
1230
1231           if (part.dirty || !part.indentation) lineDirty = true;
1232           maybeTouch(from);
1233           from = part;
1234
1235           // Every <br> gets a copy of the parser state and a lexical
1236           // context assigned to it. The first is used to be able to
1237           // later resume parsing from this point, the second is used
1238           // for indentation.
1239           part.parserFromHere = parsed.copy();
1240           part.indentation = token.indentation;
1241           part.dirty = false;
1242
1243           // If the target argument wasn't an integer, go at least
1244           // until that node.
1245           if (endTime == null && part == target) throw StopIteration;
1246
1247           // A clean line with more than one node means we are done.
1248           // Throwing a StopIteration is the way to break out of a
1249           // MochiKit forEach loop.
1250           if ((endTime != null && time() >= endTime) || (!lineDirty && !prevLineDirty && lineNodes > 1 && !cleanLines))
1251             throw StopIteration;
1252           prevLineDirty = lineDirty; lineDirty = false; lineNodes = 0;
1253           parts.next();
1254         }
1255         else {
1256           if (!isSpan(part))
1257             throw "Parser out of sync. Expected SPAN.";
1258           if (part.dirty)
1259             lineDirty = true;
1260           lineNodes++;
1261
1262           // If the part matches the token, we can leave it alone.
1263           if (correctPart(token, part)){
1264             part.dirty = false;
1265             parts.next();
1266           }
1267           // Otherwise, we have to fix it.
1268           else {
1269             lineDirty = true;
1270             // Insert the correct part.
1271             var newPart = tokenPart(token);
1272             container.insertBefore(newPart, part);
1273             if (active) active(newPart, token, self);
1274             var tokensize = token.value.length;
1275             var offset = 0;
1276             // Eat up parts until the text for this token has been
1277             // removed, adjusting the stored selection info (see
1278             // select.js) in the process.
1279             while (tokensize > 0) {
1280               part = parts.get();
1281               var partsize = part.currentText.length;
1282               select.snapshotReplaceNode(part.firstChild, newPart.firstChild, tokensize, offset);
1283               if (partsize > tokensize){
1284                 shortenPart(part, tokensize);
1285                 tokensize = 0;
1286               }
1287               else {
1288                 tokensize -= partsize;
1289                 offset += partsize;
1290                 parts.remove();
1291               }
1292             }
1293           }
1294         }
1295       });
1296       maybeTouch(from);
1297       webkitLastLineHack(this.container);
1298
1299       // The function returns some status information that is used by
1300       // hightlightDirty to determine whether and where it has to
1301       // continue.
1302       return {node: parts.getNonEmpty(),
1303               dirty: lineDirty};
1304     }
1305   };
1306
1307   return Editor;
1308 })();
1309
1310 addEventHandler(window, "load", function() {
1311   var CodeMirror = window.frameElement.CodeMirror;
1312   CodeMirror.editor = new Editor(CodeMirror.options);
1313   this.parent.setTimeout(method(CodeMirror, "init"), 0);
1314 });