export interface DiffLine {
type: 'added' | 'removed';
content: string;
lineNum: number;
}
export type DiffCalculator = (oldStr: string, newStr: string) => DiffLine[];
export const calculateDiff = (oldStr: string, newStr: string): DiffLine[] => {
const oldLines = oldStr.split('\n');
const newLines = newStr.split('\n');
const lcsTable: number[][] = Array.from({ length: oldLines.length + 1 }, () =>
new Array<number>(newLines.length + 1).fill(0),
);
for (let oldIndex = oldLines.length - 1; oldIndex >= 0; oldIndex -= 1) {
for (let newIndex = newLines.length - 1; newIndex >= 0; newIndex -= 1) {
if (oldLines[oldIndex] === newLines[newIndex]) {
lcsTable[oldIndex][newIndex] = lcsTable[oldIndex + 1][newIndex + 1] + 1;
} else {
lcsTable[oldIndex][newIndex] = Math.max(
lcsTable[oldIndex + 1][newIndex],
lcsTable[oldIndex][newIndex + 1],
);
}
}
}
const diffLines: DiffLine[] = [];
let oldIndex = 0;
let newIndex = 0;
while (oldIndex < oldLines.length && newIndex < newLines.length) {
const oldLine = oldLines[oldIndex];
const newLine = newLines[newIndex];
if (oldLine === newLine) {
oldIndex += 1;
newIndex += 1;
continue;
}
if (lcsTable[oldIndex + 1][newIndex] >= lcsTable[oldIndex][newIndex + 1]) {
diffLines.push({ type: 'removed', content: oldLine, lineNum: oldIndex + 1 });
oldIndex += 1;
continue;
}
diffLines.push({ type: 'added', content: newLine, lineNum: newIndex + 1 });
newIndex += 1;
}
while (oldIndex < oldLines.length) {
diffLines.push({ type: 'removed', content: oldLines[oldIndex], lineNum: oldIndex + 1 });
oldIndex += 1;
}
while (newIndex < newLines.length) {
diffLines.push({ type: 'added', content: newLines[newIndex], lineNum: newIndex + 1 });
newIndex += 1;
}
return diffLines;
};
export const createCachedDiffCalculator = (): DiffCalculator => {
const cache = new Map<string, DiffLine[]>();
return (oldStr: string, newStr: string) => {
const key = JSON.stringify([oldStr, newStr]);
const cached = cache.get(key);
if (cached) {
return cached;
}
const calculated = calculateDiff(oldStr, newStr);
cache.set(key, calculated);
if (cache.size > 100) {
const firstKey = cache.keys().next().value;
if (firstKey) {
cache.delete(firstKey);
}
}
return calculated;
};
};