This commit is contained in:
Timothy Jaeryang Baek 2026-10-15 15:23:52 +04:00
parent 1595c20130
commit 10ce0d2365
2 changed files with 50 additions and 21 deletions

View file

@ -6,6 +6,48 @@ export const diffEditorConfig: DiffConfig = { timeout: 100 };
export type MergeDirection = 'to-working' | 'to-proposal';
// Match rows by the text preserved inside a chunk, not their ordinal position.
export function changedLinePairs(a: Text, b: Text, chunk: Chunk): [number, number][] {
const candidates = new Map<number, Map<number, { a: number; b: number; weight: number }>>();
let fromA = chunk.fromA;
let fromB = chunk.fromB;
for (let index = 0; index <= chunk.changes.length; index++) {
const change = chunk.changes[index];
const endA = change ? chunk.fromA + change.fromA : Math.min(chunk.toA, a.length);
const endB = change ? chunk.fromB + change.fromB : Math.min(chunk.toB, b.length);
while (fromA < endA && fromB < endB) {
const lineA = a.lineAt(fromA),
lineB = b.lineAt(fromB);
const length = Math.min(endA - fromA, endB - fromB, lineA.to - fromA, lineB.to - fromB);
const weight = a.sliceString(fromA, fromA + length).replace(/\s/g, '').length;
if (weight) {
let row = candidates.get(lineA.from);
if (!row) candidates.set(lineA.from, (row = new Map()));
const pair = row.get(lineB.from) ?? { a: lineA.from, b: lineB.from, weight: 0 };
pair.weight += weight;
row.set(lineB.from, pair);
}
fromA += length || 1;
fromB += length || 1;
}
if (change) {
fromA = chunk.fromA + change.toA;
fromB = chunk.fromB + change.toB;
}
}
const bestA = new Map<number, { a: number; b: number; weight: number }>();
const bestB = new Map<number, { a: number; b: number; weight: number }>();
for (const row of candidates.values()) {
for (const pair of row.values()) {
if (pair.weight > (bestA.get(pair.a)?.weight ?? 0)) bestA.set(pair.a, pair);
if (pair.weight > (bestB.get(pair.b)?.weight ?? 0)) bestB.set(pair.b, pair);
}
}
return [...bestA.values()]
.filter((pair) => bestB.get(pair.b) === pair)
.map((pair) => [pair.a, pair.b]);
}
export function changedLineCounts(a: Text, b: Text, chunks: readonly Chunk[]) {
const count = (doc: Text, side: 'A' | 'B') => {
let total = 0;

View file

@ -7,16 +7,19 @@ import {
type DecorationSet
} from '@codemirror/view';
import type { Chunk } from '@codemirror/merge';
import { changedLinePairs } from './diffEditor';
export function pairedLinePosition(a: Text, b: Text, chunks: readonly Chunk[], position: number) {
let offset = 0;
for (const chunk of chunks) {
if (position < chunk.fromA) break;
if (position < chunk.toA) {
const row = a.lineAt(position).number - a.lineAt(chunk.fromA).number;
const start = b.lineAt(Math.min(chunk.fromB, b.length)).number;
const end = b.lineAt(Math.min(Math.max(chunk.fromB, chunk.toB - 1), b.length)).number;
return b.line(Math.min(start + row, end)).from;
const pairs = changedLinePairs(a, b, chunk);
const line = a.lineAt(position).from;
return (
(pairs.find(([from]) => from >= line) ?? pairs.at(-1))?.[1] ??
Math.min(chunk.fromB, b.length)
);
}
offset = chunk.toB - chunk.toA;
}
@ -128,23 +131,7 @@ export function alignChangedLines(a: EditorView, b: EditorView, chunks: readonly
align(posA, posB);
}
if (!chunk) break;
if (chunk.fromA < chunk.toA && chunk.fromB < chunk.toB) {
const lineA = a.state.doc.lineAt(chunk.fromA).number,
lineB = b.state.doc.lineAt(chunk.fromB).number;
let row = Math.max(
0,
a.state.doc.lineAt(a.viewport.from).number - lineA,
b.state.doc.lineAt(b.viewport.from).number - lineB
);
while (lineA + row <= a.state.doc.lines && lineB + row <= b.state.doc.lines) {
const posA = a.state.doc.line(lineA + row).from,
posB = b.state.doc.line(lineB + row).from;
if (posA >= chunk.toA || posB >= chunk.toB || posA > a.viewport.to || posB > b.viewport.to)
break;
align(posA, posB);
row++;
}
}
for (const [posA, posB] of changedLinePairs(a.state.doc, b.state.doc, chunk)) align(posA, posB);
fromA = chunk.toA;
fromB = chunk.toB;
}