Skip to content

Commit 4e13ab4

Browse files
authored
Merge pull request #26 from stanley2058/feature/line-based-chunks
feature/line based chunks
2 parents 3277be9 + 9e3e881 commit 4e13ab4

File tree

1 file changed

+151
-19
lines changed

1 file changed

+151
-19
lines changed

addon/merge/merge.js

+151-19
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@
5151
this.classes.classLocation = classLocation
5252

5353
this.diff = getDiff(asString(orig), asString(options.value), this.mv.options.ignoreWhitespace);
54-
this.chunks = getChunks(this.diff);
54+
this.chunks = getChunks(this.diff, this.mv.options.chunkPerLine);
5555
this.diffOutOfDate = this.dealigned = false;
5656
this.needsScrollSync = null
5757

@@ -78,7 +78,7 @@
7878
function ensureDiff(dv) {
7979
if (dv.diffOutOfDate) {
8080
dv.diff = getDiff(dv.orig.getValue(), dv.edit.getValue(), dv.mv.options.ignoreWhitespace);
81-
dv.chunks = getChunks(dv.diff);
81+
dv.chunks = getChunks(dv.diff, dv.mv.options.chunkPerLine);
8282
dv.diffOutOfDate = false;
8383
CodeMirror.signal(dv.edit, "updateDiff", dv.diff);
8484
}
@@ -101,8 +101,8 @@
101101
}
102102
ensureDiff(dv);
103103
if (dv.showDifferences) {
104-
updateMarks(dv.edit, dv.diff, edit, DIFF_INSERT, dv.classes);
105-
updateMarks(dv.orig, dv.diff, orig, DIFF_DELETE, dv.classes);
104+
updateMarks(dv.edit, dv.diff, edit, DIFF_INSERT, dv.classes, dv.chunks, dv.mv.options.chunkPerLine);
105+
updateMarks(dv.orig, dv.diff, orig, DIFF_DELETE, dv.classes, dv.chunks, dv.mv.options.chunkPerLine);
106106
}
107107

108108
if (dv.mv.options.connect == "align")
@@ -242,20 +242,20 @@
242242
}
243243

