|
51 | 51 | this.classes.classLocation = classLocation
|
52 | 52 |
|
53 | 53 | 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); |
55 | 55 | this.diffOutOfDate = this.dealigned = false;
|
56 | 56 | this.needsScrollSync = null
|
57 | 57 |
|
|
78 | 78 | function ensureDiff(dv) {
|
79 | 79 | if (dv.diffOutOfDate) {
|
80 | 80 | 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); |
82 | 82 | dv.diffOutOfDate = false;
|
83 | 83 | CodeMirror.signal(dv.edit, "updateDiff", dv.diff);
|
84 | 84 | }
|
|
101 | 101 | }
|
102 | 102 | ensureDiff(dv);
|
103 | 103 | 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); |
106 | 106 | }
|
107 | 107 |
|
108 | 108 | if (dv.mv.options.connect == "align")
|
|
242 | 242 | }
|
243 | 243 |
|
244 | 244 | // 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) { |
246 | 246 | var vp = editor.getViewport();
|
247 | 247 | editor.operation(function() {
|
248 | 248 | if (state.from == state.to || vp.from - state.to > 20 || state.from - vp.to > 20) {
|
249 | 249 | 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); |
251 | 251 | state.from = vp.from; state.to = vp.to;
|
252 | 252 | } else {
|
253 | 253 | 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); |
255 | 255 | state.from = vp.from;
|
256 | 256 | }
|
257 | 257 | 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); |
259 | 259 | state.to = vp.to;
|
260 | 260 | }
|
261 | 261 | }
|
|
272 | 272 | return line;
|
273 | 273 | }
|
274 | 274 |
|
275 |
| - function markChanges(editor, diff, type, marks, from, to, classes) { |
| 275 | + function markChanges(editor, diff, type, marks, from, to, classes, chunks, chunkPerLine) { |
276 | 276 | var pos = Pos(0, 0);
|
277 | 277 | var top = Pos(from, 0), bot = editor.clipPos(Pos(to - 1));
|
278 | 278 | var cls = type == DIFF_DELETE ? classes.del : classes.insert;
|
|
289 | 289 | }
|
290 | 290 | }
|
291 | 291 |
|
| 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 | + |
292 | 300 | var chunkStart = 0, pending = false;
|
293 | 301 | for (var i = 0; i < diff.length; ++i) {
|
294 | 302 | var part = diff[i], tp = part[0], str = part[1];
|
|
297 | 305 | moveOver(pos, str);
|
298 | 306 | var cleanTo = pos.line + (endOfLineClean(diff, i) ? 1 : 0);
|
299 | 307 | if (cleanTo > cleanFrom) {
|
300 |
| - if (pending) { markChunk(chunkStart, cleanFrom); pending = false } |
| 308 | + if (pending) { |
| 309 | + if (!chunkPerLine) markChunk(chunkStart, cleanFrom); |
| 310 | + pending = false; |
| 311 | + } |
301 | 312 | chunkStart = cleanTo;
|
302 | 313 | }
|
303 | 314 | } else {
|
|
334 | 345 | var ch = dv.chunks[i];
|
335 | 346 | if (ch.editFrom <= vpEdit.to && ch.editTo >= vpEdit.from &&
|
336 | 347 | ch.origFrom <= vpOrig.to && ch.origTo >= vpOrig.from)
|
337 |
| - drawConnectorsForChunk(dv, ch, sTopOrig, sTopEdit, w); |
| 348 | + drawConnectorsForChunk(dv, ch, sTopOrig, sTopEdit, w, i); |
338 | 349 | }
|
339 | 350 | }
|
340 | 351 |
|
|
411 | 422 | }
|
412 | 423 |
|
413 | 424 | function findAlignedLines(dv, other) {
|
| 425 | + if (dv.mv.options.chunkPerLine) return findAlignedLinesByChunks(dv.chunks) |
414 | 426 | var alignable = alignableFor(dv.edit, dv.chunks, false), result = []
|
415 | 427 | if (other) for (var i = 0, j = 0; i < other.chunks.length; i++) {
|
416 | 428 | var n = other.chunks[i].editTo
|
|
427 | 439 | return result
|
428 | 440 | }
|
429 | 441 |
|
| 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 | + |
430 | 454 | function alignChunks(dv, force) {
|
431 | 455 | if (!dv.dealigned && !force) return;
|
432 | 456 | if (!dv.orig.curOp) return dv.orig.operation(function() {
|
|
455 | 479 | }
|
456 | 480 |
|
457 | 481 | 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) |
459 | 483 | 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); |
461 | 485 |
|
462 | 486 | for (var i = 0; i < cm.length; i++)
|
463 | 487 | cm[i].scrollTo(null, scroll[i]);
|
464 | 488 | }
|
465 | 489 |
|
466 |
| - function alignLines(cm, cmOffset, lines, aligners) { |
| 490 | + function alignLines(cm, cmOffset, lines, aligners, padDirection) { |
467 | 491 | var maxOffset = -1e8, offset = [];
|
468 | 492 | for (var i = 0; i < cm.length; i++) if (lines[i] != null) {
|
469 | 493 | var off = cm[i].heightAtLine(lines[i], "local") - cmOffset[i];
|
|
472 | 496 | }
|
473 | 497 | for (var i = 0; i < cm.length; i++) if (lines[i] != null) {
|
474 | 498 | 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)); |
477 | 500 | }
|
478 | 501 | }
|
479 | 502 |
|
| 503 | + function padAlign(cm, line, size, padDirection) { |
| 504 | + if (padDirection === 'below') return padBelow(cm, line, size) |
| 505 | + return padAbove(cm, line, size) |
| 506 | + } |
| 507 | + |
480 | 508 | function padAbove(cm, line, size) {
|
481 | 509 | var above = true;
|
482 | 510 | if (line > cm.lastLine()) {
|
|
489 | 517 | return cm.addLineWidget(line, elt, {height: size, above: above, mergeSpacer: true, handleMouseEvents: true});
|
490 | 518 | }
|
491 | 519 |
|
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) { |
493 | 528 | var flip = dv.type == "left";
|
494 | 529 | var top = dv.orig.heightAtLine(chunk.origFrom, "local", true) - sTopOrig;
|
495 | 530 | if (dv.svg) {
|
|
517 | 552 | copy.chunk = chunk;
|
518 | 553 | copy.style.top = (chunk.origTo > chunk.origFrom ? top : dv.edit.heightAtLine(chunk.editFrom, "local") - sTopEdit) + "px";
|
519 | 554 | copy.setAttribute("role", "button");
|
| 555 | + copy.setAttribute("data-chunk-index", index); |
520 | 556 |
|
521 | 557 | if (editOriginals) {
|
522 | 558 | var leftButton = typeof dv.getLeftRevertButton === 'function' && dv.getLeftRevertButton("CodeMirror-merge-copy-reverse");
|
|
530 | 566 | copyReverse.style.top = topReverse + "px";
|
531 | 567 | dv.type == "right" ? copyReverse.style.left = "2px" : copyReverse.style.right = "2px";
|
532 | 568 | copyReverse.setAttribute("role", "button");
|
| 569 | + copyReverse.setAttribute("data-chunk-index", index); |
533 | 570 | }
|
534 | 571 | }
|
535 | 572 | }
|
|
679 | 716 | return diff;
|
680 | 717 | }
|
681 | 718 |
|
682 |
| - function getChunks(diff) { |
| 719 | + function getChunks(diff, chunkPerLine) { |
| 720 | + if (chunkPerLine) return getLineChunks(diff); |
683 | 721 | var chunks = [];
|
684 | 722 | if (!diff.length) return chunks;
|
685 | 723 | var startEdit = 0, startOrig = 0;
|
|
704 | 742 | if (startEdit <= edit.line || startOrig <= orig.line)
|
705 | 743 | chunks.push({origFrom: startOrig, origTo: orig.line + 1,
|
706 | 744 | 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 |
708 | 840 | }
|
709 | 841 |
|
710 | 842 | function endOfLineClean(diff, i) {
|
|
0 commit comments