mirror of
https://github.com/t8y2/dbx.git
synced 2026-10-26 08:02:06 +00:00
298 lines
8.7 KiB
TypeScript
298 lines
8.7 KiB
TypeScript
import { diffLines, type Change } from "diff";
|
|
|
|
export type HunkType = "equal" | "delete" | "insert" | "modify";
|
|
|
|
export interface DiffLine {
|
|
type: HunkType;
|
|
content: string;
|
|
lineNumber: number | null;
|
|
isPadding: boolean;
|
|
}
|
|
|
|
export interface DiffHunk {
|
|
id: string;
|
|
type: HunkType;
|
|
leftLines: DiffLine[];
|
|
rightLines: DiffLine[];
|
|
// Measured pixel positions after rendering
|
|
leftTop: number;
|
|
leftBottom: number;
|
|
rightTop: number;
|
|
rightBottom: number;
|
|
}
|
|
|
|
const SIMILARITY_THRESHOLD = 0.3;
|
|
const ALIGN_WINDOW = 3;
|
|
|
|
function splitLines(value: string): string[] {
|
|
const lines = value.split("\n");
|
|
if (lines.length > 0 && lines[lines.length - 1] === "") lines.pop();
|
|
return lines;
|
|
}
|
|
|
|
function buildLines(lines: string[], type: HunkType, startLineNum: number): DiffLine[] {
|
|
return lines.map((content, idx) => ({
|
|
type,
|
|
content,
|
|
lineNumber: startLineNum + idx,
|
|
isPadding: false,
|
|
}));
|
|
}
|
|
|
|
function buildPaddingLines(count: number): DiffLine[] {
|
|
return Array.from({ length: count }, () => ({
|
|
type: "equal" as HunkType,
|
|
content: "",
|
|
lineNumber: null,
|
|
isPadding: true,
|
|
}));
|
|
}
|
|
|
|
function normalizeDdl(ddl: string): string {
|
|
return ddl
|
|
.replace(/\r\n/g, "\n")
|
|
.replace(/\r/g, "\n")
|
|
.split("\n")
|
|
.map((line) => line.replace(/[ \t]+$/g, ""))
|
|
.join("\n");
|
|
}
|
|
|
|
function collectSameKindChanges(changes: Change[], startIdx: number, kind: "added" | "removed"): [string[], number] {
|
|
const parts: string[] = [];
|
|
let i = startIdx;
|
|
while (i < changes.length && changes[i][kind]) {
|
|
parts.push(changes[i].value);
|
|
i++;
|
|
}
|
|
return [parts, i];
|
|
}
|
|
|
|
function levenshteinDistance(a: string, b: string): number {
|
|
const matrix: number[][] = [];
|
|
for (let i = 0; i <= b.length; i++) matrix[i] = [i];
|
|
for (let j = 0; j <= a.length; j++) matrix[0][j] = j;
|
|
for (let i = 1; i <= b.length; i++) {
|
|
for (let j = 1; j <= a.length; j++) {
|
|
matrix[i][j] = b.charAt(i - 1) === a.charAt(j - 1) ? matrix[i - 1][j - 1] : Math.min(matrix[i - 1][j - 1] + 1, matrix[i][j - 1] + 1, matrix[i - 1][j] + 1);
|
|
}
|
|
}
|
|
return matrix[b.length][a.length];
|
|
}
|
|
|
|
function computeSimilarity(a: string, b: string): number {
|
|
if (a === b) return 1;
|
|
const longer = a.length > b.length ? a : b;
|
|
if (longer.length === 0) return 1;
|
|
const distance = levenshteinDistance(a, b);
|
|
return (longer.length - distance) / longer.length;
|
|
}
|
|
|
|
type AlignedItem = { type: "modify"; left: string; right: string } | { type: "delete"; left: string } | { type: "insert"; right: string };
|
|
|
|
function alignLineByLine(removedLines: string[], addedLines: string[]): AlignedItem[] {
|
|
const result: AlignedItem[] = [];
|
|
let r = 0;
|
|
let a = 0;
|
|
|
|
while (r < removedLines.length || a < addedLines.length) {
|
|
if (r < removedLines.length && a < addedLines.length) {
|
|
const directSim = computeSimilarity(removedLines[r], addedLines[a]);
|
|
if (directSim >= SIMILARITY_THRESHOLD) {
|
|
result.push({ type: "modify", left: removedLines[r], right: addedLines[a] });
|
|
r++;
|
|
a++;
|
|
continue;
|
|
}
|
|
|
|
// Look ahead for best match within a small window
|
|
let bestR = -1;
|
|
let bestA = -1;
|
|
let bestSim = SIMILARITY_THRESHOLD;
|
|
|
|
for (let ri = r; ri < Math.min(r + ALIGN_WINDOW, removedLines.length); ri++) {
|
|
for (let aj = a; aj < Math.min(a + ALIGN_WINDOW, addedLines.length); aj++) {
|
|
const sim = computeSimilarity(removedLines[ri], addedLines[aj]);
|
|
if (sim > bestSim) {
|
|
bestSim = sim;
|
|
bestR = ri;
|
|
bestA = aj;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (bestR >= 0 && bestA >= 0) {
|
|
while (r < bestR) {
|
|
result.push({ type: "delete", left: removedLines[r] });
|
|
r++;
|
|
}
|
|
while (a < bestA) {
|
|
result.push({ type: "insert", right: addedLines[a] });
|
|
a++;
|
|
}
|
|
result.push({ type: "modify", left: removedLines[r], right: addedLines[a] });
|
|
r++;
|
|
a++;
|
|
} else {
|
|
result.push({ type: "delete", left: removedLines[r] });
|
|
result.push({ type: "insert", right: addedLines[a] });
|
|
r++;
|
|
a++;
|
|
}
|
|
} else if (r < removedLines.length) {
|
|
result.push({ type: "delete", left: removedLines[r] });
|
|
r++;
|
|
} else {
|
|
result.push({ type: "insert", right: addedLines[a] });
|
|
a++;
|
|
}
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
export function buildHunks(sourceDdl: string, targetDdl: string): DiffHunk[] {
|
|
const normalizedSource = normalizeDdl(sourceDdl);
|
|
const normalizedTarget = normalizeDdl(targetDdl);
|
|
const changes = diffLines(normalizedSource, normalizedTarget, { newlineIsToken: false });
|
|
|
|
const hunks: DiffHunk[] = [];
|
|
let leftLineNum = 1;
|
|
let rightLineNum = 1;
|
|
let hunkIdCounter = 0;
|
|
|
|
function nextId(): string {
|
|
return `hunk-${hunkIdCounter++}`;
|
|
}
|
|
|
|
let i = 0;
|
|
while (i < changes.length) {
|
|
const change = changes[i];
|
|
|
|
if (!change.added && !change.removed) {
|
|
const lines = splitLines(change.value);
|
|
hunks.push({
|
|
id: nextId(),
|
|
type: "equal",
|
|
leftLines: buildLines(lines, "equal", leftLineNum),
|
|
rightLines: buildLines(lines, "equal", rightLineNum),
|
|
leftTop: 0,
|
|
leftBottom: 0,
|
|
rightTop: 0,
|
|
rightBottom: 0,
|
|
});
|
|
leftLineNum += lines.length;
|
|
rightLineNum += lines.length;
|
|
i++;
|
|
continue;
|
|
}
|
|
|
|
if (change.removed) {
|
|
const [removedParts, afterRemoved] = collectSameKindChanges(changes, i, "removed");
|
|
const [addedParts, afterAdded] = collectSameKindChanges(changes, afterRemoved, "added");
|
|
const removedValue = removedParts.join("");
|
|
const removedLines = splitLines(removedValue);
|
|
|
|
if (addedParts.length > 0) {
|
|
const addedValue = addedParts.join("");
|
|
const addedLines = splitLines(addedValue);
|
|
const aligned = alignLineByLine(removedLines, addedLines);
|
|
|
|
for (const item of aligned) {
|
|
if (item.type === "modify") {
|
|
const maxLines = 1;
|
|
const leftReal = buildLines([item.left], "modify", leftLineNum);
|
|
const rightReal = buildLines([item.right], "modify", rightLineNum);
|
|
leftLineNum++;
|
|
rightLineNum++;
|
|
hunks.push({
|
|
id: nextId(),
|
|
type: "modify",
|
|
leftLines: padLines(leftReal, maxLines, "modify"),
|
|
rightLines: padLines(rightReal, maxLines, "modify"),
|
|
leftTop: 0,
|
|
leftBottom: 0,
|
|
rightTop: 0,
|
|
rightBottom: 0,
|
|
});
|
|
} else if (item.type === "delete") {
|
|
const leftReal = buildLines([item.left], "delete", leftLineNum);
|
|
leftLineNum++;
|
|
hunks.push({
|
|
id: nextId(),
|
|
type: "delete",
|
|
leftLines: leftReal,
|
|
rightLines: buildPaddingLines(1),
|
|
leftTop: 0,
|
|
leftBottom: 0,
|
|
rightTop: 0,
|
|
rightBottom: 0,
|
|
});
|
|
} else if (item.type === "insert") {
|
|
const rightReal = buildLines([item.right], "insert", rightLineNum);
|
|
rightLineNum++;
|
|
hunks.push({
|
|
id: nextId(),
|
|
type: "insert",
|
|
leftLines: buildPaddingLines(1),
|
|
rightLines: rightReal,
|
|
leftTop: 0,
|
|
leftBottom: 0,
|
|
rightTop: 0,
|
|
rightBottom: 0,
|
|
});
|
|
}
|
|
}
|
|
} else {
|
|
const maxLines = removedLines.length;
|
|
const leftReal = buildLines(removedLines, "delete", leftLineNum);
|
|
leftLineNum += removedLines.length;
|
|
hunks.push({
|
|
id: nextId(),
|
|
type: "delete",
|
|
leftLines: leftReal,
|
|
rightLines: buildPaddingLines(maxLines),
|
|
leftTop: 0,
|
|
leftBottom: 0,
|
|
rightTop: 0,
|
|
rightBottom: 0,
|
|
});
|
|
}
|
|
i = afterAdded;
|
|
continue;
|
|
}
|
|
|
|
if (change.added) {
|
|
const [addedParts, afterAdded] = collectSameKindChanges(changes, i, "added");
|
|
const addedValue = addedParts.join("");
|
|
const addedLines = splitLines(addedValue);
|
|
const rightReal = buildLines(addedLines, "insert", rightLineNum);
|
|
rightLineNum += addedLines.length;
|
|
hunks.push({
|
|
id: nextId(),
|
|
type: "insert",
|
|
leftLines: buildPaddingLines(addedLines.length),
|
|
rightLines: rightReal,
|
|
leftTop: 0,
|
|
leftBottom: 0,
|
|
rightTop: 0,
|
|
rightBottom: 0,
|
|
});
|
|
i = afterAdded;
|
|
continue;
|
|
}
|
|
}
|
|
|
|
return hunks;
|
|
}
|
|
|
|
function padLines(lines: DiffLine[], targetCount: number, type: HunkType): DiffLine[] {
|
|
if (lines.length >= targetCount) return lines;
|
|
const padding = Array.from({ length: targetCount - lines.length }, () => ({
|
|
type,
|
|
content: "",
|
|
lineNumber: null,
|
|
isPadding: true,
|
|
}));
|
|
return [...lines, ...padding];
|
|
}
|