diff --git a/lib/codemirror.js b/lib/codemirror.js
index b2e194273f22d0acc5401b7d05780703e3b6ff6a..1bf02d2089fa0351b7581e00fd43803e88b562f3 100644
--- a/lib/codemirror.js
+++ b/lib/codemirror.js
@@ -108,6 +108,7 @@
 
       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);
     });
   }
@@ -467,18 +468,18 @@
   }
 
   // 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 ? Math.max(0, 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)};
@@ -543,83 +544,46 @@
 
   // 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 (webkit && barMeasure.scrollWidth > barMeasure.clientWidth &&
-          barMeasure.scrollWidth < barMeasure.clientWidth + 1 &&
-          !hScrollbarTakesSpace(cm))
-        updateScrollbars(cm); // (Issue #2562)
-      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) {
@@ -628,7 +592,7 @@
     }
 
     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));
@@ -636,13 +600,15 @@
     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.
@@ -654,24 +620,50 @@
     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) {
+      updateHeightsInViewport(cm);
+      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.
+        var visible = visibleLines(cm.display, cm.doc, viewport);
+        if (visible.from >= cm.display.viewFrom && visible.to <= cm.display.viewTo)
+          break;
+      }
+      if (!updateDisplayIfNeeded(cm, update)) break;
+    }
+
+    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)) {
+      postUpdateDisplay(cm, update);
+      var barMeasure = measureForScrollbars(cm);
+      updateSelection(cm);
+      setDocumentHeight(cm, barMeasure);
+      updateScrollbars(cm, barMeasure);
+    }
   }
 
   function setDocumentHeight(cm, measure) {
@@ -1257,10 +1249,10 @@
   // 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];
@@ -1275,16 +1267,23 @@
     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 updateSelection(cm, drawn) {
+    if (!drawn) drawn = drawSelection(cm);
+    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";
+    }
   }
 
   // Draws a cursor for the given range
@@ -1967,28 +1966,88 @@
     if (!group) return;
 
     try { fireCallbacksForOps(group); }
-    finally {      
-      cm.curOp = operationGroup = null;
+    finally {
+      operationGroup = null;
       for (var i = 0; i < group.ops.length; i++)
-        endOperationInner(group.ops[i]);
+        group.ops[i].cm.curOp = null;
+      endOperations(group);
     }
   }
 
-  function endOperationInner(op) {
-    var cm = op.cm, display = cm.display, doc = cm.doc;
+  // 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) 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) postUpdateDisplay(cm, op.update);
+
+    // If the max line changed since it was last measured, measure it,
+    // and ensure the document's width matches it.
+    // updateDisplayIfNeeded 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;
+      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 != doc.height) updateScrollbars(cm);
+
+    op.barMeasure = measureForScrollbars(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);
+    }
+
+    if (op.newSelectionNodes)
+      updateSelection(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;
 
     // Abort mouse wheel delta measurement, when scrolling explicitly
     if (display.wheelStartX != null && (op.scrollTop != null || op.scrollLeft != null || op.scrollToPos))
@@ -2011,11 +2070,6 @@
       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;
@@ -2024,6 +2078,19 @@
     if (unhidden) for (var i = 0; i < unhidden.length; ++i)
       if (unhidden[i].lines.length) signal(unhidden[i], "unhide");
 
+    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);
@@ -2916,10 +2983,10 @@
   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
@@ -3002,7 +3069,7 @@
       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) {