Upstream version 10.39.225.0
[platform/framework/web/crosswalk.git] / src / third_party / WebKit / Source / devtools / front_end / cm / codemirror.js
index fa31adc..c8f4809 100644 (file)
@@ -1,3 +1,6 @@
+// CodeMirror, copyright (c) by Marijn Haverbeke and others
+// Distributed under an MIT license: http://codemirror.net/LICENSE
+
 // This is CodeMirror (http://codemirror.net), a code editor
 // implemented in JavaScript on top of the browser's DOM.
 //
   var gecko = /gecko\/\d/i.test(navigator.userAgent);
   // ie_uptoN means Internet Explorer version N or lower
   var ie_upto10 = /MSIE \d/.test(navigator.userAgent);
-  var ie_upto7 = ie_upto10 && (document.documentMode == null || document.documentMode < 8);
-  var ie_upto8 = ie_upto10 && (document.documentMode == null || document.documentMode < 9);
-  var ie_upto9 = ie_upto10 && (document.documentMode == null || document.documentMode < 10);
-  var ie_11up = /Trident\/([7-9]|\d{2,})\./.test(navigator.userAgent);
+  var ie_11up = /Trident\/(?:[7-9]|\d{2,})\..*rv:(\d+)/.exec(navigator.userAgent);
   var ie = ie_upto10 || ie_11up;
+  var ie_version = ie && (ie_upto10 ? document.documentMode || 6 : ie_11up[1]);
   var webkit = /WebKit\//.test(navigator.userAgent);
   var qtwebkit = webkit && /Qt\/\d+\.\d+/.test(navigator.userAgent);
   var chrome = /Chrome\//.test(navigator.userAgent);
@@ -47,7 +48,7 @@
   if (presto_version && presto_version >= 15) { presto = false; webkit = true; }
   // Some browsers use the wrong event properties to signal cmd/ctrl on OS X
   var flipCtrlCmd = mac && (qtwebkit || presto && (presto_version == null || presto_version < 12.11));
-  var captureRightClick = gecko || (ie && !ie_upto8);
+  var captureRightClick = gecko || (ie && ie_version >= 9);
 
   // Optimize some code when these features are not used.
   var sawReadOnlySpans = false, sawCollapsedSpans = false;
 
     // Override magic textarea content restore that IE sometimes does
     // on our hidden textarea on reload
-    if (ie_upto10) setTimeout(bind(resetInput, this, true), 20);
+    if (ie && ie_version < 11) setTimeout(bind(resetInput, this, true), 20);
 
     registerEventHandlers(this);
+    ensureGlobalHandlers();
 
     var cm = this;
     runInOp(this, function() {
 
       for (var opt in optionHandlers) if (optionHandlers.hasOwnProperty(opt))
         optionHandlers[opt](cm, options[opt], Init);
+      maybeUpdateLineNumberWidth(cm);
       for (var i = 0; i < initHooks.length; ++i) initHooks[i](cm);
     });
   }
                             d.scrollbarFiller, d.gutterFiller, d.scroller], "CodeMirror");
 
     // Work around IE7 z-index bug (not perfect, hence IE7 not really being supported)
-    if (ie_upto7) { d.gutters.style.zIndex = -1; d.scroller.style.paddingRight = 0; }
+    if (ie && ie_version < 8) { d.gutters.style.zIndex = -1; d.scroller.style.paddingRight = 0; }
     // Needed to hide big blue blinking cursor on Mobile Safari
     if (ios) input.style.width = "0px";
     if (!webkit) d.scroller.draggable = true;
     // Needed to handle Tab key in KHTML
     if (khtml) { d.inputDiv.style.height = "1px"; d.inputDiv.style.position = "absolute"; }
     // Need to set a minimum width to see the scrollbar on IE7 (but must not set it on IE8).
-    if (ie_upto7) d.scrollbarH.style.minHeight = d.scrollbarV.style.minWidth = "18px";
+    if (ie && ie_version < 8) d.scrollbarH.style.minHeight = d.scrollbarV.style.minWidth = "18px";
 
     if (place.appendChild) place.appendChild(d.wrapper);
     else place(d.wrapper);
 
     // True when shift is held down.
     d.shift = false;
+
+    // Used to track whether anything happened since the context menu
+    // was opened.
+    d.selForContextMenu = null;
   }
 
   // STATE UPDATES
 
   // SCROLLBARS
 
