export type StructuredPatchLine = {
type: "context" | "delete" | "add";
text: string;
};
export type StructuredPatchHunk = {
oldStart: number;
oldLines: number;
newStart: number;
newLines: number;
lines: StructuredPatchLine[];
};
export function buildStructuredPatch(oldContent: string | null, newContent: string): StructuredPatchHunk[] {
if (oldContent === null) {
const addedLines = splitLines(newContent);
if (addedLines.length === 0) return [];
return [{
oldStart: 1,
oldLines: 0,
newStart: 1,
newLines: addedLines.length,
lines: addedLines.map((text) => ({ type: "add" as const, text })),
}];
}
if (oldContent === newContent) {
return [];
}
const oldLines = splitLines(oldContent);
const newLines = splitLines(newContent);
let prefix = 0;
while (prefix < oldLines.length && prefix < newLines.length && oldLines[prefix] === newLines[prefix]) {
prefix += 1;
}
let suffix = 0;
while (
suffix < oldLines.length - prefix
&& suffix < newLines.length - prefix
&& oldLines[oldLines.length - 1 - suffix] === newLines[newLines.length - 1 - suffix]
) {
suffix += 1;
}
const contextLines = 3;
const oldChangedEnd = oldLines.length - suffix;
const newChangedEnd = newLines.length - suffix;
const oldContextStart = Math.max(0, prefix - contextLines);
const newContextStart = Math.max(0, prefix - contextLines);
const oldContextEnd = Math.min(oldLines.length, oldChangedEnd + contextLines);
const newContextEnd = Math.min(newLines.length, newChangedEnd + contextLines);
const lines: StructuredPatchLine[] = [];
for (const text of oldLines.slice(oldContextStart, prefix)) {
lines.push({ type: "context", text });
}
for (const text of oldLines.slice(prefix, oldChangedEnd)) {
lines.push({ type: "delete", text });
}
for (const text of newLines.slice(prefix, newChangedEnd)) {
lines.push({ type: "add", text });
}
for (const text of newLines.slice(newChangedEnd, newContextEnd)) {
lines.push({ type: "context", text });
}
return [{
oldStart: oldContextStart + 1,
oldLines: oldContextEnd - oldContextStart,
newStart: newContextStart + 1,
newLines: newContextEnd - newContextStart,
lines,
}];
}
export function buildUnifiedDiff(filePath: string, oldContent: string | null, newContent: string): string {
const hunks = buildStructuredPatch(oldContent, newContent);
if (oldContent !== null && hunks.length === 0) {
return "";
}
const fromPath = oldContent === null ? "/dev/null" : `a/${filePath}`;
const toPath = `b/${filePath}`;
const body = hunks.map((hunk) => {
const header = `@@ -${formatRange(hunk.oldStart, hunk.oldLines)} +${formatRange(hunk.newStart, hunk.newLines)} @@`;
const lines = hunk.lines.map((line) => `${prefixFor(line.type)}${line.text}`);
return [header, ...lines].join("\n");
}).join("\n");
return `--- ${fromPath}\n+++ ${toPath}${body ? `\n${body}` : ""}`;
}
function splitLines(content: string): string[] {
return content.length === 0 ? [] : content.split("\n");
}
function prefixFor(type: StructuredPatchLine["type"]): " " | "-" | "+" {
switch (type) {
case "context":
return " ";
case "delete":
return "-";
case "add":
return "+";
}
}
function formatRange(start: number, length: number): string {
if (length === 0) return `${start},0`;
if (length === 1) return `${start}`;
return `${start},${length}`;
}