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(); 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 }; }); };