+  function hScrollbarTakesSpace(cm) {
+    return cm.display.scroller.clientHeight - cm.display.wrapper.clientHeight < scrollerCutOff - 3;
+  }
+
   // Prepare DOM reads needed to update the scrollbars. Done in one
   // shot to minimize update/measure roundtrips.
   function measureForScrollbars(cm) {
       clientHeight: scroll.clientHeight,
       barHeight: cm.display.scrollbarV.clientHeight,
       scrollWidth: scroll.scrollWidth, clientWidth: scroll.clientWidth,
+      hScrollbarTakesSpace: hScrollbarTakesSpace(cm),
       barWidth: cm.display.scrollbarH.clientWidth,
       docHeight: Math.round(cm.doc.height + paddingVert(cm.display))
     };
   // content.
   function updateScrollbars(cm, measure) {
     if (!measure) measure = measureForScrollbars(cm);
-    var d = cm.display;
+    var d = cm.display, sWidth = scrollbarWidth(d.measure);
     var scrollHeight = measure.docHeight + scrollerCutOff;
     var needsH = measure.scrollWidth > measure.clientWidth;
+    if (needsH && measure.scrollWidth <= measure.clientWidth + 1 &&
+        sWidth > 0 && !measure.hScrollbarTakesSpace)
+      needsH = false; // (Issue #2562)
     var needsV = scrollHeight > measure.clientHeight;
+
     if (needsV) {
       d.scrollbarV.style.display = "block";
-      d.scrollbarV.style.bottom = needsH ? scrollbarWidth(d.measure) + "px" : "0";
+      d.scrollbarV.style.bottom = needsH ? sWidth + "px" : "0";
       // A bug in IE8 can cause this value to be negative, so guard it.
       d.scrollbarV.firstChild.style.height =
         Math.max(0, scrollHeight - measure.clientHeight + (measure.barHeight || d.scrollbarV.clientHeight)) + "px";
     }
     if (needsH) {
       d.scrollbarH.style.display = "block";
-      d.scrollbarH.style.right = needsV ? scrollbarWidth(d.measure) + "px" : "0";
+      d.scrollbarH.style.right = needsV ? sWidth + "px" : "0";
       d.scrollbarH.firstChild.style.width =
         (measure.scrollWidth - measure.clientWidth + (measure.barWidth || d.scrollbarH.clientWidth)) + "px";
     } else {
     }
     if (needsH && needsV) {
       d.scrollbarFiller.style.display = "block";
-      d.scrollbarFiller.style.height = d.scrollbarFiller.style.width = scrollbarWidth(d.measure) + "px";
+      d.scrollbarFiller.style.height = d.scrollbarFiller.style.width = sWidth + "px";
     } else d.scrollbarFiller.style.display = "";
     if (needsH && cm.options.coverGutterNextToScrollbar && cm.options.fixedGutter) {
       d.gutterFiller.style.display = "block";
-      d.gutterFiller.style.height = scrollbarWidth(d.measure) + "px";
+      d.gutterFiller.style.height = sWidth + "px";
       d.gutterFiller.style.width = d.gutters.offsetWidth + "px";
     } else d.gutterFiller.style.display = "";
 
     if (!cm.state.checkedOverlayScrollbar && measure.clientHeight > 0) {
-      if (scrollbarWidth(d.measure) === 0) {
+      if (sWidth === 0) {
         var w = mac && !mac_geMountainLion ? "12px" : "18px";
         d.scrollbarV.style.minWidth = d.scrollbarH.style.minHeight = w;
         var barMouseDown = function(e) {
   }
 
   // Compute the lines that are visible in a given viewport (defaults
-  // the the current scroll position). viewPort may contain top,
+  // the the current scroll position). viewport may contain top,
   // height, and ensure (see op.scrollToPos) properties.
-  function visibleLines(display, doc, viewPort) {
-    var top = viewPort && viewPort.top != null ? viewPort.top : display.scroller.scrollTop;
+  function visibleLines(display, doc, viewport) {
+    var top = viewport && viewport.top != null ? Math.max(0, viewport.top) : display.scroller.scrollTop;
     top = Math.floor(top - paddingTop(display));
-    var bottom = viewPort && viewPort.bottom != null ? viewPort.bottom : top + display.wrapper.clientHeight;
+    var bottom = viewport && viewport.bottom != null ? viewport.bottom : top + display.wrapper.clientHeight;
 
     var from = lineAtHeight(doc, top), to = lineAtHeight(doc, bottom);
     // Ensure is a {from: {line, ch}, to: {line, ch}} object, and
     // forces those lines into the viewport (if possible).
-    if (viewPort && viewPort.ensure) {
-      var ensureFrom = viewPort.ensure.from.line, ensureTo = viewPort.ensure.to.line;
+    if (viewport && viewport.ensure) {
+      var ensureFrom = viewport.ensure.from.line, ensureTo = viewport.ensure.to.line;
       if (ensureFrom < from)
         return {from: ensureFrom,
                 to: lineAtHeight(doc, heightAtLine(getLine(doc, ensureFrom)) + display.wrapper.clientHeight)};
         return {from: lineAtHeight(doc, heightAtLine(getLine(doc, ensureTo)) - display.wrapper.clientHeight),
                 to: ensureTo};
     }
-    return {from: from, to: to};
+    return {from: from, to: Math.max(to, from + 1)};
   }
 
   // LINE NUMBERS
 
   // DISPLAY DRAWING
 
-  // Updates the display, selection, and scrollbars, using the
-  // information in display.view to find out which nodes are no longer
-  // up-to-date. Tries to bail out early when no changes are needed,
-  // unless forced is true.
-  // Returns true if an actual update happened, false otherwise.
-  function updateDisplay(cm, viewPort, forced) {
-    var oldFrom = cm.display.viewFrom, oldTo = cm.display.viewTo, updated;
-    var visible = visibleLines(cm.display, cm.doc, viewPort);
-    for (var first = true;; first = false) {
-      var oldWidth = cm.display.scroller.clientWidth;
-      if (!updateDisplayInner(cm, visible, forced)) break;
-      updated = true;
-
-      // If the max line changed since it was last measured, measure it,
-      // and ensure the document's width matches it.
-      if (cm.display.maxLineChanged && !cm.options.lineWrapping)
-        adjustContentWidth(cm);
-
-      var barMeasure = measureForScrollbars(cm);
-      updateSelection(cm);
-      setDocumentHeight(cm, barMeasure);
-      updateScrollbars(cm, barMeasure);
-      if (webkit && cm.options.lineWrapping)
-        checkForWebkitWidthBug(cm, barMeasure); // (Issue #2420)
-      if (first && cm.options.lineWrapping && oldWidth != cm.display.scroller.clientWidth) {
-        forced = true;
-        continue;
-      }
-      forced = false;
-
-      // Clip forced viewport to actual scrollable area.
-      if (viewPort && viewPort.top != null)
-        viewPort = {top: Math.min(barMeasure.docHeight - scrollerCutOff - barMeasure.clientHeight, viewPort.top)};
-      // Updated line heights might result in the drawn area not
-      // actually covering the viewport. Keep looping until it does.
-      visible = visibleLines(cm.display, cm.doc, viewPort);
-      if (visible.from >= cm.display.viewFrom && visible.to <= cm.display.viewTo)
-        break;
-    }
+  function DisplayUpdate(cm, viewport, force) {
+    var display = cm.display;
 
-    cm.display.updateLineNumbers = null;
-    if (updated) {
-      signalLater(cm, "update", cm);
-      if (cm.display.viewFrom != oldFrom || cm.display.viewTo != oldTo)
-        signalLater(cm, "viewportChange", cm, cm.display.viewFrom, cm.display.viewTo);
-    }
-    return updated;
+    this.viewport = viewport;
+    // Store some values that we'll need later (but don't want to force a relayout for)
+    this.visible = visibleLines(display, cm.doc, viewport);
+    this.editorIsHidden = !display.wrapper.offsetWidth;
+    this.wrapperHeight = display.wrapper.clientHeight;
+    this.oldViewFrom = display.viewFrom; this.oldViewTo = display.viewTo;
+    this.oldScrollerWidth = display.scroller.clientWidth;
+    this.force = force;
+    this.dims = getDimensions(cm);
   }
 
   // Does the actual updating of the line display. Bails out
   // (returning false) when there is nothing to be done and forced is
   // false.
-  function updateDisplayInner(cm, visible, forced) {
+  function updateDisplayIfNeeded(cm, update) {
     var display = cm.display, doc = cm.doc;
-    if (!display.wrapper.offsetWidth) {
+    if (update.editorIsHidden) {
       resetView(cm);
-      return;
+      return false;
     }
 
     // Bail out if the visible area is already rendered and nothing changed.
-    if (!forced && visible.from >= display.viewFrom && visible.to <= display.viewTo &&
+    if (!update.force &&
+        update.visible.from >= display.viewFrom && update.visible.to <= display.viewTo &&
+        (display.updateLineNumbers == null || display.updateLineNumbers >= display.viewTo) &&
         countDirtyView(cm) == 0)
-      return;
+      return false;
 
-    if (maybeUpdateLineNumberWidth(cm))
+    if (maybeUpdateLineNumberWidth(cm)) {
       resetView(cm);
-    var dims = getDimensions(cm);
+      update.dims = getDimensions(cm);
+    }
 
     // Compute a suitable new viewport (from & to)
     var end = doc.first + doc.size;
-    var from = Math.max(visible.from - cm.options.viewportMargin, doc.first);
-    var to = Math.min(end, visible.to + cm.options.viewportMargin);
+    var from = Math.max(update.visible.from - cm.options.viewportMargin, doc.first);
+    var to = Math.min(end, update.visible.to + cm.options.viewportMargin);
     if (display.viewFrom < from && from - display.viewFrom < 20) from = Math.max(doc.first, display.viewFrom);
     if (display.viewTo > to && display.viewTo - to < 20) to = Math.min(end, display.viewTo);
     if (sawCollapsedSpans) {
     }
 
     var different = from != display.viewFrom || to != display.viewTo ||
-      display.lastSizeC != display.wrapper.clientHeight;
+      display.lastSizeC != update.wrapperHeight;
     adjustView(cm, from, to);
 
     display.viewOffset = heightAtLine(getLine(cm.doc, display.viewFrom));
     cm.display.mover.style.top = display.viewOffset + "px";
 
     var toUpdate = countDirtyView(cm);
-    if (!different && toUpdate == 0 && !forced) return;
+    if (!different && toUpdate == 0 && !update.force &&
+        (display.updateLineNumbers == null || display.updateLineNumbers >= display.viewTo))
+      return false;
 
     // For big changes, we hide the enclosing element during the
     // update, since that speeds up the operations on most browsers.
     var focused = activeElt();
     if (toUpdate > 4) display.lineDiv.style.display = "none";
-    patchDisplay(cm, display.updateLineNumbers, dims);
+    patchDisplay(cm, display.updateLineNumbers, update.dims);
     if (toUpdate > 4) display.lineDiv.style.display = "";
     // There might have been a widget with a focused element that got
     // hidden or updated, if so re-focus it.
     removeChildren(display.selectionDiv);
 
     if (different) {
-      display.lastSizeC = display.wrapper.clientHeight;
+      display.lastSizeC = update.wrapperHeight;
       startWorker(cm, 400);
     }
 
-    updateHeightsInViewport(cm);
+    display.updateLineNumbers = null;
 
     return true;
   }
 
-  function adjustContentWidth(cm) {
-    var display = cm.display;
-    var width = measureChar(cm, display.maxLine, display.maxLine.text.length).left;
-    display.maxLineChanged = false;
-    var minWidth = Math.max(0, width + 3);
-    var maxScrollLeft = Math.max(0, display.sizer.offsetLeft + minWidth + scrollerCutOff - display.scroller.clientWidth);
-    display.sizer.style.minWidth = minWidth + "px";
-    if (maxScrollLeft < cm.doc.scrollLeft)
-      setScrollLeft(cm, Math.min(display.scroller.scrollLeft, maxScrollLeft), true);
+  function postUpdateDisplay(cm, update) {
+    var force = update.force, viewport = update.viewport;
+    for (var first = true;; first = false) {
+      if (first && cm.options.lineWrapping && update.oldScrollerWidth != cm.display.scroller.clientWidth) {
+        force = true;
+      } else {
+        force = false;
+        // Clip forced viewport to actual scrollable area.
+        if (viewport && viewport.top != null)
+          viewport = {top: Math.min(cm.doc.height + paddingVert(cm.display) - scrollerCutOff -
+                                    cm.display.scroller.clientHeight, viewport.top)};
+        // Updated line heights might result in the drawn area not
+        // actually covering the viewport. Keep looping until it does.
+        update.visible = visibleLines(cm.display, cm.doc, viewport);
+        if (update.visible.from >= cm.display.viewFrom && update.visible.to <= cm.display.viewTo)
+          break;
+      }
+      if (!updateDisplayIfNeeded(cm, update)) break;
+      updateHeightsInViewport(cm);
+      var barMeasure = measureForScrollbars(cm);
+      updateSelection(cm);
+      setDocumentHeight(cm, barMeasure);
+      updateScrollbars(cm, barMeasure);
+    }
+
+    signalLater(cm, "update", cm);
+    if (cm.display.viewFrom != update.oldViewFrom || cm.display.viewTo != update.oldViewTo)
+      signalLater(cm, "viewportChange", cm, cm.display.viewFrom, cm.display.viewTo);
+  }
+
+  function updateDisplaySimple(cm, viewport) {
+    var update = new DisplayUpdate(cm, viewport);
+    if (updateDisplayIfNeeded(cm, update)) {
+      updateHeightsInViewport(cm);
+      postUpdateDisplay(cm, update);
+      var barMeasure = measureForScrollbars(cm);
+      updateSelection(cm);
+      setDocumentHeight(cm, barMeasure);
+      updateScrollbars(cm, barMeasure);
+    }
   }
 
   function setDocumentHeight(cm, measure) {
     cm.display.gutters.style.height = Math.max(measure.docHeight, measure.clientHeight - scrollerCutOff) + "px";
   }
 
-
   function checkForWebkitWidthBug(cm, measure) {
     // Work around Webkit bug where it sometimes reserves space for a
     // non-existing phantom scrollbar in the scroller (Issue #2420)
     for (var i = 0; i < display.view.length; i++) {
       var cur = display.view[i], height;
       if (cur.hidden) continue;
-      if (ie_upto7) {
+      if (ie && ie_version < 8) {
         var bot = cur.node.offsetTop + cur.node.offsetHeight;
         height = bot - prevBottom;
         prevBottom = bot;
       if (lineView.text.parentNode)
         lineView.text.parentNode.replaceChild(lineView.node, lineView.text);
       lineView.node.appendChild(lineView.text);
-      if (ie_upto7) lineView.node.style.zIndex = 2;
+      if (ie && ie_version < 8) lineView.node.style.zIndex = 2;
     }
     return lineView.node;
   }
     if (hasHandler(doc, "beforeSelectionChange") || doc.cm && hasHandler(doc.cm, "beforeSelectionChange"))
       sel = filterSelectionChange(doc, sel);
 
-    var bias = cmp(sel.primary().head, doc.sel.primary().head) < 0 ? -1 : 1;
+    var bias = options && options.bias ||
+      (cmp(sel.primary().head, doc.sel.primary().head) < 0 ? -1 : 1);
     setSelectionInner(doc, skipAtomicInSelection(doc, sel, bias, true));
 
     if (!(options && options.scroll === false) && doc.cm)
   // SELECTION DRAWING
 
   // Redraw the selection and/or cursor
-  function updateSelection(cm) {
-    var display = cm.display, doc = cm.doc;
-    var curFragment = document.createDocumentFragment();
-    var selFragment = document.createDocumentFragment();
+  function drawSelection(cm) {
+    var display = cm.display, doc = cm.doc, result = {};
+    var curFragment = result.cursors = document.createDocumentFragment();
+    var selFragment = result.selection = document.createDocumentFragment();
 
     for (var i = 0; i < doc.sel.ranges.length; i++) {
       var range = doc.sel.ranges[i];
     if (cm.options.moveInputWithCursor) {
       var headPos = cursorCoords(cm, doc.sel.primary().head, "div");
       var wrapOff = display.wrapper.getBoundingClientRect(), lineOff = display.lineDiv.getBoundingClientRect();
-      var top = Math.max(0, Math.min(display.wrapper.clientHeight - 10,
-                                     headPos.top + lineOff.top - wrapOff.top));
-      var left = Math.max(0, Math.min(display.wrapper.clientWidth - 10,
-                                      headPos.left + lineOff.left - wrapOff.left));
-      display.inputDiv.style.top = top + "px";
-      display.inputDiv.style.left = left + "px";
+      result.teTop = Math.max(0, Math.min(display.wrapper.clientHeight - 10,
+                                          headPos.top + lineOff.top - wrapOff.top));
+      result.teLeft = Math.max(0, Math.min(display.wrapper.clientWidth - 10,
+                                           headPos.left + lineOff.left - wrapOff.left));
     }
 
-    removeChildrenAndAdd(display.cursorDiv, curFragment);
-    removeChildrenAndAdd(display.selectionDiv, selFragment);
+    return result;
+  }
+
+  function showSelection(cm, drawn) {
+    removeChildrenAndAdd(cm.display.cursorDiv, drawn.cursors);
+    removeChildrenAndAdd(cm.display.selectionDiv, drawn.selection);
+    if (drawn.teTop != null) {
+      cm.display.inputDiv.style.top = drawn.teTop + "px";
+      cm.display.inputDiv.style.left = drawn.teLeft + "px";
+    }
+  }
+
+  function updateSelection(cm) {
+    showSelection(cm, drawSelection(cm));
   }
 
   // Draws a cursor for the given range
   function drawSelectionCursor(cm, range, output) {
-    var pos = cursorCoords(cm, range.head, "div");
+    var pos = cursorCoords(cm, range.head, "div", null, null, !cm.options.singleCursorHeightPerLine);
 
     var cursor = output.appendChild(elt("div", "\u00a0", "CodeMirror-cursor"));
     cursor.style.left = pos.left + "px";
       display.blinker = setInterval(function() {
         display.cursorDiv.style.visibility = (on = !on) ? "" : "hidden";
       }, cm.options.cursorBlinkRate);
+    else if (cm.options.cursorBlinkRate < 0)
+      display.cursorDiv.style.visibility = "hidden";
   }
 
   // HIGHLIGHT WORKER
     if (doc.frontier >= cm.display.viewTo) return;
     var end = +new Date + cm.options.workTime;
     var state = copyState(doc.mode, getStateBefore(cm, doc.frontier));
+    var changedLines = [];
 
-    runInOp(cm, function() {
     doc.iter(doc.frontier, Math.min(doc.first + doc.size, cm.display.viewTo + 500), function(line) {
       if (doc.frontier >= cm.display.viewFrom) { // Visible
         var oldStyles = line.styles;
         var highlighted = highlightLine(cm, line, state, true);
         line.styles = highlighted.styles;
-        if (highlighted.classes) line.styleClasses = highlighted.classes;
-        else if (line.styleClasses) line.styleClasses = null;
-        var ischange = !oldStyles || oldStyles.length != line.styles.length;
+        var oldCls = line.styleClasses, newCls = highlighted.classes;
+        if (newCls) line.styleClasses = newCls;
+        else if (oldCls) line.styleClasses = null;
+        var ischange = !oldStyles || oldStyles.length != line.styles.length ||
+          oldCls != newCls && (!oldCls || !newCls || oldCls.bgClass != newCls.bgClass || oldCls.textClass != newCls.textClass);
         for (var i = 0; !ischange && i < oldStyles.length; ++i) ischange = oldStyles[i] != line.styles[i];
-        if (ischange) regLineChange(cm, doc.frontier, "text");
+        if (ischange) changedLines.push(doc.frontier);
         line.stateAfter = copyState(doc.mode, state);
       } else {
         processLine(cm, line.text, state);
         return true;
       }
     });
+    if (changedLines.length) runInOp(cm, function() {
+      for (var i = 0; i < changedLines.length; i++)
+        regLineChange(cm, changedLines[i], "text");
     });
   }
 
 
   // Given a prepared measurement object, measures the position of an
   // actual character (or fetches it from the cache).
-  function measureCharPrepared(cm, prepared, ch, bias) {
+  function measureCharPrepared(cm, prepared, ch, bias, varHeight) {
     if (prepared.before) ch = -1;
     var key = ch + (bias || ""), found;
     if (prepared.cache.hasOwnProperty(key)) {
       found = measureCharInner(cm, prepared, ch, bias);
       if (!found.bogus) prepared.cache[key] = found;
     }
-    return {left: found.left, right: found.right, top: found.top, bottom: found.bottom};
+    return {left: found.left, right: found.right,
+            top: varHeight ? found.rtop : found.top,
+            bottom: varHeight ? found.rbottom : found.bottom};
   }
 
   var nullRect = {left: 0, right: 0, top: 0, bottom: 0};
     if (node.nodeType == 3) { // If it is a text node, use a range to retrieve the coordinates.
       while (start && isExtendingChar(prepared.line.text.charAt(mStart + start))) --start;
       while (mStart + end < mEnd && isExtendingChar(prepared.line.text.charAt(mStart + end))) ++end;
-      if (ie_upto8 && start == 0 && end == mEnd - mStart) {
+      if (ie && ie_version < 9 && start == 0 && end == mEnd - mStart) {
         rect = node.parentNode.getBoundingClientRect();
       } else if (ie && cm.options.lineWrapping) {
         var rects = range(node, start, end).getClientRects();
         else
           rect = nullRect;
       } else {
-        rect = range(node, start, end).getBoundingClientRect();
+        rect = range(node, start, end).getBoundingClientRect() || nullRect;
       }
     } else { // If it is a widget, simply get the box for the whole widget.
       if (start > 0) collapse = bias = "right";
       else
         rect = node.getBoundingClientRect();
     }
-    if (ie_upto8 && !start && (!rect || !rect.left && !rect.right)) {
+    if (ie && ie_version < 9 && !start && (!rect || !rect.left && !rect.right)) {
       var rSpan = node.parentNode.getClientRects()[0];
       if (rSpan)
         rect = {left: rSpan.left, right: rSpan.left + charWidth(cm.display), top: rSpan.top, bottom: rSpan.bottom};
         rect = nullRect;
     }
 
-    var top, bot = (rect.bottom + rect.top) / 2 - prepared.rect.top;
+    if (ie && ie_version < 11) rect = maybeUpdateRectForZooming(cm.display.measure, rect);
+
+    var rtop = rect.top - prepared.rect.top, rbot = rect.bottom - prepared.rect.top;
+    var mid = (rtop + rbot) / 2;
     var heights = prepared.view.measure.heights;
     for (var i = 0; i < heights.length - 1; i++)
-      if (bot < heights[i]) break;
-    top = i ? heights[i - 1] : 0; bot = heights[i];
+      if (mid < heights[i]) break;
+    var top = i ? heights[i - 1] : 0, bot = heights[i];
     var result = {left: (collapse == "right" ? rect.right : rect.left) - prepared.rect.left,
                   right: (collapse == "left" ? rect.left : rect.right) - prepared.rect.left,
                   top: top, bottom: bot};
     if (!rect.left && !rect.right) result.bogus = true;
+    if (!cm.options.singleCursorHeightPerLine) { result.rtop = rtop; result.rbottom = rbot; }
+
     return result;
   }
 
+  // Work around problem with bounding client rects on ranges being
+  // returned incorrectly when zoomed on IE10 and below.
+  function maybeUpdateRectForZooming(measure, rect) {
+    if (!window.screen || screen.logicalXDPI == null ||
+        screen.logicalXDPI == screen.deviceXDPI || !hasBadZoomedRects(measure))
+      return rect;
+    var scaleX = screen.logicalXDPI / screen.deviceXDPI;
+    var scaleY = screen.logicalYDPI / screen.deviceYDPI;
+    return {left: rect.left * scaleX, right: rect.right * scaleX,
+            top: rect.top * scaleY, bottom: rect.bottom * scaleY};
+  }
+
   function clearLineMeasurementCacheFor(lineView) {
     if (lineView.measure) {
       lineView.measure.cache = {};
   // Returns a box for a given cursor position, which may have an
   // 'other' property containing the position of the secondary cursor
   // on a bidi boundary.
-  function cursorCoords(cm, pos, context, lineObj, preparedMeasure) {
+  function cursorCoords(cm, pos, context, lineObj, preparedMeasure, varHeight) {
     lineObj = lineObj || getLine(cm.doc, pos.line);
     if (!preparedMeasure) preparedMeasure = prepareMeasureForLine(cm, lineObj);
     function get(ch, right) {
-      var m = measureCharPrepared(cm, preparedMeasure, ch, right ? "right" : "left");
+      var m = measureCharPrepared(cm, preparedMeasure, ch, right ? "right" : "left", varHeight);
       if (right) m.left = m.right; else m.right = m.left;
       return intoCoordSystem(cm, lineObj, m, context);
     }
   // error-prone). Instead, display updates are batched and then all
   // combined and executed at once.
 
+  var operationGroup = null;
+
   var nextOpId = 0;
   // Start a new operation.
   function startOperation(cm) {
     cm.curOp = {
+      cm: cm,
       viewChanged: false,      // Flag that indicates that lines might need to be redrawn
       startHeight: cm.doc.height, // Used to detect need to update scrollbar
       forceUpdate: false,      // Used to force a redraw
       typing: false,           // Whether this reset should be careful to leave existing text (for compositing)
       changeObjs: null,        // Accumulated changes, for firing change events
       cursorActivityHandlers: null, // Set of handlers to fire cursorActivity on
+      cursorActivityCalled: 0, // Tracks which cursorActivity handlers have been called already
       selectionChanged: false, // Whether the selection needs to be redrawn
       updateMaxLine: false,    // Set when the widest line needs to be determined anew
       scrollLeft: null, scrollTop: null, // Intermediate scroll position, not pushed to DOM yet
       scrollToPos: null,       // Used to scroll to a specific position
       id: ++nextOpId           // Unique ID
     };
-    if (!delayedCallbackDepth++) delayedCallbacks = [];
+    if (operationGroup) {
+      operationGroup.ops.push(cm.curOp);
+    } else {
+      cm.curOp.ownsGroup = operationGroup = {
+        ops: [cm.curOp],
+        delayedCallbacks: []
+      };
+    }
+  }
+
+  function fireCallbacksForOps(group) {
+    // Calls delayed callbacks and cursorActivity handlers until no
+    // new ones appear
+    var callbacks = group.delayedCallbacks, i = 0;
+    do {
+      for (; i < callbacks.length; i++)
+        callbacks[i]();
+      for (var j = 0; j < group.ops.length; j++) {
+        var op = group.ops[j];
+        if (op.cursorActivityHandlers)
+          while (op.cursorActivityCalled < op.cursorActivityHandlers.length)
+            op.cursorActivityHandlers[op.cursorActivityCalled++](op.cm);
+      }
+    } while (i < callbacks.length);
   }
 
   // Finish an operation, updating the display and signalling delayed events
   function endOperation(cm) {
-    var op = cm.curOp, doc = cm.doc, display = cm.display;
-    cm.curOp = null;
-
+    var op = cm.curOp, group = op.ownsGroup;
+    if (!group) return;
+
+    try { fireCallbacksForOps(group); }
+    finally {
+      operationGroup = null;
+      for (var i = 0; i < group.ops.length; i++)
+        group.ops[i].cm.curOp = null;
+      endOperations(group);
+    }
+  }
+
+  // The DOM updates done when an operation finishes are batched so
+  // that the minimum number of relayouts are required.
+  function endOperations(group) {
+    var ops = group.ops;
+    for (var i = 0; i < ops.length; i++) // Read DOM
+      endOperation_R1(ops[i]);
+    for (var i = 0; i < ops.length; i++) // Write DOM (maybe)
+      endOperation_W1(ops[i]);
+    for (var i = 0; i < ops.length; i++) // Read DOM
+      endOperation_R2(ops[i]);
+    for (var i = 0; i < ops.length; i++) // Write DOM (maybe)
+      endOperation_W2(ops[i]);
+    for (var i = 0; i < ops.length; i++) // Read DOM
+      endOperation_finish(ops[i]);
+  }
+
+  function endOperation_R1(op) {
+    var cm = op.cm, display = cm.display;
     if (op.updateMaxLine) findMaxLine(cm);
 
-    // If it looks like an update might be needed, call updateDisplay
-    if (op.viewChanged || op.forceUpdate || op.scrollTop != null ||
-        op.scrollToPos && (op.scrollToPos.from.line < display.viewFrom ||
-                           op.scrollToPos.to.line >= display.viewTo) ||
-        display.maxLineChanged && cm.options.lineWrapping) {
-      var updated = updateDisplay(cm, {top: op.scrollTop, ensure: op.scrollToPos}, op.forceUpdate);
-      if (cm.display.scroller.offsetHeight) cm.doc.scrollTop = cm.display.scroller.scrollTop;
+    op.mustUpdate = op.viewChanged || op.forceUpdate || op.scrollTop != null ||
+      op.scrollToPos && (op.scrollToPos.from.line < display.viewFrom ||
+                         op.scrollToPos.to.line >= display.viewTo) ||
+      display.maxLineChanged && cm.options.lineWrapping;
+    op.update = op.mustUpdate &&
+      new DisplayUpdate(cm, op.mustUpdate && {top: op.scrollTop, ensure: op.scrollToPos}, op.forceUpdate);
+  }
+
+  function endOperation_W1(op) {
+    op.updatedDisplay = op.mustUpdate && updateDisplayIfNeeded(op.cm, op.update);
+  }
+
+  function endOperation_R2(op) {
+    var cm = op.cm, display = cm.display;
+    if (op.updatedDisplay) updateHeightsInViewport(cm);
+
+    op.barMeasure = measureForScrollbars(cm);
+
+    // If the max line changed since it was last measured, measure it,
+    // and ensure the document's width matches it.
+    // updateDisplay_W2 will use these properties to do the actual resizing
+    if (display.maxLineChanged && !cm.options.lineWrapping) {
+      op.adjustWidthTo = measureChar(cm, display.maxLine, display.maxLine.text.length).left + 3;
+      op.maxScrollLeft = Math.max(0, display.sizer.offsetLeft + op.adjustWidthTo +
+                                  scrollerCutOff - display.scroller.clientWidth);
     }
-    // If no update was run, but the selection changed, redraw that.
-    if (!updated && op.selectionChanged) updateSelection(cm);
-    if (!updated && op.startHeight != cm.doc.height) updateScrollbars(cm);
+
+    if (op.updatedDisplay || op.selectionChanged)
+      op.newSelectionNodes = drawSelection(cm);
+  }
+
+  function endOperation_W2(op) {
+    var cm = op.cm;
+
+    if (op.adjustWidthTo != null) {
+      cm.display.sizer.style.minWidth = op.adjustWidthTo + "px";
+      if (op.maxScrollLeft < cm.doc.scrollLeft)
+        setScrollLeft(cm, Math.min(cm.display.scroller.scrollLeft, op.maxScrollLeft), true);
+      cm.display.maxLineChanged = false;
+    }
+
+    if (op.newSelectionNodes)
+      showSelection(cm, op.newSelectionNodes);
+    if (op.updatedDisplay)
+      setDocumentHeight(cm, op.barMeasure);
+    if (op.updatedDisplay || op.startHeight != cm.doc.height)
+      updateScrollbars(cm, op.barMeasure);
+
+    if (op.selectionChanged) restartBlink(cm);
+
+    if (cm.state.focused && op.updateInput)
+      resetInput(cm, op.typing);
+  }
+
+  function endOperation_finish(op) {
+    var cm = op.cm, display = cm.display, doc = cm.doc;
+
+    if (op.adjustWidthTo != null && Math.abs(op.barMeasure.scrollWidth - cm.display.scroller.scrollWidth) > 1)
+      updateScrollbars(cm);
+
+    if (op.updatedDisplay) postUpdateDisplay(cm, op.update);
+
+    // Abort mouse wheel delta measurement, when scrolling explicitly
+    if (display.wheelStartX != null && (op.scrollTop != null || op.scrollLeft != null || op.scrollToPos))
+      display.wheelStartX = display.wheelStartY = null;
 
     // Propagate the scroll position to the actual DOM scroller
     if (op.scrollTop != null && display.scroller.scrollTop != op.scrollTop) {
     }
     // If we need to scroll a specific position into view, do so.
     if (op.scrollToPos) {
-      var coords = scrollPosIntoView(cm, clipPos(cm.doc, op.scrollToPos.from),
-                                     clipPos(cm.doc, op.scrollToPos.to), op.scrollToPos.margin);
+      var coords = scrollPosIntoView(cm, clipPos(doc, op.scrollToPos.from),
+                                     clipPos(doc, op.scrollToPos.to), op.scrollToPos.margin);
       if (op.scrollToPos.isCursor && cm.state.focused) maybeScrollWindow(cm, coords);
     }
 
-    if (op.selectionChanged) restartBlink(cm);
-
-    if (cm.state.focused && op.updateInput)
-      resetInput(cm, op.typing);
-
     // Fire events for markers that are hidden/unidden by editing or
     // undoing
     var hidden = op.maybeHiddenMarkers, unhidden = op.maybeUnhiddenMarkers;
     if (unhidden) for (var i = 0; i < unhidden.length; ++i)
       if (unhidden[i].lines.length) signal(unhidden[i], "unhide");
 
-    var delayed;
-    if (!--delayedCallbackDepth) {
-      delayed = delayedCallbacks;
-      delayedCallbacks = null;
+    if (display.wrapper.offsetHeight)
+      doc.scrollTop = cm.display.scroller.scrollTop;
+
+    // Apply workaround for two webkit bugs
+    if (op.updatedDisplay && webkit) {
+      if (cm.options.lineWrapping)
+        checkForWebkitWidthBug(cm, op.barMeasure); // (Issue #2420)
+      if (op.barMeasure.scrollWidth > op.barMeasure.clientWidth &&
+          op.barMeasure.scrollWidth < op.barMeasure.clientWidth + 1 &&
+          !hScrollbarTakesSpace(cm))
+        updateScrollbars(cm); // (Issue #2562)
     }
+
     // Fire change events, and delayed event handlers
     if (op.changeObjs)
       signal(cm, "changes", cm, op.changeObjs);
-    if (delayed) for (var i = 0; i < delayed.length; ++i) delayed[i]();
-    if (op.cursorActivityHandlers)
-      for (var i = 0; i < op.cursorActivityHandlers.length; i++)
-        op.cursorActivityHandlers[i](cm);
   }
 
   // Run the given function in an operation
 
   function viewCuttingPoint(cm, oldN, newN, dir) {
     var index = findViewIndex(cm, oldN), diff, view = cm.display.view;
-    if (!sawCollapsedSpans) return {index: index, lineN: newN};
+    if (!sawCollapsedSpans || newN == cm.doc.first + cm.doc.size)
+      return {index: index, lineN: newN};
     for (var i = 0, n = cm.display.viewFrom; i < index; i++)
       n += view[i].size;
     if (n != oldN) {
     cm.display.poll.set(20, p);
   }
 
+  // This will be set to an array of strings when copying, so that,
+  // when pasting, we know what kind of selections the copied text
+  // was made out of.
+  var lastCopied = null;
+
   // Read input from the textarea, and update the document to match.
   // When something is selected, it is present in the textarea, and
   // selected (unless it is huge, in which case a placeholder is
     var text = input.value;
     // If nothing changed, bail.
     if (text == prevInput && !cm.somethingSelected()) return false;
-    // Work around nonsensical selection resetting in IE9/10
-    if (ie && !ie_upto8 && cm.display.inputHasSelection === text) {
+    // Work around nonsensical selection resetting in IE9/10, and
+    // inexplicable appearance of private area unicode characters on
+    // some key combos in Mac (#2689).
+    if (ie && ie_version >= 9 && cm.display.inputHasSelection === text ||
+        mac && /[\uf700-\uf7ff]/.test(text)) {
       resetInput(cm);
       return false;
     }
     if (withOp) startOperation(cm);
     cm.display.shift = false;
 
+    if (text.charCodeAt(0) == 0x200b && doc.sel == cm.display.selForContextMenu && !prevInput)
+      prevInput = "\u200b";
     // Find the part of the input that is actually new
     var same = 0, l = Math.min(prevInput.length, text.length);
     while (same < l && prevInput.charCodeAt(same) == text.charCodeAt(same)) ++same;
     var inserted = text.slice(same), textLines = splitLines(inserted);
 
     // When pasing N lines into N selections, insert one line per selection
-    var multiPaste = cm.state.pasteIncoming && textLines.length > 1 && doc.sel.ranges.length == textLines.length;
+    var multiPaste = null;
+    if (cm.state.pasteIncoming && doc.sel.ranges.length > 1) {
+      if (lastCopied && lastCopied.join("\n") == inserted)
+        multiPaste = doc.sel.ranges.length % lastCopied.length == 0 && map(lastCopied, splitLines);
+      else if (textLines.length == doc.sel.ranges.length)
+        multiPaste = map(textLines, function(l) { return [l]; });
+    }
 
     // Normal behavior is to insert the new text into every selection
     for (var i = doc.sel.ranges.length - 1; i >= 0; i--) {
       else if (cm.state.overwrite && range.empty() && !cm.state.pasteIncoming)
         to = Pos(to.line, Math.min(getLine(doc, to.line).text.length, to.ch + lst(textLines).length));
       var updateInput = cm.curOp.updateInput;
-      var changeEvent = {from: from, to: to, text: multiPaste ? [textLines[i]] : textLines,
+      var changeEvent = {from: from, to: to, text: multiPaste ? multiPaste[i % multiPaste.length] : textLines,
                          origin: cm.state.pasteIncoming ? "paste" : cm.state.cutIncoming ? "cut" : "+input"};
       makeChange(cm.doc, changeEvent);
       signalLater(cm, "inputRead", cm, changeEvent);
       var content = minimal ? "-" : selected || cm.getSelection();
       cm.display.input.value = content;
       if (cm.state.focused) selectInput(cm.display.input);
-      if (ie && !ie_upto8) cm.display.inputHasSelection = content;
+      if (ie && ie_version >= 9) cm.display.inputHasSelection = content;
     } else if (!typing) {
       cm.display.prevInput = cm.display.input.value = "";
-      if (ie && !ie_upto8) cm.display.inputHasSelection = null;
+      if (ie && ie_version >= 9) cm.display.inputHasSelection = null;
     }
     cm.display.inaccurateSelection = minimal;
   }
     var d = cm.display;
     on(d.scroller, "mousedown", operation(cm, onMouseDown));
     // Older IE's will not fire a second mousedown for a double click
-    if (ie_upto10)
+    if (ie && ie_version < 11)
       on(d.scroller, "dblclick", operation(cm, function(e) {
         if (signalDOMEvent(cm, e)) return;
         var pos = posFromMouse(cm, e);
         if (!pos || clickInGutter(cm, e) || eventInWidget(cm.display, e)) return;
         e_preventDefault(e);
-        var word = findWordAt(cm.doc, pos);
+        var word = findWordAt(cm, pos);
         extendSelection(cm.doc, word.anchor, word.head);
       }));
     else
     // Prevent wrapper from ever scrolling
     on(d.wrapper, "scroll", function() { d.wrapper.scrollTop = d.wrapper.scrollLeft = 0; });
 
-    // When the window resizes, we need to refresh active editors.
-    var resizeTimer;
-    function onResize() {
-      if (resizeTimer == null) resizeTimer = setTimeout(function() {
-        resizeTimer = null;
-        // Might be a text scaling operation, clear size caches.
-        d.cachedCharWidth = d.cachedTextHeight = d.cachedPaddingH = knownScrollbarWidth = null;
-        cm.setSize();
-      }, 100);
-    }
-    on(window, "resize", onResize);
-    // The above handler holds on to the editor and its data
-    // structures. Here we poll to unregister it when the editor is no
-    // longer in the document, so that it can be garbage-collected.
-    function unregister() {
-      if (contains(document.body, d.wrapper)) setTimeout(unregister, 5000);
-      else off(window, "resize", onResize);
-    }
-    setTimeout(unregister, 5000);
-
-    on(d.input, "keyup", operation(cm, onKeyUp));
+    on(d.input, "keyup", function(e) { onKeyUp.call(cm, e); });
     on(d.input, "input", function() {
-      if (ie && !ie_upto8 && cm.display.inputHasSelection) cm.display.inputHasSelection = null;
+      if (ie && ie_version >= 9 && cm.display.inputHasSelection) cm.display.inputHasSelection = null;
       fastPoll(cm);
     });
     on(d.input, "keydown", operation(cm, onKeyDown));
       if (webkit && !cm.state.fakedLastChar && !(new Date - cm.state.lastMiddleDown < 200)) {
         var start = d.input.selectionStart, end = d.input.selectionEnd;
         d.input.value += "$";
-        d.input.selectionStart = start;
+        // The selection end needs to be set before the start, otherwise there
+        // can be an intermediate non-empty selection between the two, which
+        // can override the middle-click paste buffer on linux and cause the
+        // wrong thing to get pasted.
         d.input.selectionEnd = end;
+        d.input.selectionStart = start;
         cm.state.fakedLastChar = true;
       }
       cm.state.pasteIncoming = true;
 
     function prepareCopyCut(e) {
       if (cm.somethingSelected()) {
+        lastCopied = cm.getSelections();
         if (d.inaccurateSelection) {
           d.prevInput = "";
           d.inaccurateSelection = false;
-          d.input.value = cm.getSelection();
+          d.input.value = lastCopied.join("\n");
           selectInput(d.input);
         }
       } else {
-        var text = "", ranges = [];
+        var text = [], ranges = [];
         for (var i = 0; i < cm.doc.sel.ranges.length; i++) {
           var line = cm.doc.sel.ranges[i].head.line;
           var lineRange = {anchor: Pos(line, 0), head: Pos(line + 1, 0)};
           ranges.push(lineRange);
-          text += cm.getRange(lineRange.anchor, lineRange.head);
+          text.push(cm.getRange(lineRange.anchor, lineRange.head));
         }
         if (e.type == "cut") {
           cm.setSelections(ranges, null, sel_dontScroll);
         } else {
           d.prevInput = "";
-          d.input.value = text;
+          d.input.value = text.join("\n");
           selectInput(d.input);
         }
+        lastCopied = text;
       }
       if (e.type == "cut") cm.state.cutIncoming = true;
     }
     });
   }
 
+  // Called when the window resizes
+  function onResize(cm) {
+    // Might be a text scaling operation, clear size caches.
+    var d = cm.display;
+    d.cachedCharWidth = d.cachedTextHeight = d.cachedPaddingH = null;
+    cm.setSize();
+  }
+
   // MOUSE EVENTS
 
   // Return true when the given mouse event happened in a widget
       lastClick = {time: now, pos: start};
     }
 
-    var sel = cm.doc.sel, addNew = mac ? e.metaKey : e.ctrlKey;
-    if (cm.options.dragDrop && dragAndDrop && !addNew && !isReadOnly(cm) &&
+    var sel = cm.doc.sel, modifier = mac ? e.metaKey : e.ctrlKey;
+    if (cm.options.dragDrop && dragAndDrop && !isReadOnly(cm) &&
         type == "single" && sel.contains(start) > -1 && sel.somethingSelected())
-      leftButtonStartDrag(cm, e, start);
+      leftButtonStartDrag(cm, e, start, modifier);
     else
-      leftButtonSelect(cm, e, start, type, addNew);
+      leftButtonSelect(cm, e, start, type, modifier);
   }
 
   // Start a text drag. When it ends, see if any dragging actually
   // happen, and treat as a click if it didn't.
-  function leftButtonStartDrag(cm, e, start) {
+  function leftButtonStartDrag(cm, e, start, modifier) {
     var display = cm.display;
     var dragEnd = operation(cm, function(e2) {
       if (webkit) display.scroller.draggable = false;
       off(display.scroller, "drop", dragEnd);
       if (Math.abs(e.clientX - e2.clientX) + Math.abs(e.clientY - e2.clientY) < 10) {
         e_preventDefault(e2);
-        extendSelection(cm.doc, start);
+        if (!modifier)
+          extendSelection(cm.doc, start);
         focusInput(cm);
         // Work around unexplainable focus problem in IE9 (#2127)
-        if (ie_upto10 && !ie_upto8)
+        if (ie && ie_version == 9)
           setTimeout(function() {document.body.focus(); focusInput(cm);}, 20);
       }
     });
       start = posFromMouse(cm, e, true, true);
       ourIndex = -1;
     } else if (type == "double") {
-      var word = findWordAt(doc, start);
+      var word = findWordAt(cm, start);
       if (cm.display.shift || doc.extend)
         ourRange = extendRange(doc, ourRange, word.anchor, word.head);
       else
             ranges.push(new Range(Pos(line, leftPos), Pos(line, findColumn(text, right, tabSize))));
         }
         if (!ranges.length) ranges.push(new Range(start, start));
-        setSelection(doc, normalizeSelection(startSel.ranges.slice(0, ourIndex).concat(ranges), ourIndex), sel_mouse);
+        setSelection(doc, normalizeSelection(startSel.ranges.slice(0, ourIndex).concat(ranges), ourIndex),
+                     {origin: "*mouse", scroll: false});
+        cm.scrollIntoView(pos);
       } else {
         var oldRange = ourRange;
         var anchor = oldRange.anchor, head = pos;
         if (type != "single") {
           if (type == "double")
-            var range = findWordAt(doc, pos);
+            var range = findWordAt(cm, pos);
           else
             var range = new Range(Pos(pos.line, 0), clipPos(doc, Pos(pos.line + 1, 0)));
           if (cmp(range.anchor, anchor) > 0) {
     }
 
     var move = operation(cm, function(e) {
-      if ((ie && !ie_upto9) ?  !e.buttons : !e_button(e)) done(e);
+      if (!e_button(e)) done(e);
       else extend(e);
     });
     var up = operation(cm, done);
       try {
         var text = e.dataTransfer.getData("Text");
         if (text) {
-          var selected = cm.state.draggingText && cm.listSelections();
+          if (cm.state.draggingText && !(mac ? e.metaKey : e.ctrlKey))
+            var selected = cm.listSelections();
           setSelectionNoUndo(cm.doc, simpleSelection(pos, pos));
           if (selected) for (var i = 0; i < selected.length; ++i)
             replaceRange(cm.doc, "", selected[i].anchor, selected[i].head, "drag");
   function setScrollTop(cm, val) {
     if (Math.abs(cm.doc.scrollTop - val) < 2) return;
     cm.doc.scrollTop = val;
-    if (!gecko) updateDisplay(cm, {top: val});
+    if (!gecko) updateDisplaySimple(cm, {top: val});
     if (cm.display.scroller.scrollTop != val) cm.display.scroller.scrollTop = val;
     if (cm.display.scrollbarV.scrollTop != val) cm.display.scrollbarV.scrollTop = val;
-    if (gecko) updateDisplay(cm);
+    if (gecko) updateDisplaySimple(cm);
     startWorker(cm, 100);
   }
   // Sync scroller and scrollbar, ensure the gutter elements are
       var top = cm.doc.scrollTop, bot = top + display.wrapper.clientHeight;
       if (pixels < 0) top = Math.max(0, top + pixels - 50);
       else bot = Math.min(cm.doc.height, bot + pixels + 50);
-      updateDisplay(cm, {top: top, bottom: bot});
+      updateDisplaySimple(cm, {top: top, bottom: bot});
     }
 
     if (wheelSamples < 20) {
     ensureFocus(cm);
     if (signalDOMEvent(cm, e)) return;
     // IE does strange things with escape.
-    if (ie_upto10 && e.keyCode == 27) e.returnValue = false;
+    if (ie && ie_version < 11 && e.keyCode == 27) e.returnValue = false;
     var code = e.keyCode;
     cm.display.shift = code == 16 || e.shiftKey;
     var handled = handleKeyBinding(cm, e);
   }
 
   function onKeyUp(e) {
-    if (signalDOMEvent(this, e)) return;
     if (e.keyCode == 16) this.doc.sel.shift = false;
+    signalDOMEvent(this, e);
   }
 
   function onKeyPress(e) {
     var cm = this;
-    if (signalDOMEvent(cm, e)) return;
+    if (signalDOMEvent(cm, e) || e.ctrlKey && !e.altKey || mac && e.metaKey) return;
     var keyCode = e.keyCode, charCode = e.charCode;
     if (presto && keyCode == lastStoppedKey) {lastStoppedKey = null; e_preventDefault(e); return;}
     if (((presto && (!e.which || e.which < 10)) || khtml) && handleKeyBinding(cm, e)) return;
     var ch = String.fromCharCode(charCode == null ? keyCode : charCode);
     if (handleCharBinding(cm, e, ch)) return;
-    if (ie && !ie_upto8) cm.display.inputHasSelection = null;
+    if (ie && ie_version >= 9) cm.display.inputHasSelection = null;
     fastPoll(cm);
   }
 
       "px; left: " + (e.clientX - 5) + "px; z-index: 1000; background: " +
       (ie ? "rgba(255, 255, 255, .05)" : "transparent") +
       "; outline: none; border-width: 0; outline: none; overflow: hidden; opacity: .05; filter: alpha(opacity=5);";
+    if (webkit) var oldScrollY = window.scrollY; // Work around Chrome issue (#2712)
     focusInput(cm);
+    if (webkit) window.scrollTo(null, oldScrollY);
     resetInput(cm);
     // Adds "Select all" to context menu in FF
     if (!cm.somethingSelected()) display.input.value = display.prevInput = " ";
         var extval = display.input.value = "\u200b" + (selected ? display.input.value : "");
         display.prevInput = selected ? "" : "\u200b";
         display.input.selectionStart = 1; display.input.selectionEnd = extval.length;
+        // Re-set this, in case some other handler touched the
+        // selection in the meantime.
+        display.selForContextMenu = cm.doc.sel;
       }
     }
     function rehide() {
       display.inputDiv.style.position = "relative";
       display.input.style.cssText = oldCSS;
-      if (ie_upto8) display.scrollbarV.scrollTop = display.scroller.scrollTop = scrollPos;
+      if (ie && ie_version < 9) display.scrollbarV.scrollTop = display.scroller.scrollTop = scrollPos;
       slowPoll(cm);
 
       // Try to detect the user choosing select-all
       if (display.input.selectionStart != null) {
-        if (!ie || ie_upto8) prepareSelectAllHack();
+        if (!ie || (ie && ie_version < 9)) prepareSelectAllHack();
         var i = 0, poll = function() {
           if (display.selForContextMenu == cm.doc.sel && display.input.selectionStart == 0)
             operation(cm, commands.selectAll)(cm);
       }
     }
 
-    if (ie && !ie_upto8) prepareSelectAllHack();
+    if (ie && ie_version >= 9) prepareSelectAllHack();
     if (captureRightClick) {
       e_stop(e);
       var mouseup = function() {
 
       antiChanges.push(historyChangeFromChange(doc, change));
 
-      var after = i ? computeSelAfterChange(doc, change, null) : lst(source);
+      var after = i ? computeSelAfterChange(doc, change) : lst(source);
       makeChangeSingleDoc(doc, change, after, mergeOldSpans(doc, change));
-      if (doc.cm) ensureCursorVisible(doc.cm);
+      if (!i && doc.cm) doc.cm.scrollIntoView({from: change.from, to: changeEnd(change)});
       var rebased = [];
 
       // Propagate to the linked documents
   // Sub-views need their line numbers shifted when text is added
   // above or below them in the parent document.
   function shiftDoc(doc, distance) {
+    if (distance == 0) return;
     doc.first += distance;
     doc.sel = new Selection(map(doc.sel.ranges, function(range) {
       return new Range(Pos(range.anchor.line + distance, range.anchor.ch),
                        Pos(range.head.line + distance, range.head.ch));
     }), doc.sel.primIndex);
-    if (doc.cm) regChange(doc.cm, doc.first, doc.first - distance, distance);
+    if (doc.cm) {
+      regChange(doc.cm, doc.first, doc.first - distance, distance);
+      for (var d = doc.cm.display, l = d.viewFrom; l < d.viewTo; l++)
+        regLineChange(doc.cm, l, "gutter");
+    }
   }
 
   // More lower-level change function, handling only a single document
 
     change.removed = getBetween(doc, change.from, change.to);
 
-    if (!selAfter) selAfter = computeSelAfterChange(doc, change, null);
+    if (!selAfter) selAfter = computeSelAfterChange(doc, change);
     if (doc.cm) makeChangeSingleDocInEditor(doc.cm, change, spans);
     else updateDoc(doc, change, spans);
     setSelectionNoUndo(doc, selAfter, sel_dontScroll);
       if (changeHandler) signalLater(cm, "change", cm, obj);
       if (changesHandler) (cm.curOp.changeObjs || (cm.curOp.changeObjs = [])).push(obj);
     }
+    cm.display.selForContextMenu = null;
   }
 
   function replaceRange(doc, code, from, to, origin) {
     if (y1 < 0) y1 = 0;
     var screentop = cm.curOp && cm.curOp.scrollTop != null ? cm.curOp.scrollTop : display.scroller.scrollTop;
     var screen = display.scroller.clientHeight - scrollerCutOff, result = {};
+    if (y2 - y1 > screen) y2 = y1 + screen;
     var docBottom = cm.doc.height + paddingVert(display);
     var atTop = y1 < snapMargin, atBottom = y2 > docBottom - snapMargin;
     if (y1 < screentop) {
     }
 
     var screenleft = cm.curOp && cm.curOp.scrollLeft != null ? cm.curOp.scrollLeft : display.scroller.scrollLeft;
-    var screenw = display.scroller.clientWidth - scrollerCutOff;
-    x1 += display.gutters.offsetWidth; x2 += display.gutters.offsetWidth;
-    var gutterw = display.gutters.offsetWidth;
-    var atLeft = x1 < gutterw + 10;
-    if (x1 < screenleft + gutterw || atLeft) {
-      if (atLeft) x1 = 0;
-      result.scrollLeft = Math.max(0, x1 - 10 - gutterw);
-    } else if (x2 > screenw + screenleft - 3) {
-      result.scrollLeft = x2 + 10 - screenw;
-    }
+    var screenw = display.scroller.clientWidth - scrollerCutOff - display.gutters.offsetWidth;
+    var tooWide = x2 - x1 > screenw;
+    if (tooWide) x2 = y1 + screen;
+    if (x1 < 10)
+      result.scrollLeft = 0;
+    else if (x1 < screenleft)
+      result.scrollLeft = Math.max(0, x1 - (tooWide ? 0 : 10));
+    else if (x2 > screenw + screenleft - 3)
+      result.scrollLeft = x2 + (tooWide ? 0 : 10) - screenw;
+
     return result;
   }
 
     if (how == "smart") {
       // Fall back to "prev" when the mode doesn't have an indentation
       // method.
-      if (!cm.doc.mode.indent) how = "prev";
+      if (!doc.mode.indent) how = "prev";
       else state = getStateBefore(cm, n);
     }
 
       indentation = 0;
       how = "not";
     } else if (how == "smart") {
-      indentation = cm.doc.mode.indent(state, line.text.slice(curSpaceString.length), line.text);
-      if (indentation == Pass) {
+      indentation = doc.mode.indent(state, line.text.slice(curSpaceString.length), line.text);
+      if (indentation == Pass || indentation > 150) {
         if (!aggressive) return;
         how = "prev";
       }
     if (pos < indentation) indentString += spaceStr(indentation - pos);
 
     if (indentString != curSpaceString) {
-      replaceRange(cm.doc, indentString, Pos(n, 0), Pos(n, curSpaceString.length), "+input");
+      replaceRange(doc, indentString, Pos(n, 0), Pos(n, curSpaceString.length), "+input");
     } else {
       // Ensure that, if the cursor was in the whitespace at the start
       // of the line, it is moved to the end of that space.
   // Utility for applying a change to a line by handle or number,
   // returning the number and optionally registering the line as
   // changed.
-  function changeLine(cm, handle, changeType, op) {
-    var no = handle, line = handle, doc = cm.doc;
+  function changeLine(doc, handle, changeType, op) {
+    var no = handle, line = handle;
     if (typeof handle == "number") line = getLine(doc, clipLine(doc, handle));
     else no = lineNo(handle);
     if (no == null) return null;
-    if (op(line, no)) regLineChange(cm, no, changeType);
+    if (op(line, no) && doc.cm) regLineChange(doc.cm, no, changeType);
     return line;
   }
 
     else if (unit == "column") moveOnce(true);
     else if (unit == "word" || unit == "group") {
       var sawType = null, group = unit == "group";
+      var helper = doc.cm && doc.cm.getHelper(pos, "wordChars");
       for (var first = true;; first = false) {
         if (dir < 0 && !moveOnce(!first)) break;
         var cur = lineObj.text.charAt(ch) || "\n";
-        var type = isWordChar(cur) ? "w"
+        var type = isWordChar(cur, helper) ? "w"
           : group && cur == "\n" ? "n"
           : !group || /\s/.test(cur) ? null
           : "p";
   }
 
   // Find the word at the given position (as returned by coordsChar).
-  function findWordAt(doc, pos) {
-    var line = getLine(doc, pos.line).text;
+  function findWordAt(cm, pos) {
+    var doc = cm.doc, line = getLine(doc, pos.line).text;
     var start = pos.ch, end = pos.ch;
     if (line) {
+      var helper = cm.getHelper(pos, "wordChars");
       if ((pos.xRel < 0 || end == line.length) && start) --start; else ++end;
       var startChar = line.charAt(start);
-      var check = isWordChar(startChar) ? isWordChar
+      var check = isWordChar(startChar, helper)
+        ? function(ch) { return isWordChar(ch, helper); }
         : /\s/.test(startChar) ? function(ch) {return /\s/.test(ch);}
         : function(ch) {return !/\s/.test(ch) && !isWordChar(ch);};
       while (start > 0 && check(line.charAt(start - 1))) --start;
       for (var i = 0; i < ranges.length; i++) {
         var range = ranges[i];
         if (!range.empty()) {
-          var start = Math.max(end, range.from().line);
-          var to = range.to();
+          var from = range.from(), to = range.to();
+          var start = Math.max(end, from.line);
           end = Math.min(this.lastLine(), to.line - (to.ch ? 0 : 1)) + 1;
           for (var j = start; j < end; ++j)
             indentLine(this, j, how);
+          var newRanges = this.doc.sel.ranges;
+          if (from.ch == 0 && ranges.length == newRanges.length && newRanges[i].from().ch > 0)
+            replaceOneSelection(this.doc, i, new Range(from, newRanges[i].to()));
         } else if (range.head.line > end) {
           indentLine(this, range.head.line, how, true);
           end = range.head.line;
     defaultCharWidth: function() { return charWidth(this.display); },
 
     setGutterMarker: methodOp(function(line, gutterID, value) {
-      return changeLine(this, line, "gutter", function(line) {
+      return changeLine(this.doc, line, "gutter", function(line) {
         var markers = line.gutterMarkers || (line.gutterMarkers = {});
         markers[gutterID] = value;
         if (!value && isEmpty(markers)) line.gutterMarkers = null;
       });
     }),
 
-    addLineClass: methodOp(function(handle, where, cls) {
-      return changeLine(this, handle, "class", function(line) {
-        var prop = where == "text" ? "textClass" : where == "background" ? "bgClass" : "wrapClass";
-        if (!line[prop]) line[prop] = cls;
-        else if (new RegExp("(?:^|\\s)" + cls + "(?:$|\\s)").test(line[prop])) return false;
-        else line[prop] += " " + cls;
-        return true;
-      });
-    }),
-
-    removeLineClass: methodOp(function(handle, where, cls) {
-      return changeLine(this, handle, "class", function(line) {
-        var prop = where == "text" ? "textClass" : where == "background" ? "bgClass" : "wrapClass";
-        var cur = line[prop];
-        if (!cur) return false;
-        else if (cls == null) line[prop] = null;
-        else {
-          var found = cur.match(new RegExp("(?:^|\\s+)" + cls + "(?:$|\\s+)"));
-          if (!found) return false;
-          var end = found.index + found[0].length;
-          line[prop] = cur.slice(0, found.index) + (!found.index || end == cur.length ? "" : " ") + cur.slice(end) || null;
-        }
-        return true;
-      });
-    }),
-
     addLineWidget: methodOp(function(handle, node, options) {
       return addLineWidget(this, handle, node, options);
     }),
 
     triggerOnKeyDown: methodOp(onKeyDown),
     triggerOnKeyPress: methodOp(onKeyPress),
-    triggerOnKeyUp: methodOp(onKeyUp),
+    triggerOnKeyUp: onKeyUp,
 
     execCommand: function(cmd) {
       if (commands.hasOwnProperty(cmd))
     }),
 
     setSize: methodOp(function(width, height) {
+      var cm = this;
       function interpret(val) {
         return typeof val == "number" || /^\d+$/.test(String(val)) ? val + "px" : val;
       }
-      if (width != null) this.display.wrapper.style.width = interpret(width);
-      if (height != null) this.display.wrapper.style.height = interpret(height);
-      if (this.options.lineWrapping) clearLineMeasurementCache(this);
-      this.curOp.forceUpdate = true;
-      signal(this, "refresh", this);
+      if (width != null) cm.display.wrapper.style.width = interpret(width);
+      if (height != null) cm.display.wrapper.style.height = interpret(height);
+      if (cm.options.lineWrapping) clearLineMeasurementCache(this);
+      var lineNo = cm.display.viewFrom;
+      cm.doc.iter(lineNo, cm.display.viewTo, function(line) {
+        if (line.widgets) for (var i = 0; i < line.widgets.length; i++)
+          if (line.widgets[i].noHScroll) { regLineChange(cm, lineNo, "widget"); break; }
+        ++lineNo;
+      });
+      cm.curOp.forceUpdate = true;
+      signal(cm, "refresh", this);
     }),
 
     operation: function(f){return runInOp(this, f);},
     clearCaches(cm);
     regChange(cm);
   }, true);
-  option("specialChars", /[\t\u0000-\u0019\u00ad\u200b\u2028\u2029\ufeff]/g, function(cm, val) {
+  option("specialChars", /[\t\u0000-\u0019\u00ad\u200b-\u200f\u2028\u2029\ufeff]/g, function(cm, val) {
     cm.options.specialChars = new RegExp(val.source + (val.test("\t") ? "" : "|\t"), "g");
     cm.refresh();
   }, true);
 
   option("cursorBlinkRate", 530);
   option("cursorScrollMargin", 0);
-  option("cursorHeight", 1);
+  option("cursorHeight", 1, updateSelection, true);
+  option("singleCursorHeightPerLine", true, updateSelection, true);
   option("workTime", 100);
   option("workDelay", 100);
   option("flattenSpans", true, resetModeState, true);
         return {from: Pos(range.from().line, 0), to: range.from()};
       });
     },
+    delWrappedLineLeft: function(cm) {
+      deleteNearSelection(cm, function(range) {
+        var top = cm.charCoords(range.head, "div").top + 5;
+        var leftPos = cm.coordsChar({left: 0, top: top}, "div");
+        return {from: leftPos, to: range.from()};
+      });
+    },
+    delWrappedLineRight: function(cm) {
+      deleteNearSelection(cm, function(range) {
+        var top = cm.charCoords(range.head, "div").top + 5;
+        var rightPos = cm.coordsChar({left: cm.display.lineDiv.offsetWidth + 100, top: top}, "div");
+        return {from: range.from(), to: rightPos };
+      });
+    },
     undo: function(cm) {cm.undo();},
     redo: function(cm) {cm.redo();},
     undoSelection: function(cm) {cm.undoSelection();},
     goDocStart: function(cm) {cm.extendSelection(Pos(cm.firstLine(), 0));},
     goDocEnd: function(cm) {cm.extendSelection(Pos(cm.lastLine()));},
     goLineStart: function(cm) {
-      cm.extendSelectionsBy(function(range) { return lineStart(cm, range.head.line); }, sel_move);
+      cm.extendSelectionsBy(function(range) { return lineStart(cm, range.head.line); },
+                            {origin: "+move", bias: 1});
     },
     goLineStartSmart: function(cm) {
       cm.extendSelectionsBy(function(range) {
-        var start = lineStart(cm, range.head.line);
-        var line = cm.getLineHandle(start.line);
-        var order = getOrder(line);
-        if (!order || order[0].level == 0) {
-          var firstNonWS = Math.max(0, line.text.search(/\S/));
-          var inWS = range.head.line == start.line && range.head.ch <= firstNonWS && range.head.ch;
-          return Pos(start.line, inWS ? 0 : firstNonWS);
-        }
-        return start;
-      }, sel_move);
+        return lineStartSmart(cm, range.head);
+      }, {origin: "+move", bias: 1});
     },
     goLineEnd: function(cm) {
-      cm.extendSelectionsBy(function(range) { return lineEnd(cm, range.head.line); }, sel_move);
+      cm.extendSelectionsBy(function(range) { return lineEnd(cm, range.head.line); },
+                            {origin: "+move", bias: -1});
     },
     goLineRight: function(cm) {
       cm.extendSelectionsBy(function(range) {
         return cm.coordsChar({left: 0, top: top}, "div");
       }, sel_move);
     },
+    goLineLeftSmart: function(cm) {
+      cm.extendSelectionsBy(function(range) {
+        var top = cm.charCoords(range.head, "div").top + 5;
+        var pos = cm.coordsChar({left: 0, top: top}, "div");
+        if (pos.ch < cm.getLine(pos.line).search(/\S/)) return lineStartSmart(cm, range.head);
+        return pos;
+      }, sel_move);
+    },
     goLineUp: function(cm) {cm.moveV(-1, "line");},
     goLineDown: function(cm) {cm.moveV(1, "line");},
     goPageUp: function(cm) {cm.moveV(-1, "page");},
     },
     transposeChars: function(cm) {
       runInOp(cm, function() {
-        var ranges = cm.listSelections();
+        var ranges = cm.listSelections(), newSel = [];
         for (var i = 0; i < ranges.length; i++) {
           var cur = ranges[i].head, line = getLine(cm.doc, cur.line).text;
-          if (cur.ch > 0 && cur.ch < line.length - 1)
-            cm.replaceRange(line.charAt(cur.ch) + line.charAt(cur.ch - 1),
-                            Pos(cur.line, cur.ch - 1), Pos(cur.line, cur.ch + 1));
+          if (line) {
+            if (cur.ch == line.length) cur = new Pos(cur.line, cur.ch - 1);
+            if (cur.ch > 0) {
+              cur = new Pos(cur.line, cur.ch + 1);
+              cm.replaceRange(line.charAt(cur.ch - 1) + line.charAt(cur.ch - 2),
+                              Pos(cur.line, cur.ch - 2), cur, "+transpose");
+            } else if (cur.line > cm.doc.first) {
+              var prev = getLine(cm.doc, cur.line - 1).text;
+              if (prev)
+                cm.replaceRange(line.charAt(0) + "\n" + prev.charAt(prev.length - 1),
+                                Pos(cur.line - 1, prev.length - 1), Pos(cur.line, 1), "+transpose");
+            }
+          }
+          newSel.push(new Range(cur, cur));
         }
+        cm.setSelections(newSel);
       });
     },
     newlineAndIndent: function(cm) {
   };
   keyMap.macDefault = {
     "Cmd-A": "selectAll", "Cmd-D": "deleteLine", "Cmd-Z": "undo", "Shift-Cmd-Z": "redo", "Cmd-Y": "redo",
-    "Cmd-Up": "goDocStart", "Cmd-End": "goDocEnd", "Cmd-Down": "goDocEnd", "Alt-Left": "goGroupLeft",
-    "Alt-Right": "goGroupRight", "Cmd-Left": "goLineStart", "Cmd-Right": "goLineEnd", "Alt-Backspace": "delGroupBefore",
+    "Cmd-Home": "goDocStart", "Cmd-Up": "goDocStart", "Cmd-End": "goDocEnd", "Cmd-Down": "goDocEnd", "Alt-Left": "goGroupLeft",
+    "Alt-Right": "goGroupRight", "Cmd-Left": "goLineLeft", "Cmd-Right": "goLineRight", "Alt-Backspace": "delGroupBefore",
     "Ctrl-Alt-Backspace": "delGroupAfter", "Alt-Delete": "delGroupAfter", "Cmd-S": "save", "Cmd-F": "find",
     "Cmd-G": "findNext", "Shift-Cmd-G": "findPrev", "Cmd-Alt-F": "replace", "Shift-Cmd-Alt-F": "replaceAll",
-    "Cmd-[": "indentLess", "Cmd-]": "indentMore", "Cmd-Backspace": "delLineLeft",
+    "Cmd-[": "indentLess", "Cmd-]": "indentMore", "Cmd-Backspace": "delWrappedLineLeft", "Cmd-Delete": "delWrappedLineRight",
     "Cmd-U": "undoSelection", "Shift-Cmd-U": "redoSelection",
     fallthrough: ["basic", "emacsy"]
   };
       var fromCmp = cmp(found.from, from) || extraLeft(sp.marker) - extraLeft(marker);
       var toCmp = cmp(found.to, to) || extraRight(sp.marker) - extraRight(marker);
       if (fromCmp >= 0 && toCmp <= 0 || fromCmp <= 0 && toCmp >= 0) continue;
-      if (fromCmp <= 0 && (cmp(found.to, from) || extraRight(sp.marker) - extraLeft(marker)) > 0 ||
-          fromCmp >= 0 && (cmp(found.from, to) || extraLeft(sp.marker) - extraRight(marker)) < 0)
+      if (fromCmp <= 0 && (cmp(found.to, from) > 0 || (sp.marker.inclusiveRight && marker.inclusiveLeft)) ||
+          fromCmp >= 0 && (cmp(found.from, to) < 0 || (sp.marker.inclusiveLeft && marker.inclusiveRight)))
         return true;
     }
   }
 
   function widgetHeight(widget) {
     if (widget.height != null) return widget.height;
-    if (!contains(document.body, widget.node))
-      removeChildrenAndAdd(widget.cm.display.measure, elt("div", [widget.node], null, "position: relative"));
+    if (!contains(document.body, widget.node)) {
+      var parentStyle = "position: relative;";
+      if (widget.coverGutter)
+        parentStyle += "margin-left: -" + widget.cm.getGutterElement().offsetWidth + "px;";
+      removeChildrenAndAdd(widget.cm.display.measure, elt("div", [widget.node], null, parentStyle));
+    }
     return widget.height = widget.node.offsetHeight;
   }
 
   function addLineWidget(cm, handle, node, options) {
     var widget = new LineWidget(cm, node, options);
     if (widget.noHScroll) cm.display.alignWidgets = true;
-    changeLine(cm, handle, "widget", function(line) {
+    changeLine(cm.doc, handle, "widget", function(line) {
       var widgets = line.widgets || (line.widgets = []);
       if (widget.insertAt == null) widgets.push(widget);
       else widgets.splice(Math.min(widgets.length - 1, Math.max(0, widget.insertAt)), 0, widget);
   }
 
   function readToken(mode, stream, state) {
-    var style = mode.token(stream, state);
-    if (stream.pos <= stream.start)
-      throw new Error("Mode " + mode.name + " failed to advance stream.");
-    return style;
+    for (var i = 0; i < 10; i++) {
+      var style = mode.token(stream, state);
+      if (stream.pos > stream.start) return style;
+    }
+    throw new Error("Mode " + mode.name + " failed to advance stream.");
   }
 
   // Run the given mode's parser over a line, calling f for each token.
     }
 
     signal(cm, "renderLine", cm, lineView.line, builder.pre);
+    if (builder.pre.className)
+      builder.textClass = joinClasses(builder.pre.className, builder.textClass || "");
     return builder;
   }
 
       builder.col += text.length;
       var content = document.createTextNode(text);
       builder.map.push(builder.pos, builder.pos + text.length, content);
-      if (ie_upto8) mustWrap = true;
+      if (ie && ie_version < 9) mustWrap = true;
       builder.pos += text.length;
     } else {
       var content = document.createDocumentFragment(), pos = 0;
         var skipped = m ? m.index - pos : text.length - pos;
         if (skipped) {
           var txt = document.createTextNode(text.slice(pos, pos + skipped));
-          if (ie_upto8) content.appendChild(elt("span", [txt]));
+          if (ie && ie_version < 9) content.appendChild(elt("span", [txt]));
           else content.appendChild(txt);
           builder.map.push(builder.pos, builder.pos + skipped, txt);
           builder.col += skipped;
           builder.col += tabWidth;
         } else {
           var txt = builder.cm.options.specialCharPlaceholder(m[0]);
-          if (ie_upto8) content.appendChild(elt("span", [txt]));
+          if (ie && ie_version < 9) content.appendChild(elt("span", [txt]));
           else content.appendChild(txt);
           builder.col += 1;
         }
     },
     changeGeneration: function(forceSplit) {
       if (forceSplit)
-        this.history.lastOp = this.history.lastOrigin = null;
+        this.history.lastOp = this.history.lastSelOp = this.history.lastOrigin = null;
       return this.history.generation;
     },
     isClean: function (gen) {
       hist.undone = copyHistoryArray(histData.undone.slice(0), null, true);
     },
 
+    addLineClass: docMethodOp(function(handle, where, cls) {
+      return changeLine(this, handle, "class", function(line) {
+        var prop = where == "text" ? "textClass" : where == "background" ? "bgClass" : "wrapClass";
+        if (!line[prop]) line[prop] = cls;
+        else if (new RegExp("(?:^|\\s)" + cls + "(?:$|\\s)").test(line[prop])) return false;
+        else line[prop] += " " + cls;
+        return true;
+      });
+    }),
+    removeLineClass: docMethodOp(function(handle, where, cls) {
+      return changeLine(this, handle, "class", function(line) {
+        var prop = where == "text" ? "textClass" : where == "background" ? "bgClass" : "wrapClass";
+        var cur = line[prop];
+        if (!cur) return false;
+        else if (cls == null) line[prop] = null;
+        else {
+          var found = cur.match(new RegExp("(?:^|\\s+)" + cls + "(?:$|\\s+)"));
+          if (!found) return false;
+          var end = found.index + found[0].length;
+          line[prop] = cur.slice(0, found.index) + (!found.index || end == cur.length ? "" : " ") + cur.slice(end) || null;
+        }
+        return true;
+      });
+    }),
+
     markText: function(from, to, options) {
       return markText(this, clipPos(this, from), clipPos(this, to), options, "range");
     },
     // Used to track when changes can be merged into a single undo
     // event
     this.lastModTime = this.lastSelTime = 0;
-    this.lastOp = null;
+    this.lastOp = this.lastSelOp = null;
     this.lastOrigin = this.lastSelOrigin = null;
     // Used by the isClean() method
     this.generation = this.maxGeneration = startGen || 1;
     hist.done.push(selAfter);
     hist.generation = ++hist.maxGeneration;
     hist.lastModTime = hist.lastSelTime = time;
-    hist.lastOp = opId;
+    hist.lastOp = hist.lastSelOp = opId;
     hist.lastOrigin = hist.lastSelOrigin = change.origin;
 
     if (!last) signal(doc, "historyAdded");
     // the current, or the origins don't allow matching. Origins
     // starting with * are always merged, those starting with + are
     // merged when similar and close together in time.
-    if (opId == hist.lastOp ||
+    if (opId == hist.lastSelOp ||
         (origin && hist.lastSelOrigin == origin &&
          (hist.lastModTime == hist.lastSelTime && hist.lastOrigin == origin ||
           selectionEventCanBeMerged(doc, origin, lst(hist.done), sel))))
 
     hist.lastSelTime = +new Date;
     hist.lastSelOrigin = origin;
-    hist.lastOp = opId;
+    hist.lastSelOp = opId;
     if (options && options.clearRedo !== false)
       clearSelectionEvents(hist.undone);
   }
     for (var i = 0; i < arr.length; ++i) arr[i].apply(null, args);
   };
 
+  var orphanDelayedCallbacks = null;
+
   // Often, we want to signal events at a point where we are in the
   // middle of some work, but don't want the handler to start calling
   // other methods on the editor, which might be in an inconsistent
   // signalLater looks whether there are any handlers, and schedules
   // them to be executed when the last operation ends, or, if no
   // operation is active, when a timeout fires.
-  var delayedCallbacks, delayedCallbackDepth = 0;
   function signalLater(emitter, type /*, values...*/) {
     var arr = emitter._handlers && emitter._handlers[type];
     if (!arr) return;
-    var args = Array.prototype.slice.call(arguments, 2);
-    if (!delayedCallbacks) {
-      ++delayedCallbackDepth;
-      delayedCallbacks = [];
-      setTimeout(fireDelayed, 0);
+    var args = Array.prototype.slice.call(arguments, 2), list;
+    if (operationGroup) {
+      list = operationGroup.delayedCallbacks;
+    } else if (orphanDelayedCallbacks) {
+      list = orphanDelayedCallbacks;
+    } else {
+      list = orphanDelayedCallbacks = [];
+      setTimeout(fireOrphanDelayed, 0);
     }
     function bnd(f) {return function(){f.apply(null, args);};};
     for (var i = 0; i < arr.length; ++i)
-      delayedCallbacks.push(bnd(arr[i]));
+      list.push(bnd(arr[i]));
   }
 
-  function fireDelayed() {
-    --delayedCallbackDepth;
-    var delayed = delayedCallbacks;
-    delayedCallbacks = null;
+  function fireOrphanDelayed() {
+    var delayed = orphanDelayedCallbacks;
+    orphanDelayedCallbacks = null;
     for (var i = 0; i < delayed.length; ++i) delayed[i]();
   }
 
   }
 
   var nonASCIISingleCaseWordChar = /[\u00df\u3040-\u309f\u30a0-\u30ff\u3400-\u4db5\u4e00-\u9fcc\uac00-\ud7af]/;
-  var isWordChar = CodeMirror.isWordChar = function(ch) {
+  var isWordCharBasic = CodeMirror.isWordChar = function(ch) {
     return /\w/.test(ch) || ch > "\x80" &&
       (ch.toUpperCase() != ch.toLowerCase() || nonASCIISingleCaseWordChar.test(ch));
   };
+  function isWordChar(ch, helper) {
+    if (!helper) return isWordCharBasic(ch);
+    if (helper.source.indexOf("\\w") > -1 && isWordCharBasic(ch)) return true;
+    return helper.test(ch);
+  }
 
   function isEmpty(obj) {
     for (var n in obj) if (obj.hasOwnProperty(n) && obj[n]) return false;
   function activeElt() { return document.activeElement; }
   // Older versions of IE throws unspecified error when touching
   // document.activeElement in some cases (during loading, in iframe)
-  if (ie_upto10) activeElt = function() {
+  if (ie && ie_version < 11) activeElt = function() {
     try { return document.activeElement; }
     catch(e) { return document.body; }
   };
     return b;
   }
 
+  // WINDOW-WIDE EVENTS
+
+  // These must be handled carefully, because naively registering a
+  // handler for each editor will cause the editors to never be
+  // garbage collected.
+
+  function forEachCodeMirror(f) {
+    if (!document.body.getElementsByClassName) return;
+    var byClass = document.body.getElementsByClassName("CodeMirror");
+    for (var i = 0; i < byClass.length; i++) {
+      var cm = byClass[i].CodeMirror;
+      if (cm) f(cm);
+    }
+  }
+
+  var globalsRegistered = false;
+  function ensureGlobalHandlers() {
+    if (globalsRegistered) return;
+    registerGlobalHandlers();
+    globalsRegistered = true;
+  }
+  function registerGlobalHandlers() {
+    // When the window resizes, we need to refresh active editors.
+    var resizeTimer;
+    on(window, "resize", function() {
+      if (resizeTimer == null) resizeTimer = setTimeout(function() {
+        resizeTimer = null;
+        knownScrollbarWidth = null;
+        forEachCodeMirror(onResize);
+      }, 100);
+    });
+    // When the window loses focus, we want to show the editor as blurred
+    on(window, "blur", function() {
+      forEachCodeMirror(onBlur);
+    });
+  }
+
   // FEATURE DETECTION
 
   // Detect drag-and-drop
   var dragAndDrop = function() {
     // There is *some* kind of drag-and-drop support in IE6-8, but I
     // couldn't get it to work yet.
-    if (ie_upto8) return false;
+    if (ie && ie_version < 9) return false;
     var div = elt('div');
     return "draggable" in div || "dragDrop" in div;
   }();
       var test = elt("span", "\u200b");
       removeChildrenAndAdd(measure, elt("span", [test, document.createTextNode("x")]));
       if (measure.firstChild.offsetHeight != 0)
-        zwspSupported = test.offsetWidth <= 1 && test.offsetHeight > 2 && !ie_upto7;
+        zwspSupported = test.offsetWidth <= 1 && test.offsetHeight > 2 && !(ie && ie_version < 8);
     }
     if (zwspSupported) return elt("span", "\u200b");
     else return elt("span", "\u00a0", null, "display: inline-block; width: 1px; margin-right: -1px");
     return typeof e.oncopy == "function";
   })();
 
+  var badZoomedRects = null;
+  function hasBadZoomedRects(measure) {
+    if (badZoomedRects != null) return badZoomedRects;
+    var node = removeChildrenAndAdd(measure, elt("span", "x"));
+    var normal = node.getBoundingClientRect();
+    var fromRange = range(node, 0, 1).getBoundingClientRect();
+    return badZoomedRects = Math.abs(normal.left - fromRange.left) > 1;
+  }
+
   // KEY NAMES
 
   var keyNames = {3: "Enter", 8: "Backspace", 9: "Tab", 13: "Enter", 16: "Shift", 17: "Ctrl", 18: "Alt",
     var ch = !order ? line.text.length : order[0].level % 2 ? lineLeft(line) : lineRight(line);
     return Pos(lineN == null ? lineNo(line) : lineN, ch);
   }
+  function lineStartSmart(cm, pos) {
+    var start = lineStart(cm, pos.line);
+    var line = getLine(cm.doc, start.line);
+    var order = getOrder(line);
+    if (!order || order[0].level == 0) {
+      var firstNonWS = Math.max(0, line.text.search(/\S/));
+      var inWS = pos.line == start.line && pos.ch <= firstNonWS && pos.ch;
+      return Pos(start.line, inWS ? 0 : firstNonWS);
+    }
+    return start;
+  }
 
   function compareBidiLevel(order, a, b) {
     var linedir = order[0].level;
 
   // THE END
 
-  CodeMirror.version = "4.1.1";
+  CodeMirror.version = "4.4.1";
 
   return CodeMirror;
 });