tui: add line-based diff hunk computation
computeHunks() returns Hunk[] (add | del | ctx) with line numbers on the new side for add/ctx and the old side for del. Both inputs are normalized to end with a newline before calling jsdiff's diffLines, otherwise a missing EOF newline is treated as a token boundary and the last line shows up as a spurious del+add pair.
This commit is contained in:
parent
ba40f10e6e
commit
4d22232ec0
2 changed files with 86 additions and 0 deletions
39
src/tui/shared/diff.ts
Normal file
39
src/tui/shared/diff.ts
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
import { diffLines } from 'diff';
|
||||
import { Hunk } from './types.js';
|
||||
|
||||
export function computeHunks(oldText: string, newText: string): Hunk[] {
|
||||
// jsdiff treats trailing-newline presence as a token boundary, so a missing
|
||||
// EOF newline shows up as a spurious del+add pair on the last line. Normalize.
|
||||
const a = oldText.endsWith('\n') ? oldText : oldText + '\n';
|
||||
const b = newText.endsWith('\n') ? newText : newText + '\n';
|
||||
const parts = diffLines(a, b);
|
||||
const out: Hunk[] = [];
|
||||
let oldLine = 1;
|
||||
let newLine = 1;
|
||||
|
||||
for (const part of parts) {
|
||||
const lines = part.value.split('\n');
|
||||
if (lines.length > 0 && lines[lines.length - 1] === '') {
|
||||
lines.pop();
|
||||
}
|
||||
if (part.added) {
|
||||
for (const text of lines) {
|
||||
out.push({ kind: 'add', lineNo: newLine, text });
|
||||
newLine++;
|
||||
}
|
||||
} else if (part.removed) {
|
||||
for (const text of lines) {
|
||||
out.push({ kind: 'del', lineNo: oldLine, text });
|
||||
oldLine++;
|
||||
}
|
||||
} else {
|
||||
for (const text of lines) {
|
||||
out.push({ kind: 'ctx', lineNo: newLine, text });
|
||||
oldLine++;
|
||||
newLine++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return out;
|
||||
}
|
||||
47
tests/tui/diff.test.ts
Normal file
47
tests/tui/diff.test.ts
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
import { describe, it, expect } from 'vitest';
|
||||
import { computeHunks } from '../../src/tui/shared/diff.ts';
|
||||
|
||||
describe('computeHunks', () => {
|
||||
it('returns ctx hunks when both sides are identical', () => {
|
||||
const a = 'one\ntwo\nthree';
|
||||
const hunks = computeHunks(a, a);
|
||||
expect(hunks.every((h) => h.kind === 'ctx')).toBe(true);
|
||||
expect(hunks).toHaveLength(3);
|
||||
});
|
||||
|
||||
it('marks added lines', () => {
|
||||
const a = 'one\ntwo';
|
||||
const b = 'one\ntwo\nthree';
|
||||
const hunks = computeHunks(a, b);
|
||||
const added = hunks.filter((h) => h.kind === 'add');
|
||||
expect(added).toHaveLength(1);
|
||||
expect(added[0].text).toBe('three');
|
||||
});
|
||||
|
||||
it('marks deleted lines', () => {
|
||||
const a = 'one\ntwo\nthree';
|
||||
const b = 'one\ntwo';
|
||||
const hunks = computeHunks(a, b);
|
||||
const deleted = hunks.filter((h) => h.kind === 'del');
|
||||
expect(deleted).toHaveLength(1);
|
||||
expect(deleted[0].text).toBe('three');
|
||||
});
|
||||
|
||||
it('handles a substitution as del + add', () => {
|
||||
const a = 'a\nb\nc';
|
||||
const b = 'a\nB\nc';
|
||||
const hunks = computeHunks(a, b);
|
||||
expect(hunks.some((h) => h.kind === 'del' && h.text === 'b')).toBe(true);
|
||||
expect(hunks.some((h) => h.kind === 'add' && h.text === 'B')).toBe(true);
|
||||
});
|
||||
|
||||
it('reports add/ctx line numbers against the new side, del against the old side', () => {
|
||||
const a = 'a\nb\nc';
|
||||
const b = 'a\nx\nc';
|
||||
const hunks = computeHunks(a, b);
|
||||
const del = hunks.find((h) => h.kind === 'del')!;
|
||||
const add = hunks.find((h) => h.kind === 'add')!;
|
||||
expect(del.lineNo).toBe(2);
|
||||
expect(add.lineNo).toBe(2);
|
||||
});
|
||||
});
|
||||
Loading…
Reference in a new issue