import { parsePatchFiles } from "@pierre/diffs/utils/parsePatchFiles"; import type { FileDiffMetadata } from "@pierre/diffs/types"; export const DIFF_THEME_NAMES = { light: "pierre-light", dark: "pierre-dark", } as const; export type DiffThemeName = (typeof DIFF_THEME_NAMES)[keyof typeof DIFF_THEME_NAMES]; export function resolveDiffThemeName(theme: "light" | "dark"): DiffThemeName { return theme === "dark" ? DIFF_THEME_NAMES.dark : DIFF_THEME_NAMES.light; } const FNV_OFFSET_BASIS_32 = 0x811c9dc5; const FNV_PRIME_32 = 0x01000193; const SECONDARY_HASH_SEED = 0x9e3779b9; const SECONDARY_HASH_MULTIPLIER = 0x85ebca6b; export function fnv1a32( input: string, seed = FNV_OFFSET_BASIS_32, multiplier = FNV_PRIME_32, ): number { let hash = seed >>> 0; for (let index = 0; index < input.length; index += 1) { hash ^= input.charCodeAt(index); hash = Math.imul(hash, multiplier) >>> 0; } return hash >>> 0; } export function buildPatchCacheKey(patch: string, scope = "diff-panel"): string { const normalizedPatch = patch.trim(); const primary = fnv1a32(normalizedPatch, FNV_OFFSET_BASIS_32, FNV_PRIME_32).toString(36); const secondary = fnv1a32( normalizedPatch, SECONDARY_HASH_SEED, SECONDARY_HASH_MULTIPLIER, ).toString(36); return `${scope}:${normalizedPatch.length}:${primary}:${secondary}`; } export type RenderablePatch = | { kind: "files"; files: FileDiffMetadata[]; } | { kind: "raw"; text: string; reason: string; }; interface RenderablePatchOptions { /** * Pierre's partial-patch parser keeps hunk render starts in source-file * coordinates. Its virtualizer iterates partial patches as compact rows, so * review diffs need compact render starts while retaining collapsedBefore * for the "N unmodified lines" separator. */ compactPartialHunkOffsets?: boolean; } export function compactPartialHunkOffsets(file: FileDiffMetadata): FileDiffMetadata { if (!file.isPartial) return file; let splitLineStart = 0; let unifiedLineStart = 0; const hunks = file.hunks.map((hunk) => { const compactHunk = { ...hunk, splitLineStart, unifiedLineStart, }; splitLineStart += hunk.splitLineCount; unifiedLineStart += hunk.unifiedLineCount; return compactHunk; }); return { ...file, hunks, splitLineCount: splitLineStart, unifiedLineCount: unifiedLineStart, ...(file.cacheKey ? { cacheKey: `${file.cacheKey}:compact-partial` } : {}), }; } export function getRenderablePatch( patch: string | undefined, cacheScope = "diff-panel", options: RenderablePatchOptions = {}, ): RenderablePatch | null { if (!patch) return null; const normalizedPatch = patch.trim(); if (normalizedPatch.length === 0) return null; try { const parsedPatches = parsePatchFiles( normalizedPatch, buildPatchCacheKey(normalizedPatch, cacheScope), ); const files = parsedPatches.flatMap((parsedPatch) => options.compactPartialHunkOffsets ? parsedPatch.files.map(compactPartialHunkOffsets) : parsedPatch.files, ); if (files.length > 0) { return { kind: "files", files }; } return { kind: "raw", text: normalizedPatch, reason: "Unsupported diff format. Showing raw patch.", }; } catch { return { kind: "raw", text: normalizedPatch, reason: "Failed to parse patch. Showing raw patch.", }; } } export function resolveFileDiffPath(fileDiff: FileDiffMetadata): string { const raw = fileDiff.name ?? fileDiff.prevName ?? ""; if (raw.startsWith("a/") || raw.startsWith("b/")) { return raw.slice(2); } return raw; } export function buildFileDiffRenderKey(fileDiff: FileDiffMetadata): string { return fileDiff.cacheKey ?? `${fileDiff.prevName ?? "none"}:${fileDiff.name}`; } export function getDiffCollapseIconClassName(fileDiff: FileDiffMetadata): string { switch (fileDiff.type) { case "new": return "text-[var(--diffs-addition-base)]"; case "deleted": return "text-[var(--diffs-deletion-base)]"; case "change": case "rename-pure": case "rename-changed": return "text-[var(--diffs-modified-base)]"; default: return "text-muted-foreground/80"; } }