diff --git a/addon/merge/merge.js b/addon/merge/merge.js
index da5dcca55a3a93d00cbaa89bc03bc5689ee111bf..071fb280a1fcdabcc630dfe00e39a58bdf322863 100644
--- a/addon/merge/merge.js
+++ b/addon/merge/merge.js
@@ -37,8 +37,13 @@
     constructor: DiffView,
     init: function(pane, orig, options) {
       this.edit = this.mv.edit;
-      (this.edit.state.diffViews || (this.edit.state.diffViews = [])).push(this);
+      ;(this.edit.state.diffViews || (this.edit.state.diffViews = [])).push(this);
       this.orig = CodeMirror(pane, copyObj({value: orig, readOnly: !this.mv.options.allowEditingOriginals}, copyObj(options)));
+      if (this.mv.options.connect == "align") {
+        if (!this.edit.state.trackAlignable) this.edit.state.trackAlignable = new TrackAlignable(this.edit)
+        this.orig.state.trackAlignable = new TrackAlignable(this.orig)
+      }
+
       this.orig.state.diffViews = [this];
       var classLocation = options.chunkClassLocation || "background";
       if (Object.prototype.toString.call(classLocation) != "[object Array]") classLocation = [classLocation]
@@ -129,10 +134,9 @@
     dv.orig.on("change", change);
     dv.edit.on("swapDoc", swapDoc);
     dv.orig.on("swapDoc", swapDoc);
-    var events = ["markerAdded", "markerCleared", "lineWidgetAdded", "lineWidgetCleared"]
-    for (var i = 0; i < events.length; i++) {
-      dv.edit.on(events[i], setDealign);
-      dv.orig.on(events[i], setDealign);
+    if (dv.mv.options.connect == "align") {
+      CodeMirror.on(dv.edit.state.trackAlignable, "realign", setDealign)
+      CodeMirror.on(dv.orig.state.trackAlignable, "realign", setDealign)
     }
     dv.edit.on("viewportChange", function() { set(false); });
     dv.orig.on("viewportChange", function() { set(false); });
@@ -339,24 +343,73 @@
     return origStart + (editLine - editStart);
   }
 
-  function findAlignedLines(dv, other) {
-    var linesToAlign = [];
-    for (var i = 0; i < dv.chunks.length; i++) {
-      var chunk = dv.chunks[i];
-      linesToAlign.push([chunk.origTo, chunk.editTo, other ? getMatchingOrigLine(chunk.editTo, other.chunks) : null]);
-    }
-    if (other) {
-      chunkLoop: for (var i = 0; i < other.chunks.length; i++) {
-        var chunk = other.chunks[i];
-        for (var j = 0; j < linesToAlign.length; j++) {
-          var diff = linesToAlign[j][1] - chunk.editTo;
-          if (diff == 0) continue chunkLoop
-          if (diff > 0) break;
-        }
-        linesToAlign.splice(j, 0, [getMatchingOrigLine(chunk.editTo, dv.chunks), chunk.editTo, chunk.origTo]);
+  // Combines information about chunks and widgets/markers to return
+  // an array of lines, in a single editor, that probably need to be
+  // aligned with their counterparts in the editor next to it.
+  function alignableFor(cm, chunks, isOrig) {
+    var tracker = cm.state.trackAlignable
+    var start = cm.firstLine(), trackI = 0
+    var result = []
+    for (var i = 0;; i++) {
+      var chunk = chunks[i]
+      var chunkStart = !chunk ? cm.lastLine() + 1 : isOrig ? chunk.origFrom : chunk.editFrom
+      for (; trackI < tracker.alignable.length; trackI += 2) {
+        var n = tracker.alignable[trackI] + 1
+        if (n <= start) continue
+        if (n < chunkStart) result.push(n)
+        else break
+      }
+      if (!chunk) break
+      result.push(start = isOrig ? chunk.origTo : chunk.editTo)
+    }
+    return result
+  }
+
+  // Given information about alignable lines in two editors, fill in
+  // the result (an array of three-element arrays) to reflect the
+  // lines that need to be aligned with each other.
+  function mergeAlignable(result, origAlignable, chunks, setIndex) {
+    var rI = 0, origI = 0, chunkI = 0, diff = 0
+    for (;; rI++) {
+      var nextR = result[rI], nextO = origAlignable[origI]
+      if (!nextR && nextO == null) break
+
+      var rLine = nextR ? nextR[0] : 1e9, oLine = nextO == null ? 1e9 : nextO
+      while (chunkI < chunks.length) {
+        var chunk = chunks[chunkI]
+        if (chunk.editTo > rLine) break
+        diff += (chunk.origTo - chunk.origFrom) - (chunk.editTo - chunk.editFrom)
+        chunkI++
+      }
+      if (rLine == oLine - diff) {
+        nextR[setIndex] = oLine
+        origI++
+      } else if (rLine < oLine - diff) {
+        nextR[setIndex] = rLine + diff
+      } else {
+        var record = [oLine - diff, null, null]
+        record[setIndex] = oLine
+        result.splice(rI, 0, record)
+        origI++
       }
     }
-    return linesToAlign;
+  }
+
+  function findAlignedLines(dv, other) {
+    var alignable = alignableFor(dv.edit, dv.chunks, false), result = []
+    if (other) for (var i = 0, j = 0; i < other.chunks.length; i++) {
+      var n = other.chunks[i].editTo
+      while (j < alignable.length && alignable[j] < n) j++
+      if (j == alignable.length || alignable[j] != n) alignable.splice(j++, 0, n)
+    }
+    for (var i = 0; i < alignable.length; i++)
+      result.push([alignable[i], null, null])
+
+    mergeAlignable(result, alignableFor(dv.orig, dv.chunks, true), dv.chunks, 1)
+    if (other)
+      mergeAlignable(result, alignableFor(other.orig, other.chunks, true), other.chunks, 2)
+
+    return result
   }
 
   function alignChunks(dv, force) {
@@ -379,7 +432,7 @@
       aligners[i].clear();
     aligners.length = 0;
 
-    var cm = [dv.orig, dv.edit], scroll = [];
+    var cm = [dv.edit, dv.orig], scroll = [];
     if (other) cm.push(other.orig);
     for (var i = 0; i < cm.length; i++)
       scroll.push(cm[i].getScrollInfo().top);
@@ -414,7 +467,7 @@
     var elt = document.createElement("div");
     elt.className = "CodeMirror-merge-spacer";
     elt.style.height = size + "px"; elt.style.minWidth = "1px";
-    return cm.addLineWidget(line, elt, {height: size, above: above});
+    return cm.addLineWidget(line, elt, {height: size, above: above, mergeSpacer: true});
   }
 
   function drawConnectorsForChunk(dv, chunk, sTopOrig, sTopEdit, w) {
@@ -765,6 +818,124 @@
     return out;
   }
 
+  // Tracks collapsed markers and line widgets, in order to be able to
+  // accurately align the content of two editors.
+
+  var F_WIDGET = 1, F_WIDGET_BELOW = 2, F_MARKER = 4
+
+  function TrackAlignable(cm) {
+    this.cm = cm
+    this.alignable = []
+    var self = this
+    cm.on("markerAdded", function(_, marker) {
+      if (!marker.collapsed) return
+      var found = marker.find(1)
+      if (found != null) self.set(found.line, F_MARKER)
+    })
+    cm.on("markerCleared", function(_, marker, _min, max) {
+      if (max != null && marker.collapsed)
+        self.check(max, F_MARKER, self.hasMarker)
+    })
+    cm.on("lineWidgetAdded", function(_, widget, lineNo) {
+      if (widget.mergeSpacer) return
+      if (widget.above) self.set(lineNo - 1, F_WIDGET_BELOW)
+      else self.set(lineNo, F_WIDGET)
+    })
+    cm.on("lineWidgetCleared", function(_, widget, lineNo) {
+      if (widget.mergeSpacer) return
+      if (widget.above) self.check(lineNo - 1, F_WIDGET_BELOW, self.hasWidgetBelow)
+      else self.check(lineNo, F_WIDGET, self.hasWidget)
+    })
+    cm.on("change", function(_, change) {
+      var start = change.from.line, nBefore = change.to.line - change.from.line
+      var nAfter = change.text.length - 1, end = start + nAfter
+      if (nBefore || nAfter) self.map(start, nBefore, nAfter)
+      self.check(end, F_MARKER, self.hasMarker)
+      if (nBefore || nAfter) self.check(change.from.line, F_MARKER, self.hasMarker)
+    })
+  }
+
+  TrackAlignable.prototype = {
+    signal: function() {
+      CodeMirror.signal(this, "realign")
+    },
+
+    set: function(n, flags) {
+      var pos = -1
+      for (; pos < this.alignable.length; pos += 2) {
+        var diff = this.alignable[pos] - n
+        if (diff == 0) {
+          if ((this.alignable[pos + 1] & flags) == flags) return
+          this.alignable[pos + 1] |= flags
+          this.signal()
+          return
+        }
+        if (diff > 0) break
+      }
+      this.signal()
+      this.alignable.splice(pos, 0, n, flags)
+    },
+
+    find: function(n) {
+      for (var i = 0; i < this.alignable.length; i += 2)
+        if (this.alignable[i] == n) return i
+      return -1
+    },
+
+    check: function(n, flag, pred) {
+      var found = this.find(n)
+      if (found == -1 || !(this.alignable[found + 1] & flag)) return
+      if (!pred.call(this, n)) {
+        this.signal()
+        var flags = this.alignable[found + 1] & ~flag
+        if (flags) this.alignable[found + 1] = flags
+        else this.alignable.splice(found, 2)
+      }
+    },
+
+    hasMarker: function(n) {
+      var handle = this.cm.getLineHandle(n)
+      if (handle.markedSpans) for (var i = 0; i < handle.markedSpans.length; i++)
+        if (handle.markedSpans[i].mark.collapsed && handle.markedSpans[i].to != null)
+          return true
+      return false
+    },
+
+    hasWidget: function(n) {
+      var handle = this.cm.getLineHandle(n)
+      if (handle.widgets) for (var i = 0; i < handle.widgets.length; i++)
+        if (!handle.widgets[i].above && !handle.widgets[i].mergeSpacer) return true
+      return false
+    },
+
+    hasWidgetBelow: function(n) {
+      if (n == this.cm.lastLine()) return false
+      var handle = this.cm.getLineHandle(n + 1)
+      if (handle.widgets) for (var i = 0; i < handle.widgets.length; i++)
+        if (handle.widgets[i].above && !handle.widgets[i].mergeSpacer) return true
+      return false
+    },
+
+    map: function(from, nBefore, nAfter) {
+      var diff = nAfter - nBefore, to = from + nBefore, widgetFrom = -1, widgetTo = -1
+      for (var i = 0; i < this.alignable.length; i += 2) {
+        var n = this.alignable[i]
+        if (n == from && (this.alignable[i + 1] & F_WIDGET_BELOW)) widgetFrom = i
+        if (n == to && (this.alignable[i + 1] & F_WIDGET_BELOW)) widgetTo = i
+        if (n <= from) continue
+        else if (n < to) this.alignable.splice(i--, 2)
+        else this.alignable[i] += diff
+      }
+      if (widgetFrom > -1) {
+        var flags = this.alignable[widgetFrom + 1]
+        if (flags == F_WIDGET_BELOW) this.alignable.splice(widgetFrom, 2)
+        else this.alignable[widgetFrom + 1] = flags & ~F_WIDGET_BELOW
+      }
+      if (widgetTo > -1 && nAfter)
+        this.set(from + nAfter, F_WIDGET_BELOW)
+    }
+  }
+
   function posMin(a, b) { return (a.line - b.line || a.ch - b.ch) < 0 ? a : b; }
   function posMax(a, b) { return (a.line - b.line || a.ch - b.ch) > 0 ? a : b; }
   function posEq(a, b) { return a.line == b.line && a.ch == b.ch; }
diff --git a/src/model/line_widget.js b/src/model/line_widget.js
index ba7c3829e2126eb2bc6d4c566ba5123abe998ae9..1ceebd2eeae5dd33587d69b45b4de62ad127d35f 100644
--- a/src/model/line_widget.js
+++ b/src/model/line_widget.js
@@ -30,7 +30,7 @@ export class LineWidget {
         adjustScrollWhenAboveVisible(cm, line, -height)
         regLineChange(cm, no, "widget")
       })
-      signalLater(cm, "lineWidgetCleared", cm, this)
+      signalLater(cm, "lineWidgetCleared", cm, this, no)
     }
   }
 
@@ -70,6 +70,6 @@ export function addLineWidget(doc, handle, node, options) {
     }
     return true
   })
-  signalLater(cm, "lineWidgetAdded", cm, widget)
+  signalLater(cm, "lineWidgetAdded", cm, widget, typeof handle == "number" ? handle : lineNo(handle))
   return widget
 }
diff --git a/src/model/mark_text.js b/src/model/mark_text.js
index 2e8ecf77bdfff134a1e70c818fb7a9ad32442062..68c90ab75ff10f2505051d7209c91fffe87d639b 100644
--- a/src/model/mark_text.js
+++ b/src/model/mark_text.js
@@ -78,7 +78,7 @@ export class TextMarker {
       this.doc.cantEdit = false
       if (cm) reCheckSelection(cm.doc)
     }
-    if (cm) signalLater(cm, "markerCleared", cm, this)
+    if (cm) signalLater(cm, "markerCleared", cm, this, min, max)
     if (withOp) endOperation(cm)
     if (this.parent) this.parent.clear()
   }