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
 
   7 // Make sure a string does not contain two consecutive 'collapseable'
 
   8 // whitespace characters.
 
   9 function makeWhiteSpace(n) {
 
  10   var buffer = [], nb = true;
 
  12     buffer.push((nb || n == 1) ? nbsp : " ");
 
  15   return buffer.join("");
 
  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);});
 
  26 function cleanText(text) {
 
  27   return text.replace(/\u00a0/g, " ").replace(/\u200b/g, "");
 
  30 // Create a SPAN node with the expected properties for document part
 
  32 function makePartSpan(value, doc) {
 
  34   if (value.nodeType == 3) text = value.nodeValue;
 
  35   else value = doc.createTextNode(text);
 
  37   var span = doc.createElement("SPAN");
 
  39   span.appendChild(value);
 
  40   span.currentText = text;
 
  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 ?
 
  55     var last = container.lastChild;
 
  56     if (!last || !last.isPart || last.textContent != "\u200b")
 
  57       container.appendChild(makePartSpan("\u200b", container.ownerDocument));
 
  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};
 
  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);
 
  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;
 
  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;
 
  83       else if (isBR(node) && node.childNodes.length == 0) {
 
  88         forEach(node.childNodes, simplifyNode);
 
  89         if (!leaving && newlineElements.hasOwnProperty(node.nodeName.toUpperCase())) {
 
  92             result.push(doc.createElement("BR"));
 
  97     simplifyNode(root, true);
 
 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;
 
 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);
 
 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){
 
 135       if (part.nodeType == 3) {
 
 136         select.snapshotChanged();
 
 137         part = makePartSpan(part, owner);
 
 138         text = part.currentText;
 
 141       nodeQueue.push(part);
 
 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) {
 
 151       forEach(simplifyDOM(node, end), function(part) {
 
 152         toYield.push(insertPart(part));
 
 154       return yield(toYield.join(""), c);
 
 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);
 
 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);
 
 175         nodeQueue.push(node);
 
 176         return yield(node.currentText, c);
 
 178       else if (isBR(node)) {
 
 179         nodeQueue.push(node);
 
 180         return yield("\n", c);
 
 183         var end = !node.nextSibling;
 
 184         point = pointAt(node);
 
 186         return writeNode(node, c, end);
 
 190     // MochiKit iterators are objects with a next function that
 
 191     // returns the next value or throws StopIteration when there are
 
 193     return {next: function(){return cc();}, nodes: nodeQueue};
 
 196   // Determine the text size of a processed node.
 
 197   function nodeSize(node) {
 
 198     return isBR(node) ? 1 : node.currentText.length;
 
 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;
 
 207   function endOfLine(node, container) {
 
 208     if (!node) node = container.firstChild;
 
 209     else if (isBR(node)) node = node.nextSibling;
 
 211     while (node && !isBR(node)) node = node.nextSibling;
 
 215   function time() {return new Date().getTime();}
 
 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();
 
 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;
 
 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;
 
 244     this.valid = !!string;
 
 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.
 
 252         var match = cleanText(self.history.textAfter(self.line).slice(self.offset)).indexOf(string);
 
 254           return {from: {node: self.line, offset: self.offset + match},
 
 255                   to: {node: self.line, offset: self.offset + match + string.length}};
 
 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.
 
 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)
 
 265         var startOffset = self.offset + match;
 
 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])
 
 271           line = self.history.nodeAfter(line);
 
 274         if (cleanText(self.history.textAfter(line)).indexOf(target[target.length - 1]) != 0)
 
 277         return {from: {node: self.line, offset: startOffset},
 
 278                 to: {node: line, offset: target[target.length - 1].length}};
 
 282   SearchCursor.prototype = {
 
 283     findNext: function() {
 
 284       if (!this.valid) return false;
 
 285       this.atOccurrence = false;
 
 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) {
 
 295       // Set the cursor's position one character after the given
 
 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;
 
 303           self.line = self.history.nodeAfter(pos.node);
 
 309         var match = this.matches();
 
 310         // Found the search string.
 
 312           this.atOccurrence = match;
 
 313           saveAfter(match.from);
 
 316         this.line = this.history.nodeAfter(this.line);
 
 327       if (this.atOccurrence) {
 
 328         select.setCursorPos(this.editor.container, this.atOccurrence.from, this.atOccurrence.to);
 
 329         select.scrollToCursor(this.editor.container);
 
 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;
 
 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;
 
 349     var container = this.container = this.doc.body;
 
 351     this.history = new History(container, options.undoDepth, options.undoDelay,
 
 352                                this, options.onChange);
 
 356       throw "No parser loaded.";
 
 357     if (options.parserConfig && Editor.Parser.configure)
 
 358       Editor.Parser.configure(options.parserConfig);
 
 360     if (!options.readOnly)
 
 361       select.setCursorPos(container, {node: null, offset: 0});
 
 365       this.importCode(options.content);
 
 367     if (!options.readOnly) {
 
 368       if (options.continuousScanning !== false) {
 
 369         this.scanner = this.documentScanner(options.passTime);
 
 370         this.delayScanning();
 
 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";
 
 379           document.designMode = "on";
 
 381         document.documentElement.style.borderWidth = "0";
 
 382         if (!options.textWrapping)
 
 383           container.style.whiteSpace = "nowrap";
 
 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).
 
 393         var focusEvent = addEventHandler(document, "focus", function() {
 
 399       addEventHandler(document, "keydown", method(this, "keyDown"));
 
 400       addEventHandler(document, "keypress", method(this, "keyPress"));
 
 401       addEventHandler(document, "keyup", method(this, "keyUp"));
 
 403       function cursorActivity() {self.cursorActivity(false);}
 
 404       addEventHandler(document.body, "mouseup", cursorActivity);
 
 405       addEventHandler(document.body, "cut", cursorActivity);
 
 407       addEventHandler(document.body, "paste", function(event) {
 
 411           var clipboardData = event.clipboardData || window.clipboardData;
 
 412           if (clipboardData) text = clipboardData.getData('Text');
 
 416           self.replaceSelection(text);
 
 421       addEventHandler(document.body, "beforepaste", method(this, "reroutePasteEvent"));
 
 423       if (this.options.autoMatchParens)
 
 424         addEventHandler(document.body, "click", method(this, "scheduleParenBlink"));
 
 426     else if (!options.textWrapping) {
 
 427       container.style.whiteSpace = "nowrap";
 
 431   function isSafeKey(code) {
 
 432     return (code >= 16 && code <= 18) || // shift, control, alt
 
 433            (code >= 33 && code <= 40); // arrows, home, end
 
 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();
 
 443     // Extract the code from the editor.
 
 444     getCode: function() {
 
 445       if (!this.container.firstChild)
 
 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(""));
 
 456     checkLine: function(node) {
 
 457       if (node === false || !(node == null || node.parentNode == this.container))
 
 458         throw parent.CodeMirror.InvalidLineHandle;
 
 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};
 
 468     firstLine: function() {
 
 472     lastLine: function() {
 
 473       if (this.container.lastChild) return startOfLine(this.container.lastChild);
 
 477     nextLine: function(line) {
 
 478       this.checkLine(line);
 
 479       var end = endOfLine(line, this.container);
 
 483     prevLine: function(line) {
 
 484       this.checkLine(line);
 
 485       if (line == null) return false;
 
 486       return startOfLine(line.previousSibling);
 
 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};
 
 496       select.setCursorPos(this.container, start, end);
 
 497       select.scrollToCursor(this.container);
 
 500     lineContent: function(line) {
 
 501       this.checkLine(line);
 
 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(""));
 
 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},
 
 514       this.addDirtyNode(line);
 
 515       this.scheduleHighlight();
 
 518     insertIntoLine: function(line, position, content) {
 
 520       if (position == "end") {
 
 521         before = endOfLine(line, this.container);
 
 524         for (var cur = line ? line.nextSibling : this.container.firstChild; cur; cur = cur.nextSibling) {
 
 529           var text = nodeText(cur);
 
 530           if (text.length > position) {
 
 531             before = cur.nextSibling;
 
 532             content = text.slice(0, position) + content + text.slice(position);
 
 536           position -= text.length;
 
 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);
 
 545       this.addDirtyNode(line);
 
 546       this.scheduleHighlight();
 
 549     // Retrieve the selected text.
 
 550     selectedText: function() {
 
 551       var h = this.history;
 
 554       var start = select.cursorPos(this.container, true),
 
 555           end = select.cursorPos(this.container, false);
 
 556       if (!start || !end) return "";
 
 558       if (start.node == end.node)
 
 559         return h.textAfter(start.node).slice(start.offset, end.offset);
 
 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"));
 
 568     // Replace the selection with another piece of text.
 
 569     replaceSelection: function(text) {
 
 570       this.history.commit();
 
 572       var start = select.cursorPos(this.container, true),
 
 573           end = select.cursorPos(this.container, false);
 
 574       if (!start || !end) return;
 
 576       end = this.replaceRange(start, end, text);
 
 577       select.setCursorPos(this.container, end);
 
 578       webkitLastLineHack(this.container);
 
 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);
 
 594       this.parent.setTimeout(function() {
 
 595         self.capturingPaste = false;
 
 597         if (self.selectionSnapshot) // IE hack
 
 598           self.win.select.selectCoords(self.win, self.selectionSnapshot);
 
 600         if (text) self.replaceSelection(text);
 
 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};
 
 616     getSearchCursor: function(string, fromCursor) {
 
 617       return new SearchCursor(this, string, fromCursor);
 
 620     // Re-indent the whole buffer
 
 621     reindent: function() {
 
 622       if (this.container.firstChild)
 
 623         this.indentRegion(null, this.container.lastChild);
 
 626     reindentSelection: function(direction) {
 
 627       if (!select.somethingSelected(this.win)) {
 
 628         this.indentAtCursor(direction);
 
 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);
 
 638     grabKeys: function(eventHandler, filter) {
 
 639       this.frozen = eventHandler;
 
 640       this.keyFilter = filter;
 
 642     ungrabKeys: function() {
 
 643       this.frozen = "leave";
 
 644       this.keyFilter = null;
 
 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;
 
 653         this.addDirtyNode(this.firstChild);
 
 654         this.scheduleHighlight();
 
 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.keyCode))) {
 
 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();
 
 674       // The various checks for !altKey are there because AltGr sets both
 
 675       // ctrlKey and altKey to true, and should not be recognised as
 
 677       if (code == 13) { // enter
 
 678         if (event.ctrlKey && !event.altKey) {
 
 679           this.reparseBuffer();
 
 682           select.insertNewlineAtCursor(this.win);
 
 683           this.indentAtCursor();
 
 684           select.scrollToCursor(this.container);
 
 688       else if (code == 9 && this.options.tabMode != "default") { // tab
 
 689         this.handleTab(!event.ctrlKey && !event.shiftKey);
 
 692       else if (code == 32 && event.shiftKey && this.options.tabMode == "default") { // space
 
 693         this.handleTab(true);
 
 696       else if (code == 36 && !event.shiftKey && !event.ctrlKey) { // home
 
 700       else if ((code == 219 || code == 221) && event.ctrlKey && !event.altKey) { // [, ]
 
 701         this.blinkParens(event.shiftKey);
 
 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;
 
 708         if (code == 37) select.focusAfterNode(startOfLine(cursor), this.container);
 
 710           var end = endOfLine(cursor, this.container);
 
 711           select.focusAfterNode(end ? end.previousSibling : this.container.lastChild, this.container);
 
 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());
 
 720         else if (code == 90 || (safari && code == 8)) { // Z, backspace
 
 721           select.scrollToNode(this.history.undo());
 
 724         else if (code == 83 && this.options.saveFunction) { // S
 
 725           this.options.saveFunction();
 
 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
 
 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"))
 
 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();
 
 750     // Mark the node at the cursor dirty when a non-safe key is
 
 752     keyUp: function(event) {
 
 753       this.cursorActivity(isSafeKey(event.keyCode));
 
 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"))
 
 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 : "";
 
 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)
 
 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;
 
 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);
 
 791           select.snapshotMove(whiteSpace.firstChild, whiteSpace.firstChild, indentDiff, true);
 
 792           whiteSpace.currentText = makeWhiteSpace(newIndent);
 
 793           whiteSpace.firstChild.nodeValue = whiteSpace.currentText;
 
 797       else if (indentDiff > 0) {
 
 798         // If there is whitespace, we grow it.
 
 800           whiteSpace.currentText = makeWhiteSpace(newIndent);
 
 801           whiteSpace.firstChild.nodeValue = whiteSpace.currentText;
 
 803         // Otherwise, we have to add a new whitespace node.
 
 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);
 
 810         if (firstText) select.snapshotMove(firstText.firstChild, whiteSpace.firstChild, curIndent, false, true);
 
 812       if (indentDiff != 0) this.addDirtyNode(start);
 
 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;
 
 822       select.markSelection(this.win);
 
 823       if (this.highlight(pos, endOfLine(to, this.container), true, 20) === false)
 
 825       select.selectMarked();
 
 829     // When tab is pressed with text selected, the whole selection is
 
 830     // re-indented, when nothing is selected, the line with the cursor
 
 832     handleTab: function(direction) {
 
 833       if (this.options.tabMode == "spaces")
 
 834         select.insertTabAtCursor(this.win);
 
 836         this.reindentSelection(direction);
 
 840       var cur = select.selectionTopNode(this.container, true), start = cur;
 
 841       if (cur === false || !(!cur || cur.isPart || isBR(cur)) || !this.container.firstChild)
 
 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);
 
 849         select.focusAfterNode(cur, this.container);
 
 851       select.scrollToCursor(this.container);
 
 855     // Delay (or initiate) the next paren blink event.
 
 856     scheduleParenBlink: function() {
 
 857       if (this.parenEvent) this.parent.clearTimeout(this.parenEvent);
 
 859       this.parenEvent = this.parent.setTimeout(function(){self.blinkParens();}, 300);
 
 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
 
 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;
 
 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];
 
 879       // Determine the direction a paren is facing.
 
 880       function forward(ch) {
 
 881         return /[\(\[\{]/.test(ch);
 
 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)))))
 
 889       // We only look for tokens with the same className.
 
 890       var className = cursor.className, dir = forward(ch), match = matching[ch];
 
 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)
 
 902             else if (!stack.length)
 
 904             else if (stack.pop() != matching[ch])
 
 906             if (!stack.length) break;
 
 908           else if (runner.dirty || !isSpan(runner) && !isBR(runner)) {
 
 909             return {node: runner, status: "dirty"};
 
 912         return {node: runner, status: runner && ok};
 
 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);
 
 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
 
 927           found.node.dirty = false;
 
 931           blink(cursor, found.status);
 
 933             blink(found.node, found.status);
 
 934             if (jump) select.focusAfterNode(found.node.previousSibling, this.container);
 
 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)
 
 953       var lineStart = startOfLine(cursor);
 
 954       var whiteSpace = this.indentLineAfter(lineStart, direction);
 
 955       if (cursor == lineStart && whiteSpace)
 
 957       // This means the indentation has probably messed up the cursor.
 
 958       if (cursor == whiteSpace)
 
 959         select.focusAfterNode(cursor, this.container);
 
 962     // Indent all lines whose start falls inside of the current
 
 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);
 
 969         var next = endOfLine(current, this.container);
 
 970         if (current) this.highlight(before, next, true);
 
 971         this.indentLineAfter(current, direction);
 
 974       } while (current != end);
 
 975       select.setCursorPos(this.container, {node: start, offset: 0}, {node: end, offset: 0});
 
 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);
 
 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);
 
 993           this.scheduleHighlight();
 
 994           this.addDirtyNode(cursor);
 
 999     reparseBuffer: function() {
 
1000       forEach(this.container.childNodes, function(node) {node.dirty = true;});
 
1001       if (this.container.firstChild)
 
1002         this.addDirtyNode(this.container.firstChild);
 
1005     // Add a node to the set of dirty nodes, if it isn't already in
 
1007     addDirtyNode: function(node) {
 
1008       node = node || this.container.firstChild;
 
1011       for (var i = 0; i < this.dirty.length; i++)
 
1012         if (this.dirty[i] == node) return;
 
1014       if (node.nodeType != 3)
 
1016       this.dirty.push(node);
 
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.
 
1027       this.parent.clearTimeout(this.highlightTimeout);
 
1028       this.highlightTimeout = this.parent.setTimeout(function(){self.highlightDirty();}, this.options.passDelay);
 
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
 
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))
 
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;
 
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);
 
1067       if (!this.options.readOnly) select.selectMarked();
 
1068       if (start) this.scheduleHighlight();
 
1069       return this.dirty.length == 0;
 
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;
 
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)
 
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();
 
1092     // Starts the continuous scanning process for this document after
 
1093     // a given interval.
 
1094     delayScanning: function() {
 
1096         this.parent.clearTimeout(this.documentScan);
 
1097         this.documentScan = this.parent.setTimeout(this.scanner, this.options.continuousScanning);
 
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);
 
1120       if (!container.firstChild)
 
1122       // Backtrack to the first node before from that has a partial
 
1124       while (from && (!from.parserFromHere || from.dirty)) {
 
1125         if (maxBacktrack != null && isBR(from) && (--maxBacktrack) < 0)
 
1127         from = from.previousSibling;
 
1129       // If we are at the end of the document, do nothing.
 
1130       if (from && !from.nextSibling)
 
1133       // Check whether a part (<span> node) and the corresponding token
 
1135       function correctPart(token, part){
 
1136         return !part.reduced && part.currentText == token.value && part.className == token.style;
 
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;
 
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;
 
1154       function maybeTouch(node) {
 
1156           var old = node.oldNextSibling;
 
1157           if (lineDirty || old === undefined || node.nextSibling != old)
 
1158             self.history.touch(node);
 
1159           node.oldNextSibling = node.nextSibling;
 
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;
 
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
 
1172       var traversal = traverseDOM(from ? from.nextSibling : container.firstChild),
 
1173           stream = stringStream(traversal),
 
1174           parsed = from ? from.parserFromHere(stream) : Editor.Parser.make(stream);
 
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
 
1183         // Fetch current node.
 
1186             this.current = traversal.nodes.shift();
 
1187           return this.current;
 
1189         // Advance to the next part (do not fetch it yet).
 
1191           this.current = null;
 
1193         // Remove the current part from the DOM tree, and move to the
 
1196           container.removeChild(this.get());
 
1197           this.current = null;
 
1199         // Advance to the next part that is not empty, discarding empty
 
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 == "") {
 
1210             // Adjust selection information, if any. See select.js for details.
 
1211             select.snapshotMove(old.firstChild, part && (part.firstChild || part), 0);
 
1217       var lineDirty = false, prevLineDirty = true, lineNodes = 0;
 
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();
 
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.
 
1229             throw "Parser out of sync. Expected BR.";
 
1231           if (part.dirty || !part.indentation) lineDirty = true;
 
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
 
1239           part.parserFromHere = parsed.copy();
 
1240           part.indentation = token.indentation;
 
1243           // If the target argument wasn't an integer, go at least
 
1245           if (endTime == null && part == target) throw StopIteration;
 
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;
 
1257             throw "Parser out of sync. Expected SPAN.";
 
1262           // If the part matches the token, we can leave it alone.
 
1263           if (correctPart(token, part)){
 
1267           // Otherwise, we have to fix it.
 
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;
 
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) {
 
1281               var partsize = part.currentText.length;
 
1282               select.snapshotReplaceNode(part.firstChild, newPart.firstChild, tokensize, offset);
 
1283               if (partsize > tokensize){
 
1284                 shortenPart(part, tokensize);
 
1288                 tokensize -= partsize;
 
1297       webkitLastLineHack(this.container);
 
1299       // The function returns some status information that is used by
 
1300       // hightlightDirty to determine whether and where it has to
 
1302       return {node: parts.getNonEmpty(),
 
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);