Codemirror 0.8.
[redakcja.git] / redakcja / static / js / lib / codemirror / editor.js
index 4eb6d09..07410d2 100644 (file)
@@ -4,13 +4,22 @@
  * plain sequences of <span> and <br> elements
  */
 
+var internetExplorer = document.selection && window.ActiveXObject && /MSIE/.test(navigator.userAgent);
+var webkit = /AppleWebKit/.test(navigator.userAgent);
+var safari = /Apple Computers, Inc/.test(navigator.vendor);
+var gecko = /gecko\/(\d{8})/i.test(navigator.userAgent);
+// TODO this is related to the backspace-at-end-of-line bug. Remove
+// this if Opera gets their act together, make the version check more
+// broad if they don't.
+var brokenOpera = window.opera && /Version\/10.[56]/.test(navigator.userAgent);
+
 // Make sure a string does not contain two consecutive 'collapseable'
 // whitespace characters.
 function makeWhiteSpace(n) {
   var buffer = [], nb = true;
   for (; n > 0; n--) {
     buffer.push((nb || n == 1) ? nbsp : " ");
-    nb = !nb;
+    nb ^= true;
   }
   return buffer.join("");
 }
@@ -19,12 +28,12 @@ function makeWhiteSpace(n) {
 // by the browser, but will not break text-wrapping either.
 function fixSpaces(string) {
   if (string.charAt(0) == " ") string = nbsp + string.slice(1);
-  return string.replace(/\t/g, function(){return makeWhiteSpace(indentUnit);})
+  return string.replace(/\t/g, function() {return makeWhiteSpace(indentUnit);})
     .replace(/[ \u00a0]{2,}/g, function(s) {return makeWhiteSpace(s.length);});
 }
 
 function cleanText(text) {
-  return text.replace(/\u00a0/g, " ").replace(/\u200b/g, "");
+  return text.replace(/\u00a0/g, " ");
 }
 
 // Create a SPAN node with the expected properties for document part
@@ -41,22 +50,6 @@ function makePartSpan(value, doc) {
   return span;
 }
 
-// On webkit, when the last BR of the document does not have text
-// behind it, the cursor can not be put on the line after it. This
-// makes pressing enter at the end of the document occasionally do
-// nothing (or at least seem to do nothing). To work around it, this
-// function makes sure the document ends with a span containing a
-// zero-width space character. The traverseDOM iterator filters such
-// character out again, so that the parsers won't see them. This
-// function is called from a few strategic places to make sure the
-// zwsp is restored after the highlighting process eats it.
-var webkitLastLineHack = webkit ?
-  function(container) {
-    var last = container.lastChild;
-    if (!last || !last.isPart || last.textContent != "\u200b")
-      container.appendChild(makePartSpan("\u200b", container.ownerDocument));
-  } : function() {};
-
 var Editor = (function(){
   // The HTML elements whose content should be suffixed by a newline
   // when converting them to flat text.
@@ -76,17 +69,17 @@ var Editor = (function(){
 
     function simplifyNode(node, top) {
       if (node.nodeType == 3) {
-        var text = node.nodeValue = fixSpaces(node.nodeValue.replace(/[\r\u200b]/g, "").replace(/\n/g, " "));
+        var text = node.nodeValue = fixSpaces(node.nodeValue.replace(/\r/g, "").replace(/\n/g, " "));
         if (text.length) leaving = false;
         result.push(node);
       }
-      else if (node.nodeName == "BR" && node.childNodes.length == 0) {
+      else if (isBR(node) && node.childNodes.length == 0) {
         leaving = true;
         result.push(node);
       }
       else {
-        forEach(node.childNodes, simplifyNode);
-        if (!leaving && newlineElements.hasOwnProperty(node.nodeName)) {
+        for (var n = node.firstChild; n; n = n.nextSibling) simplifyNode(n);
+        if (!leaving && newlineElements.hasOwnProperty(node.nodeName.toUpperCase())) {
           leaving = true;
           if (!atEnd || !top)
             result.push(doc.createElement("BR"));
@@ -103,13 +96,7 @@ var Editor = (function(){
   // the nodes. It makes sure that all nodes up to and including the
   // one whose text is being yielded have been 'normalized' to be just
   // <span> and <br> elements.
-  // See the story.html file for some short remarks about the use of
-  // continuation-passing style in this iterator.
   function traverseDOM(start){
-    function yield(value, c){cc = c; return value;}
-    function push(fun, arg, c){return function(){return fun(arg, c);};}
-    function stop(){cc = stop; throw StopIteration;};
-    var cc = push(scanNode, start, stop);
     var owner = start.ownerDocument;
     var nodeQueue = [];
 
@@ -124,6 +111,11 @@ var Editor = (function(){
     }
     var point = null;
 
+    // This an Opera-specific hack -- always insert an empty span
+    // between two BRs, because Opera's cursor code gets terribly
+    // confused when the cursor is between two BRs.
+    var afterBR = true;
+
     // Insert a normalized node at the current point. If it is a text
     // node, wrap it in a <span>, and give that span a currentText
     // property -- this is used to cache the nodeValue, because
@@ -136,6 +128,12 @@ var Editor = (function(){
         select.snapshotChanged();
         part = makePartSpan(part, owner);
         text = part.currentText;
+        afterBR = false;
+      }
+      else {
+        if (afterBR && window.opera)
+          point(makePartSpan("", owner));
+        afterBR = true;
       }
       part.dirty = true;
       nodeQueue.push(part);
@@ -144,14 +142,13 @@ var Editor = (function(){
     }
 
     // Extract the text and newlines from a DOM node, insert them into
-    // the document, and yield the textual content. Used to replace
+    // the document, and return the textual content. Used to replace
     // non-normalized nodes.
-    function writeNode(node, c, end) {
-      var toYield = [];
-      forEach(simplifyDOM(node, end), function(part) {
-        toYield.push(insertPart(part));
-      });
-      return yield(toYield.join(""), c);
+    function writeNode(node, end) {
+      var simplified = simplifyDOM(node, end);
+      for (var i = 0; i < simplified.length; i++)
+        simplified[i] = insertPart(simplified[i]);
+      return simplified.join("");
     }
 
     // Check whether a node is a normalized <span> element.
@@ -163,55 +160,54 @@ var Editor = (function(){
       return false;
     }
 
-    // Handle a node. Add its successor to the continuation if there
-    // is one, find out whether the node is normalized. If it is,
-    // yield its content, otherwise, normalize it (writeNode will take
-    // care of yielding).
-    function scanNode(node, c){
-      if (node.nextSibling)
-        c = push(scanNode, node.nextSibling, c);
+    // Advance to next node, return string for current node.
+    function next() {
+      if (!start) throw StopIteration;
+      var node = start;
+      start = node.nextSibling;
 
       if (partNode(node)){
         nodeQueue.push(node);
-        return yield(node.currentText, c);
+        afterBR = false;
+        return node.currentText;
       }
-      else if (node.nodeName == "BR") {
+      else if (isBR(node)) {
+        if (afterBR && window.opera)
+          node.parentNode.insertBefore(makePartSpan("", owner), node);
         nodeQueue.push(node);
-        return yield("\n", c);
+        afterBR = true;
+        return "\n";
       }
       else {
         var end = !node.nextSibling;
         point = pointAt(node);
         removeElement(node);
-        return writeNode(node, c, end);
+        return writeNode(node, end);
       }
     }
 
     // MochiKit iterators are objects with a next function that
     // returns the next value or throws StopIteration when there are
     // no more values.
-    return {next: function(){return cc();}, nodes: nodeQueue};
+    return {next: next, nodes: nodeQueue};
   }
 
   // Determine the text size of a processed node.
   function nodeSize(node) {
-    if (node.nodeName == "BR")
-      return 1;
-    else
-      return node.currentText.length;
+    return isBR(node) ? 1 : node.currentText.length;
   }
 
   // Search backwards through the top-level nodes until the next BR or
   // the start of the frame.
   function startOfLine(node) {
-    while (node && node.nodeName != "BR") node = node.previousSibling;
+    while (node && !isBR(node)) node = node.previousSibling;
     return node;
   }
   function endOfLine(node, container) {
     if (!node) node = container.firstChild;
-    else if (node.nodeName == "BR") node = node.nextSibling;
+    else if (isBR(node)) node = node.nextSibling;
 
-    while (node && node.nodeName != "BR") node = node.nextSibling;
+    while (node && !isBR(node)) node = node.nextSibling;
     return node;
   }
 
@@ -223,30 +219,13 @@ var Editor = (function(){
   // indicating whether anything was found, and can be called again to
   // skip to the next find. Use the select and replace methods to
   // actually do something with the found locations.
-  function SearchCursor(editor, string, fromCursor, regexp, case_sensitive) {
-
-    function casedIndexOf(hay, needle, case_sensitive) {
-        if (case_sensitive)
-            return hay.indexOf(needle);
-        else
-            return hay.toLowerCase().indexOf(needle.toLowerCase())
-    }
-
-    function casedLastIndexOf(hay, needle, case_sensitive) {
-        if (case_sensitive)
-            return hay.lastIndexOf(needle);
-        else
-            return hay.toLowerCase().lastIndexOf(needle.toLowerCase());
-    }
-
-    function casedEqual(a, b, case_sensitive) {
-        if (case_sensitive)
-            return a == b;
-        else
-            return a.toLowerCase() == b.toLowerCase();
-    }
-
+  function SearchCursor(editor, string, fromCursor, caseFold) {
     this.editor = editor;
+    if (caseFold == undefined) {
+      caseFold = (string == string.toLowerCase());
+    }
+    this.caseFold = caseFold;
+    if (caseFold) string = string.toLowerCase();
     this.history = editor.history;
     this.history.commit();
 
@@ -274,8 +253,8 @@ var Editor = (function(){
       // For one-line strings, searching can be done simply by calling
       // indexOf on the current line.
       function() {
-        var match = casedIndexOf(cleanText(self.history.textAfter(self.line).slice(self.offset)),
-                string, case_sensitive);
+        var line = cleanText(self.history.textAfter(self.line).slice(self.offset));
+        var match = (self.caseFold ? line.toLowerCase() : line).indexOf(string);
         if (match > -1)
           return {from: {node: self.line, offset: self.offset + match},
                   to: {node: self.line, offset: self.offset + match + string.length}};
@@ -285,19 +264,21 @@ var Editor = (function(){
       // end of the line and the last match starts at the start.
       function() {
         var firstLine = cleanText(self.history.textAfter(self.line).slice(self.offset));
-        var match = casedLastIndexOf(firstLine, target[0], case_sensitive);
+        var match = (self.caseFold ? firstLine.toLowerCase() : firstLine).lastIndexOf(target[0]);
         if (match == -1 || match != firstLine.length - target[0].length)
           return false;
         var startOffset = self.offset + match;
 
         var line = self.history.nodeAfter(self.line);
         for (var i = 1; i < target.length - 1; i++) {
-          if (!casedEqual(cleanText(self.history.textAfter(line)), target[i], case_sensitive))
+          var lineText = cleanText(self.history.textAfter(line));
+          if ((self.caseFold ? lineText.toLowerCase() : lineText) != target[i])
             return false;
           line = self.history.nodeAfter(line);
         }
 
-        if (casedIndexOf(cleanText(self.history.textAfter(line)), target[target.length - 1], case_sensitive) != 0)
+        var lastLine = cleanText(self.history.textAfter(line));
+        if ((self.caseFold ? lastLine.toLowerCase() : lastLine).indexOf(target[target.length - 1]) != 0)
           return false;
 
         return {from: {node: self.line, offset: startOffset},
@@ -374,8 +355,7 @@ var Editor = (function(){
     this.doc = document;
     var container = this.container = this.doc.body;
     this.win = window;
-    this.history = new History(container, options.undoDepth, options.undoDelay,
-                               this, options.onChange);
+    this.history = new UndoHistory(container, options.undoDepth, options.undoDelay, this);
     var self = this;
 
     if (!Editor.Parser)
@@ -387,10 +367,8 @@ var Editor = (function(){
       select.setCursorPos(container, {node: null, offset: 0});
 
     this.dirty = [];
-    if (options.content)
-      this.importCode(options.content);
-    else // FF acts weird when the editable document is completely empty
-      container.appendChild(this.doc.createElement("BR"));
+    this.importCode(options.content || "");
+    this.history.onChange = options.onChange;
 
     if (!options.readOnly) {
       if (options.continuousScanning !== false) {
@@ -399,8 +377,10 @@ var Editor = (function(){
       }
 
       function setEditable() {
-        // In IE, designMode frames can not run any scripts, so we use
-        // contentEditable instead.
+        // Use contentEditable instead of designMode on IE, since designMode frames
+        // can not run any scripts. It would be nice if we could use contentEditable
+        // everywhere, but it is significantly flakier than designMode on every
+        // single non-IE browser.
         if (document.body.contentEditable != undefined && internetExplorer)
           document.body.contentEditable = "true";
         else
@@ -432,6 +412,11 @@ var Editor = (function(){
       addEventHandler(document.body, "mouseup", cursorActivity);
       addEventHandler(document.body, "cut", cursorActivity);
 
+      // workaround for a gecko bug [?] where going forward and then
+      // back again breaks designmode (no more cursor)
+      if (gecko)
+        addEventHandler(this.win, "pagehide", function(){self.unloaded = true;});
+
       addEventHandler(document.body, "paste", function(event) {
         cursorActivity();
         var text = null;
@@ -441,15 +426,14 @@ var Editor = (function(){
         }
         catch(e) {}
         if (text !== null) {
-          self.replaceSelection(text);
           event.stop();
+          self.replaceSelection(text);
+          select.scrollToCursor(self.container);
         }
       });
 
-      addEventHandler(document.body, "beforepaste", method(this, "reroutePasteEvent"));
-
       if (this.options.autoMatchParens)
-        addEventHandler(document.body, "click", method(this, "scheduleParenBlink"));
+        addEventHandler(document.body, "click", method(this, "scheduleParenHighlight"));
     }
     else if (!options.textWrapping) {
       container.style.whiteSpace = "nowrap";
@@ -476,7 +460,6 @@ var Editor = (function(){
       var accum = [];
       select.markSelection(this.win);
       forEach(traverseDOM(this.container.firstChild), method(accum, "push"));
-      webkitLastLineHack(this.container);
       select.selectMarked();
       return cleanText(accum.join(""));
     },
@@ -514,6 +497,16 @@ var Editor = (function(){
       return startOfLine(line.previousSibling);
     },
 
+    visibleLineCount: function() {
+      var line = this.container.firstChild;
+      while (line && isBR(line)) line = line.nextSibling; // BR heights are unreliable
+      if (!line) return false;
+      var innerHeight = (window.innerHeight
+                         || document.documentElement.clientHeight
+                         || document.body.clientHeight);
+      return Math.floor(innerHeight / line.offsetHeight);
+    },
+
     selectLines: function(startLine, startOffset, endLine, endOffset) {
       this.checkLine(startLine);
       var start = {node: startLine, offset: startOffset}, end = null;
@@ -526,10 +519,9 @@ var Editor = (function(){
     },
 
     lineContent: function(line) {
-      this.checkLine(line);
       var accum = [];
       for (line = line ? line.nextSibling : this.container.firstChild;
-           line && line.nodeName != "BR"; line = line.nextSibling)
+           line && !isBR(line); line = line.nextSibling)
         accum.push(nodeText(line));
       return cleanText(accum.join(""));
     },
@@ -543,6 +535,18 @@ var Editor = (function(){
       this.scheduleHighlight();
     },
 
+    removeLine: function(line) {
+      var node = line ? line.nextSibling : this.container.firstChild;
+      while (node) {
+        var next = node.nextSibling;
+        removeElement(node);
+        if (isBR(node)) break;
+        node = next;
+      }
+      this.addDirtyNode(line);
+      this.scheduleHighlight();
+    },
+
     insertIntoLine: function(line, position, content) {
       var before = null;
       if (position == "end") {
@@ -554,7 +558,7 @@ var Editor = (function(){
             before = cur;
             break;
           }
-          var text = (cur.innerText || cur.textContent || cur.nodeValue || "");
+          var text = nodeText(cur);
           if (text.length > position) {
             before = cur.nextSibling;
             content = text.slice(0, position) + content + text.slice(position);
@@ -593,8 +597,7 @@ var Editor = (function(){
       return cleanText(text.join("\n"));
     },
 
-    // Replace the selection with another p
-    // iece of text.
+    // Replace the selection with another piece of text.
     replaceSelection: function(text) {
       this.history.commit();
 
@@ -603,20 +606,58 @@ var Editor = (function(){
       if (!start || !end) return;
 
       end = this.replaceRange(start, end, text);
-      // select.setCursorPos(this.container, end);
-      webkitLastLineHack(this.container);
+      select.setCursorPos(this.container, end);
+    },
+
+    cursorCoords: function(start) {
+      var sel = select.cursorPos(this.container, start);
+      if (!sel) return null;
+      var off = sel.offset, node = sel.node, doc = this.win.document, self = this;
+      function measureFromNode(node, xOffset) {
+        var y = -(self.win.document.body.scrollTop || self.win.document.documentElement.scrollTop || 0),
+            x = -(self.win.document.body.scrollLeft || self.win.document.documentElement.scrollLeft || 0) + xOffset;
+        forEach([node, self.win.frameElement], function(n) {
+          while (n) {x += n.offsetLeft; y += n.offsetTop;n = n.offsetParent;}
+        });
+        return {x: x, y: y, yBot: y + node.offsetHeight};
+      }
+      function withTempNode(text, f) {
+        var node = doc.createElement("SPAN");
+        node.appendChild(doc.createTextNode(text));
+        try {return f(node);}
+        finally {if (node.parentNode) node.parentNode.removeChild(node);}
+      }
+
+      while (off) {
+        node = node ? node.nextSibling : this.container.firstChild;
+        var txt = nodeText(node);
+        if (off < txt.length)
+          return withTempNode(txt.substr(0, off), function(tmp) {
+            tmp.style.position = "absolute"; tmp.style.visibility = "hidden";
+            tmp.className = node.className;
+            self.container.appendChild(tmp);
+            return measureFromNode(node, tmp.offsetWidth);
+          });
+        off -= txt.length;
+      }
+      if (node && isSpan(node))
+        return measureFromNode(node, node.offsetWidth);
+      else if (node && node.nextSibling && isSpan(node.nextSibling))
+        return measureFromNode(node.nextSibling, 0);
+      else
+        return withTempNode("\u200b", function(tmp) {
+          if (node) node.parentNode.insertBefore(tmp, node.nextSibling);
+          else self.container.insertBefore(tmp, self.container.firstChild);
+          return measureFromNode(tmp, 0);
+        });
     },
 
     reroutePasteEvent: function() {
       if (this.capturingPaste || window.opera) return;
       this.capturingPaste = true;
-      var te = parent.document.createElement("TEXTAREA");
-      te.style.position = "absolute";
-      te.style.left = "-500px";
-      te.style.width = "10px";
-      te.style.top = nodeTop(frameElement) + "px";
-      parent.document.body.appendChild(te);
+      var te = window.frameElement.CodeMirror.textareaHack;
       parent.focus();
+      te.value = "";
       te.focus();
 
       var self = this;
@@ -624,10 +665,12 @@ var Editor = (function(){
         self.capturingPaste = false;
         self.win.focus();
         if (self.selectionSnapshot) // IE hack
-          self.win.select.selectCoords(self.win, self.selectionSnapshot);
+          self.win.select.setBookmark(self.container, self.selectionSnapshot);
         var text = te.value;
-        if (text) self.replaceSelection(text);
-        removeElement(te);
+        if (text) {
+          self.replaceSelection(text);
+          select.scrollToCursor(self.container);
+        }
       }, 10);
     },
 
@@ -642,8 +685,8 @@ var Editor = (function(){
               offset: lastLine.length};
     },
 
-    getSearchCursor: function(string, fromCursor, regexp, case_sensitive) {
-      return new SearchCursor(this, string, fromCursor, regexp, case_sensitive);
+    getSearchCursor: function(string, fromCursor, caseFold) {
+      return new SearchCursor(this, string, fromCursor, caseFold);
     },
 
     // Re-indent the whole buffer
@@ -670,11 +713,14 @@ var Editor = (function(){
     },
     ungrabKeys: function() {
       this.frozen = "leave";
-      this.keyFilter = null;
     },
 
-    setParser: function(name) {
+    setParser: function(name, parserConfig) {
       Editor.Parser = window[name];
+      parserConfig = parserConfig || this.options.parserConfig;
+      if (parserConfig && Editor.Parser.configure)
+        Editor.Parser.configure(parserConfig);
+
       if (this.container.firstChild) {
         forEach(this.container.childNodes, function(n) {
           if (n.nodeType != 3) n.dirty = true;
@@ -686,19 +732,19 @@ var Editor = (function(){
 
     // Intercept enter and tab, and assign their new functions.
     keyDown: function(event) {
-      if (this.frozen == "leave") this.frozen = null;
-      if (this.frozen && (!this.keyFilter || this.keyFilter(event))) {
+      if (this.frozen == "leave") {this.frozen = null; this.keyFilter = null;}
+      if (this.frozen && (!this.keyFilter || this.keyFilter(event.keyCode, event))) {
         event.stop();
         this.frozen(event);
         return;
       }
 
-      var code = event.keyCode;
+      var code = this.lastKeyDownCode = event.keyCode;
       // Don't scan when the user is typing.
       this.delayScanning();
       // Schedule a paren-highlight event, if configured.
       if (this.options.autoMatchParens)
-        this.scheduleParenBlink();
+        this.scheduleParenHighlight();
 
       // The various checks for !altKey are there because AltGr sets both
       // ctrlKey and altKey to true, and should not be recognised as
@@ -714,8 +760,8 @@ var Editor = (function(){
         }
         event.stop();
       }
-      else if (code == 9 && this.options.tabMode != "default") { // tab
-        this.handleTab(!event.ctrlKey && !event.shiftKey);
+      else if (code == 9 && this.options.tabMode != "default" && !event.ctrlKey) { // tab
+        this.handleTab(!event.shiftKey);
         event.stop();
       }
       else if (code == 32 && event.shiftKey && this.options.tabMode == "default") { // space
@@ -723,23 +769,32 @@ var Editor = (function(){
         event.stop();
       }
       else if (code == 36 && !event.shiftKey && !event.ctrlKey) { // home
-        if (this.home())
-          event.stop();
+        if (this.home()) event.stop();
+      }
+      else if (code == 35 && !event.shiftKey && !event.ctrlKey) { // end
+        if (this.end()) event.stop();
+      }
+      // Only in Firefox is the default behavior for PgUp/PgDn correct.
+      else if (code == 33 && !event.shiftKey && !event.ctrlKey && !gecko) { // PgUp
+        if (this.pageUp()) event.stop();
+      }
+      else if (code == 34 && !event.shiftKey && !event.ctrlKey && !gecko) {  // PgDn
+        if (this.pageDown()) event.stop();
       }
       else if ((code == 219 || code == 221) && event.ctrlKey && !event.altKey) { // [, ]
-        this.blinkParens(event.shiftKey);
+        this.highlightParens(event.shiftKey, true);
         event.stop();
       }
       else if (event.metaKey && !event.shiftKey && (code == 37 || code == 39)) { // Meta-left/right
         var cursor = select.selectionTopNode(this.container);
-        if (cursor === false || !this.container.firstChild) return;
-
-        if (code == 37) select.focusAfterNode(startOfLine(cursor), this.container);
-        else {
-          var end = endOfLine(cursor, this.container);
-          select.focusAfterNode(end ? end.previousSibling : this.container.lastChild, this.container);
+        if (cursor !== false && this.container.firstChild) {
+          if (code == 37) select.focusAfterNode(startOfLine(cursor), this.container);
+          else {
+            var end = endOfLine(cursor, this.container);
+            select.focusAfterNode(end ? end.previousSibling : this.container.lastChild, this.container);
+          }
+          event.stop();
         }
-        event.stop();
       }
       else if ((event.ctrlKey || event.metaKey) && !event.altKey) {
         if ((event.shiftKey && code == 90) || code == 89) { // shift-Z, Y
@@ -754,31 +809,49 @@ var Editor = (function(){
           this.options.saveFunction();
           event.stop();
         }
+        else if (internetExplorer && code == 86) {
+          this.reroutePasteEvent();
+        }
       }
+      this.keyUpOrPressAfterLastKeyDown = false;
     },
 
     // Check for characters that should re-indent the current line,
     // and prevent Opera from handling enter and tab anyway.
     keyPress: function(event) {
+      this.keyUpOrPressAfterLastKeyDown = true;
       var electric = Editor.Parser.electricChars, self = this;
       // Hack for Opera, and Firefox on OS X, in which stopping a
       // keydown event does not prevent the associated keypress event
       // from happening, so we have to cancel enter and tab again
       // here.
-      if ((this.frozen && (!this.keyFilter || this.keyFilter(event.keyCode))) ||
+      if ((this.frozen && (!this.keyFilter || this.keyFilter(event.keyCode || event.code, event))) ||
           event.code == 13 || (event.code == 9 && this.options.tabMode != "default") ||
-          (event.keyCode == 32 && event.shiftKey && this.options.tabMode == "default"))
+          (event.code == 32 && event.shiftKey && this.options.tabMode == "default"))
         event.stop();
       else if (electric && electric.indexOf(event.character) != -1)
         this.parent.setTimeout(function(){self.indentAtCursor(null);}, 0);
       else if ((event.character == "v" || event.character == "V")
                && (event.ctrlKey || event.metaKey) && !event.altKey) // ctrl-V
         this.reroutePasteEvent();
+      // Work around a bug where pressing backspace at the end of a
+      // line often causes the cursor to jump to the start of the line
+      // in Opera 10.60.
+      else if (brokenOpera && event.code == 8) {
+        var sel = select.selectionTopNode(this.container), self = this,
+            next = sel ? sel.nextSibling : this.container.firstChild;
+        if (sel !== false && next && isBR(next))
+          this.parent.setTimeout(function(){
+            if (select.selectionTopNode(self.container) == next)
+              select.focusAfterNode(next.previousSibling, self.container);
+          }, 20);
+      }
     },
 
     // Mark the node at the cursor dirty when a non-safe key is
     // released.
     keyUp: function(event) {
+      this.keyUpOrPressAfterLastKeyDown = true;
       this.cursorActivity(isSafeKey(event.keyCode));
     },
 
@@ -828,6 +901,7 @@ var Editor = (function(){
         if (whiteSpace) {
           whiteSpace.currentText = makeWhiteSpace(newIndent);
           whiteSpace.firstChild.nodeValue = whiteSpace.currentText;
+          select.snapshotMove(whiteSpace.firstChild, whiteSpace.firstChild, indentDiff, true);
         }
         // Otherwise, we have to add a new whitespace node.
         else {
@@ -835,18 +909,18 @@ var Editor = (function(){
           whiteSpace.className = "whitespace";
           if (start) insertAfter(whiteSpace, start);
           else this.container.insertBefore(whiteSpace, this.container.firstChild);
+          select.snapshotMove(firstText && (firstText.firstChild || firstText),
+                              whiteSpace.firstChild, newIndent, false, true);
         }
-        if (firstText) select.snapshotMove(firstText.firstChild, whiteSpace.firstChild, curIndent, false, true);
       }
       if (indentDiff != 0) this.addDirtyNode(start);
-      return whiteSpace;
     },
 
     // Re-highlight the selected part of the document.
     highlightAtCursor: function() {
       var pos = select.selectionTopNode(this.container, true);
       var to = select.selectionTopNode(this.container, false);
-      if (pos === false || to === false) return;
+      if (pos === false || to === false) return false;
 
       select.markSelection(this.win);
       if (this.highlight(pos, endOfLine(to, this.container), true, 20) === false)
@@ -865,12 +939,14 @@ var Editor = (function(){
         this.reindentSelection(direction);
     },
 
+    // Custom home behaviour that doesn't land the cursor in front of
+    // leading whitespace unless pressed twice.
     home: function() {
       var cur = select.selectionTopNode(this.container, true), start = cur;
-      if (cur === false || !(!cur || cur.isPart || cur.nodeName == "BR") || !this.container.firstChild)
+      if (cur === false || !(!cur || cur.isPart || isBR(cur)) || !this.container.firstChild)
         return false;
 
-      while (cur && cur.nodeName != "BR") cur = cur.previousSibling;
+      while (cur && !isBR(cur)) cur = cur.previousSibling;
       var next = cur ? cur.nextSibling : this.container.firstChild;
       if (next && next != start && next.isPart && hasClass(next, "whitespace"))
         select.focusAfterNode(next, this.container);
@@ -881,18 +957,88 @@ var Editor = (function(){
       return true;
     },
 
-    // Delay (or initiate) the next paren blink event.
-    scheduleParenBlink: function() {
+    // Some browsers (Opera) don't manage to handle the end key
+    // properly in the face of vertical scrolling.
+    end: function() {
+      var cur = select.selectionTopNode(this.container, true);
+      if (cur === false) return false;
+      cur = endOfLine(cur, this.container);
+      if (!cur) return false;
+      select.focusAfterNode(cur.previousSibling, this.container);
+      select.scrollToCursor(this.container);
+      return true;
+    },
+
+    pageUp: function() {
+      var line = this.cursorPosition().line, scrollAmount = this.visibleLineCount();
+      if (line === false || scrollAmount === false) return false;
+      // Try to keep one line on the screen.
+      scrollAmount -= 2;
+      for (var i = 0; i < scrollAmount; i++) {
+        line = this.prevLine(line);
+        if (line === false) break;
+      }
+      if (i == 0) return false; // Already at first line
+      select.setCursorPos(this.container, {node: line, offset: 0});
+      select.scrollToCursor(this.container);
+      return true;
+    },
+
+    pageDown: function() {
+      var line = this.cursorPosition().line, scrollAmount = this.visibleLineCount();
+      if (line === false || scrollAmount === false) return false;
+      // Try to move to the last line of the current page.
+      scrollAmount -= 2;
+      for (var i = 0; i < scrollAmount; i++) {
+        var nextLine = this.nextLine(line);
+        if (nextLine === false) break;
+        line = nextLine;
+      }
+      if (i == 0) return false; // Already at last line
+      select.setCursorPos(this.container, {node: line, offset: 0});
+      select.scrollToCursor(this.container);
+      return true;
+    },
+
+    // Delay (or initiate) the next paren highlight event.
+    scheduleParenHighlight: function() {
       if (this.parenEvent) this.parent.clearTimeout(this.parenEvent);
       var self = this;
-      this.parenEvent = this.parent.setTimeout(function(){self.blinkParens();}, 300);
+      this.parenEvent = this.parent.setTimeout(function(){self.highlightParens();}, 300);
     },
 
     // Take the token before the cursor. If it contains a character in
     // '()[]{}', search for the matching paren/brace/bracket, and
     // highlight them in green for a moment, or red if no proper match
     // was found.
-    blinkParens: function(jump) {
+    highlightParens: function(jump, fromKey) {
+      var self = this;
+      // give the relevant nodes a colour.
+      function highlight(node, ok) {
+        if (!node) return;
+        if (self.options.markParen) {
+          self.options.markParen(node, ok);
+        }
+        else {
+          node.style.fontWeight = "bold";
+          node.style.color = ok ? "#8F8" : "#F88";
+        }
+      }
+      function unhighlight(node) {
+        if (!node) return;
+        if (self.options.unmarkParen) {
+          self.options.unmarkParen(node);
+        }
+        else {
+          node.style.fontWeight = "";
+          node.style.color = "";
+        }
+      }
+      if (!fromKey && self.highlighted) {
+        unhighlight(self.highlighted[0]);
+        unhighlight(self.highlighted[1]);
+      }
+
       if (!window.select) return;
       // Clear the event property.
       if (this.parenEvent) this.parent.clearTimeout(this.parenEvent);
@@ -910,7 +1056,7 @@ var Editor = (function(){
         return /[\(\[\{]/.test(ch);
       }
 
-      var ch, self = this, cursor = select.selectionTopNode(this.container, true);
+      var ch, cursor = select.selectionTopNode(this.container, true);
       if (!cursor || !this.highlightAtCursor()) return;
       cursor = select.selectionTopNode(this.container, true);
       if (!(cursor && ((ch = paren(cursor)) || (cursor = cursor.nextSibling) && (ch = paren(cursor)))))
@@ -923,9 +1069,9 @@ var Editor = (function(){
       // have to scan, we just try, and when we find dirty nodes we
       // abort, parse them, and re-try.
       function tryFindMatch() {
-        var stack = [], ch, ok = true;;
+        var stack = [], ch, ok = true;
         for (var runner = cursor; runner; runner = dir ? runner.nextSibling : runner.previousSibling) {
-          if (runner.className == className && runner.nodeName == "SPAN" && (ch = paren(runner))) {
+          if (runner.className == className && isSpan(runner) && (ch = paren(runner))) {
             if (forward(ch) == dir)
               stack.push(ch);
             else if (!stack.length)
@@ -934,18 +1080,12 @@ var Editor = (function(){
               ok = false;
             if (!stack.length) break;
           }
-          else if (runner.dirty || runner.nodeName != "SPAN" && runner.nodeName != "BR") {
+          else if (runner.dirty || !isSpan(runner) && !isBR(runner)) {
             return {node: runner, status: "dirty"};
           }
         }
         return {node: runner, status: runner && ok};
       }
-      // Temporarily give the relevant nodes a colour.
-      function blink(node, ok) {
-        node.style.fontWeight = "bold";
-        node.style.color = ok ? "#8F8" : "#F88";
-        self.parent.setTimeout(function() {node.style.fontWeight = ""; node.style.color = "";}, 500);
-      }
 
       while (true) {
         var found = tryFindMatch();
@@ -957,11 +1097,14 @@ var Editor = (function(){
           continue;
         }
         else {
-          blink(cursor, found.status);
-          if (found.node) {
-            blink(found.node, found.status);
-            if (jump) select.focusAfterNode(found.node.previousSibling, this.container);
-          }
+          highlight(cursor, found.status);
+          highlight(found.node, found.status);
+          if (fromKey)
+            self.parent.setTimeout(function() {unhighlight(cursor); unhighlight(found.node);}, 500);
+          else
+            self.highlighted = [cursor, found.node];
+          if (jump && found.node)
+            select.focusAfterNode(found.node.previousSibling, this.container);
           break;
         }
       }
@@ -979,20 +1122,17 @@ var Editor = (function(){
       // there's nothing to indent.
       if (cursor === false)
         return;
-      var lineStart = startOfLine(cursor);
-      var whiteSpace = this.indentLineAfter(lineStart, direction);
-      if (cursor == lineStart && whiteSpace)
-          cursor = whiteSpace;
-      // This means the indentation has probably messed up the cursor.
-      if (cursor == whiteSpace)
-        select.focusAfterNode(cursor, this.container);
+      select.markSelection(this.win);
+      this.indentLineAfter(startOfLine(cursor), direction);
+      select.selectMarked();
     },
 
     // Indent all lines whose start falls inside of the current
     // selection.
     indentRegion: function(start, end, direction) {
       var current = (start = startOfLine(start)), before = start && startOfLine(start.previousSibling);
-      if (end.nodeName != "BR") end = endOfLine(end, this.container);
+      if (!isBR(end)) end = endOfLine(end, this.container);
+      this.addDirtyNode(start);
 
       do {
         var next = endOfLine(current, this.container);
@@ -1007,9 +1147,16 @@ var Editor = (function(){
     // Find the node that the cursor is in, mark it as dirty, and make
     // sure a highlight pass is scheduled.
     cursorActivity: function(safe) {
+      // pagehide event hack above
+      if (this.unloaded) {
+        this.win.document.designMode = "off";
+        this.win.document.designMode = "on";
+        this.unloaded = false;
+      }
+
       if (internetExplorer) {
         this.container.createTextRange().execCommand("unlink");
-        this.selectionSnapshot = select.selectionCoords(this.win);
+        this.selectionSnapshot = select.getBookmark(this.container);
       }
 
       var activity = this.options.cursorActivity;
@@ -1045,6 +1192,10 @@ var Editor = (function(){
       this.dirty.push(node);
     },
 
+    allClean: function() {
+      return !this.dirty.length;
+    },
+
     // Cause a highlight pass to happen in options.passDelay
     // milliseconds. Clear the existing timeout, if one exists. This
     // way, the passes do not happen while the user is typing, and
@@ -1067,7 +1218,7 @@ var Editor = (function(){
           // If the node has been coloured in the meantime, or is no
           // longer in the document, it should not be returned.
           while (found && found.parentNode != this.container)
-            found = found.parentNode
+            found = found.parentNode;
           if (found && (found.dirty || found.nodeType == 3))
             return found;
         } catch (e) {}
@@ -1084,7 +1235,7 @@ var Editor = (function(){
     highlightDirty: function(force) {
       // Prevent FF from raising an error when it is firing timeouts
       // on a page that's no longer loaded.
-      if (!window.select) return;
+      if (!window.select) return false;
 
       if (!this.options.readOnly) select.markSelection(this.win);
       var start, endTime = force ? null : time() + this.options.passTime;
@@ -1127,6 +1278,12 @@ var Editor = (function(){
       }
     },
 
+    isIMEOn: function() {
+      // chrome:  keyDown keyCode is 229 while IME on
+      // firefox: no keyUps or keyPresses fires after first keyDown while IME on
+      return this.lastKeyDownCode == 229 || this.keyUpOrPressAfterLastKeyDown === false;
+    },
+
     // The function that does the actual highlighting/colouring (with
     // help from the parser and the DOM normalizer). Its interface is
     // rather overcomplicated, because it is used in different
@@ -1146,18 +1303,18 @@ var Editor = (function(){
       var container = this.container, self = this, active = this.options.activeTokens;
       var endTime = (typeof target == "number" ? target : null);
 
-      if (!container.firstChild)
-        return;
+      if (!container.firstChild || this.isIMEOn())
+        return false;
       // Backtrack to the first node before from that has a partial
       // parse stored.
       while (from && (!from.parserFromHere || from.dirty)) {
-        if (maxBacktrack != null && from.nodeName == "BR" && (--maxBacktrack) < 0)
+        if (maxBacktrack != null && isBR(from) && (--maxBacktrack) < 0)
           return false;
         from = from.previousSibling;
       }
       // If we are at the end of the document, do nothing.
       if (from && !from.nextSibling)
-        return;
+        return false;
 
       // Check whether a part (<span> node) and the corresponding token
       // match.
@@ -1175,7 +1332,7 @@ var Editor = (function(){
       }
       // Create a part corresponding to a given token.
       function tokenPart(token){
-        var part = makePartSpan(token.value, self.doc);
+        var part = makePartSpan(token.value, self.doc);     
         part.className = token.style;
         return part;
       }
@@ -1202,6 +1359,11 @@ var Editor = (function(){
           stream = stringStream(traversal),
           parsed = from ? from.parserFromHere(stream) : Editor.Parser.make(stream);
 
+      function surroundedByBRs(node) {
+        return (node.previousSibling == null || isBR(node.previousSibling)) &&
+               (node.nextSibling == null || isBR(node.nextSibling));
+      }
+
       // parts is an interface to make it possible to 'delay' fetching
       // the next DOM node until we are completely done with the one
       // before it. This is necessary because often the next node is
@@ -1232,13 +1394,23 @@ var Editor = (function(){
           // Allow empty nodes when they are alone on a line, needed
           // for the FF cursor bug workaround (see select.js,
           // insertNewlineAtCursor).
-          while (part && part.nodeName == "SPAN" && part.currentText == "") {
-            var old = part;
-            this.remove();
-            part = this.get();
-            // Adjust selection information, if any. See select.js for details.
-            select.snapshotMove(old.firstChild, part && (part.firstChild || part), 0);
+          while (part && isSpan(part) && part.currentText == "") {
+            // Leave empty nodes that are alone on a line alone in
+            // Opera, since that browsers doesn't deal well with
+            // having 2 BRs in a row.
+            if (window.opera && surroundedByBRs(part)) {
+              this.next();
+              part = this.get();
+            }
+            else {
+              var old = part;
+              this.remove();
+              part = this.get();
+              // Adjust selection information, if any. See select.js for details.
+              select.snapshotMove(old.firstChild, part && (part.firstChild || part), 0);
+            }
           }
+          
           return part;
         }
       };
@@ -1254,7 +1426,7 @@ var Editor = (function(){
         if (token.value == "\n"){
           // The idea of the two streams actually staying synchronized
           // is such a long shot that we explicitly check.
-          if (part.nodeName != "BR")
+          if (!isBR(part))
             throw "Parser out of sync. Expected BR.";
 
           if (part.dirty || !part.indentation) lineDirty = true;
@@ -1282,7 +1454,7 @@ var Editor = (function(){
           parts.next();
         }
         else {
-          if (part.nodeName != "SPAN")
+          if (!isSpan(part))
             throw "Parser out of sync. Expected SPAN.";
           if (part.dirty)
             lineDirty = true;
@@ -1323,7 +1495,6 @@ var Editor = (function(){
         }
       });
       maybeTouch(from);
-      webkitLastLineHack(this.container);
 
       // The function returns some status information that is used by
       // hightlightDirty to determine whether and where it has to
@@ -1338,6 +1509,6 @@ var Editor = (function(){
 
 addEventHandler(window, "load", function() {
   var CodeMirror = window.frameElement.CodeMirror;
-  CodeMirror.editor = new Editor(CodeMirror.options);
+  var e = CodeMirror.editor = new Editor(CodeMirror.options);
   this.parent.setTimeout(method(CodeMirror, "init"), 0);
 });