import type { FileDiffMetadata, SelectionSide } from "@pierre/diffs";
import { findMatches } from "#lib/components/lines/search.ts";
export interface DiffRow {
deletion?: number;
addition?: number;
text: string;
gap: boolean;
}
export interface DiffMatch {
id: string;
row: number;
line: number;
side: SelectionSide;
start: number;
end: number;
}
const clean = (line: string | undefined): string => (line ?? "").replace(/\r?\n$/, "");
const firstLine = (start: number, count: number): number => (count === 0 ? start + 1 : start);
const buildRows = (diff: FileDiffMetadata): DiffRow[] => {
const rows: DiffRow[] = [];
const full = !diff.isPartial;
const unchanged = (deletion: number, addition: number, count: number) => {
for (let k = 0; k < count; k++) {
rows.push({
deletion: deletion + k,
addition: addition + k,
text: clean(diff.additionLines[addition + k - 1]),
gap: true
});
}
};
let deletion = 1;
let addition = 1;
for (const hunk of diff.hunks) {
deletion = firstLine(hunk.deletionStart, hunk.deletionCount);
addition = firstLine(hunk.additionStart, hunk.additionCount);
if (full) {
unchanged(
deletion - hunk.collapsedBefore,
addition - hunk.collapsedBefore,
hunk.collapsedBefore
);
}
for (const content of hunk.hunkContent) {
if (content.type === "context") {
for (let k = 0; k < content.lines; k++) {
rows.push({
deletion: deletion + k,
addition: addition + k,
text: clean(diff.additionLines[content.additionLineIndex + k]),
gap: false
});
}
deletion += content.lines;
addition += content.lines;
continue;
}
for (let k = 0; k < content.deletions; k++) {
rows.push({
deletion: deletion + k,
text: clean(diff.deletionLines[content.deletionLineIndex + k]),
gap: false
});
}
for (let k = 0; k < content.additions; k++) {
rows.push({
addition: addition + k,
text: clean(diff.additionLines[content.additionLineIndex + k]),
gap: false
});
}
deletion += content.deletions;
addition += content.additions;
}
}
if (full) unchanged(deletion, addition, diff.additionLines.length - addition + 1);
return rows;
};
const built = new WeakMap<FileDiffMetadata, DiffRow[]>();
export const diffRows = (diff: FileDiffMetadata): DiffRow[] => {
let rows = built.get(diff);
if (!rows) {
rows = buildRows(diff);
built.set(diff, rows);
}
return rows;
};
export const rowLine = (row: DiffRow, side: SelectionSide): number | undefined =>
side === "deletions" ? row.deletion : row.addition;
export const rowSide = (row: DiffRow): SelectionSide =>
row.addition === undefined ? "deletions" : "additions";
export const findDiffMatches = (
id: string,
rows: readonly DiffRow[],
query: string,
limit: number
): DiffMatch[] => {
const visible = rows.flatMap((row, index) => (row.gap ? [] : [{ row, index }]));
return findMatches(
visible.map(({ row }) => row.text),
query,
limit
).map((match) => {
const { row, index } = visible[match.line - 1];
const side = rowSide(row);
return {
id,
row: index,
line: rowLine(row, side)!,
side,
start: match.start,
end: match.end
};
});
};