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() }