244244
// FIXME maybe add a margin around viewport to prevent too many updates
245-
function updateMarks(editor, diff, state, type, classes) {
245+
function updateMarks(editor, diff, state, type, classes, chunks, chunkPerLine) {
246246
var vp = editor.getViewport();
247247
editor.operation(function() {
248248
if (state.from == state.to || vp.from - state.to > 20 || state.from - vp.to > 20) {
249249
clearMarks(editor, state.marked, classes);
250-
markChanges(editor, diff, type, state.marked, vp.from, vp.to, classes);
250+
markChanges(editor, diff, type, state.marked, vp.from, vp.to, classes, chunks, chunkPerLine);
251251
state.from = vp.from; state.to = vp.to;
252252
} else {
253253
if (vp.from < state.from) {
254-
markChanges(editor, diff, type, state.marked, vp.from, state.from, classes);
254+
markChanges(editor, diff, type, state.marked, vp.from, state.from, classes, chunks, chunkPerLine);
255255
state.from = vp.from;
256256
}
257257
if (vp.to > state.to) {
258-
markChanges(editor, diff, type, state.marked, state.to, vp.to, classes);
258+
markChanges(editor, diff, type, state.marked, state.to, vp.to, classes, chunks, chunkPerLine);
259259
state.to = vp.to;
260260
}
261261
}
@@ -272,7 +272,7 @@
272272
return line;
273273
}
274274

275-
function markChanges(editor, diff, type, marks, from, to, classes) {
275+
function markChanges(editor, diff, type, marks, from, to, classes, chunks, chunkPerLine) {
276276
var pos = Pos(0, 0);
277277
var top = Pos(from, 0), bot = editor.clipPos(Pos(to - 1));
278278
var cls = type == DIFF_DELETE ? classes.del : classes.insert;
@@ -289,6 +289,14 @@
289289
}
290290
}
291291

292+
if (chunkPerLine) {
293+
for (var i = 0; i < chunks.length; i++) {
294+
const chunk = chunks[i];
295+
if (type === DIFF_DELETE) markChunk(chunk.origFrom, chunk.origTo);
296+
else markChunk(chunk.editFrom, chunk.editTo);
297+
}
298+
}
299+
292300
var chunkStart = 0, pending = false;
293301
for (var i = 0; i < diff.length; ++i) {
294302
var part = diff[i], tp = part[0], str = part[1];
@@ -297,7 +305,10 @@
297305
moveOver(pos, str);
298306
var cleanTo = pos.line + (endOfLineClean(diff, i) ? 1 : 0);
299307
if (cleanTo > cleanFrom) {
300-
if (pending) { markChunk(chunkStart, cleanFrom); pending = false }
308+
if (pending) {
309+
if (!chunkPerLine) markChunk(chunkStart, cleanFrom);
310+
pending = false;
311+
}
301312
chunkStart = cleanTo;
302313
}
303314
} else {
@@ -334,7 +345,7 @@
334345
var ch = dv.chunks[i];
335346
if (ch.editFrom <= vpEdit.to && ch.editTo >= vpEdit.from &&
336347
ch.origFrom <= vpOrig.to && ch.origTo >= vpOrig.from)
337-
drawConnectorsForChunk(dv, ch, sTopOrig, sTopEdit, w);
348+
drawConnectorsForChunk(dv, ch, sTopOrig, sTopEdit, w, i);
338349
}
339350
}
340351

@@ -411,6 +422,7 @@
411422
}
412423

413424
function findAlignedLines(dv, other) {
425+
if (dv.mv.options.chunkPerLine) return findAlignedLinesByChunks(dv.chunks)
414426
var alignable = alignableFor(dv.edit, dv.chunks, false), result = []
415427
if (other) for (var i = 0, j = 0; i < other.chunks.length; i++) {
416428
var n = other.chunks[i].editTo
@@ -427,6 +439,18 @@
427439
return result
428440
}
429441

442+
/**
443+
* @param {Chunk[]} chunks
444+
*/
445+
function findAlignedLinesByChunks(chunks) {
446+
const alignedEnds = []
447+
for (var i = 0; i < chunks.length; i++) {
448+
const chunk = chunks[i]
449+
alignedEnds.push([chunk.editTo, chunk.origTo, null])
450+
}
451+
return alignedEnds
452+
}
453+
430454
function alignChunks(dv, force) {
431455
if (!dv.dealigned && !force) return;
432456
if (!dv.orig.curOp) return dv.orig.operation(function() {
@@ -455,15 +479,15 @@
455479
}
456480

457481
if (offset[0] != offset[1] || cm.length == 3 && offset[1] != offset[2])
458-
alignLines(cm, offset, [0, 0, 0], aligners)
482+
alignLines(cm, offset, [0, 0, 0], aligners, dv.mv.options.padDirection)
459483
for (var ln = 0; ln < linesToAlign.length; ln++)
460-
alignLines(cm, offset, linesToAlign[ln], aligners);
484+
alignLines(cm, offset, linesToAlign[ln], aligners, dv.mv.options.padDirection);
461485

462486
for (var i = 0; i < cm.length; i++)
463487
cm[i].scrollTo(null, scroll[i]);
464488
}
465489

466-
function alignLines(cm, cmOffset, lines, aligners) {
490+
function alignLines(cm, cmOffset, lines, aligners, padDirection) {
467491
var maxOffset = -1e8, offset = [];
468492
for (var i = 0; i < cm.length; i++) if (lines[i] != null) {
469493
var off = cm[i].heightAtLine(lines[i], "local") - cmOffset[i];
@@ -472,11 +496,15 @@
472496
}
473497
for (var i = 0; i < cm.length; i++) if (lines[i] != null) {
474498
var diff = maxOffset - offset[i];
475-
if (diff > 1)
476-
aligners.push(padAbove(cm[i], lines[i], diff));
499+
if (diff > 1) aligners.push(padAlign(cm[i], lines[i] - 1, diff, padDirection));
477500
}
478501
}
479502

503+
function padAlign(cm, line, size, padDirection) {
504+
if (padDirection === 'below') return padBelow(cm, line, size)
505+
return padAbove(cm, line, size)
506+
}
507+
480508
function padAbove(cm, line, size) {
481509
var above = true;
482510
if (line > cm.lastLine()) {
@@ -489,7 +517,14 @@
489517
return cm.addLineWidget(line, elt, {height: size, above: above, mergeSpacer: true, handleMouseEvents: true});
490518
}
491519

492-
function drawConnectorsForChunk(dv, chunk, sTopOrig, sTopEdit, w) {
520+
function padBelow(cm, line, size) {
521+
var elt = document.createElement("div");
522+
elt.className = "CodeMirror-merge-spacer";
523+
elt.style.height = size + "px"; elt.style.minWidth = "1px";
524+
return cm.addLineWidget(line, elt, {height: size, above: false, mergeSpacer: true, handleMouseEvents: true});
525+
}
526+
527+
function drawConnectorsForChunk(dv, chunk, sTopOrig, sTopEdit, w, index) {
493528
var flip = dv.type == "left";
494529
var top = dv.orig.heightAtLine(chunk.origFrom, "local", true) - sTopOrig;
495530
if (dv.svg) {
@@ -517,6 +552,7 @@
517552
copy.chunk = chunk;
518553
copy.style.top = (chunk.origTo > chunk.origFrom ? top : dv.edit.heightAtLine(chunk.editFrom, "local") - sTopEdit) + "px";
519554
copy.setAttribute("role", "button");
555+
copy.setAttribute("data-chunk-index", index);
520556

521557
if (editOriginals) {
522558
var leftButton = typeof dv.getLeftRevertButton === 'function' && dv.getLeftRevertButton("CodeMirror-merge-copy-reverse");
@@ -530,6 +566,7 @@
530566
copyReverse.style.top = topReverse + "px";
531567
dv.type == "right" ? copyReverse.style.left = "2px" : copyReverse.style.right = "2px";
532568
copyReverse.setAttribute("role", "button");
569+
copyReverse.setAttribute("data-chunk-index", index);
533570
}
534571
}
535572
}
@@ -679,7 +716,8 @@
679716
return diff;
680717
}
681718

682-
function getChunks(diff) {
719+
function getChunks(diff, chunkPerLine) {
720+
if (chunkPerLine) return getLineChunks(diff);
683721
var chunks = [];
684722
if (!diff.length) return chunks;
685723
var startEdit = 0, startOrig = 0;
@@ -704,7 +742,101 @@
704742
if (startEdit <= edit.line || startOrig <= orig.line)
705743
chunks.push({origFrom: startOrig, origTo: orig.line + 1,
706744
editFrom: startEdit, editTo: edit.line + 1});
707-
return chunks;
745+
return chunks
746+
}
747+
748+
function getLineChunks(diffs) {
749+
const chunks = []
750+
var origLine = 0
751+
var editLine = 0
752+
for (var i = 0; i < diffs.length; i++) {
753+
const diff = diffs[i]
754+
755+
const lines = countChar(diff[1], '\n')
756+
const origStart = origLine
757+
const editStart = editLine
758+
switch(diff[0]) {
759+
case DIFF_EQUAL: {
760+
origLine += lines
761+
editLine += lines
762+
break
763+
}
764+
case DIFF_INSERT: {
765+
editLine += lines
766+
break
767+
}
768+
case DIFF_DELETE: {
769+
origLine += lines
770+
break
771+
}
772+
}
773+
const origEnd = origLine + 1
774+
const editEnd = editLine + 1
775+
776+
if (diff[0] === DIFF_EQUAL) continue
777+
chunks.push({
778+
origFrom: origStart,
779+
origTo: origEnd,
780+
editFrom: editStart,
781+
editTo: editEnd,
782+
})
783+
}
784+
if (chunks.length === 0) return chunks
785+
786+
// combine overlapping chunks
787+
const origCombined = combineChunks(
788+
chunks,
789+
function(prev, curr) {
790+
const left = prev.origFrom
791+
const right = prev.origTo
792+
return (curr.origFrom >= left && curr.origFrom < right) ||
793+
(curr.origTo >= left && curr.origTo < right)
794+
}
795+
)
796+
const editCombined = combineChunks(
797+
origCombined,
798+
function(prev, curr) {
799+
const left = prev.editFrom
800+
const right = prev.editTo
801+
return (curr.editFrom >= left && curr.editFrom < right) ||
802+
(curr.editTo >= left && curr.editTo < right)
803+
}
804+
)
805+
806+
return editCombined
807+
}
808+
809+
/**
810+
* @typedef {{
811+
* origFrom: number
812+
* origTo: number
813+
* editFrom: number
814+
* editTo: number
815+
* }} Chunk
816+
* @param {Chunk[]} chunks
817+
* @param {(prev: Chunk, curr: Chunk) => boolean} hasOverlap
818+
*/
819+
function combineChunks(chunks, hasOverlap) {
820+
if (chunks.length === 0) return []
821+
const combined = [chunks[0]]
822+
for (var i = 1; i < chunks.length; i++) {
823+
const lastIdx = combined.length - 1
824+
const overlapping = hasOverlap(combined[lastIdx], chunks[i])
825+
if (!overlapping) combined.push(chunks[i])
826+
else {
827+
combined[lastIdx].origFrom = Math.min(combined[lastIdx].origFrom, chunks[i].origFrom)
828+
combined[lastIdx].origTo = Math.max(combined[lastIdx].origTo, chunks[i].origTo)
829+
combined[lastIdx].editFrom = Math.min(combined[lastIdx].editFrom, chunks[i].editFrom)
830+
combined[lastIdx].editTo = Math.max(combined[lastIdx].editTo, chunks[i].editTo)
831+
}
832+
}
833+
return combined
834+
}
835+
836+
function countChar(str, ch) {
837+
var occur = 0
838+
for (var i = 0; i < str.length; i++) if (str[i] === ch) occur++
839+
return occur
708840
}
709841

710842
function endOfLineClean(diff, i) {

0 commit comments

Comments
 (0)