Use @pierre/diffs library for side-by-side diff view

- Replace custom diff implementation with @pierre/diffs FileDiff component
- Add proper indentation to normalized plist output for accurate diffing
- Keep key-level diff summary for quick overview
This commit is contained in:
cc
2026-04-14 17:05:43 +02:00
parent c16a8e9f4d
commit 3f035977cd
4 changed files with 1177 additions and 153 deletions
+16 -115
View File
@@ -1,49 +1,11 @@
"use client";
import { useMemo } from "react";
import { FileDiff } from "@pierre/diffs/react";
import { parseDiffFromFile } from "@pierre/diffs";
import { diffPlistKeys, type PlistDiff } from "@/lib/plist";
interface DiffLine {
type: "unchanged" | "added" | "removed" | "changed";
oldLine?: string;
newLine?: string;
key?: string;
}
function computeLineDiff(oldLines: string[], newLines: string[]): DiffLine[] {
const result: DiffLine[] = [];
const oldSet = new Set(oldLines);
const newSet = new Set(newLines);
const maxLen = Math.max(oldLines.length, newLines.length);
let oi = 0;
let ni = 0;
while (oi < oldLines.length || ni < newLines.length) {
const oldLine = oldLines[oi];
const newLine = newLines[ni];
if (oldLine === newLine) {
result.push({ type: "unchanged", oldLine, newLine });
oi++;
ni++;
} else if (oldLine && !newSet.has(oldLine)) {
result.push({ type: "removed", oldLine, newLine: undefined });
oi++;
} else if (newLine && !oldSet.has(newLine)) {
result.push({ type: "added", oldLine: undefined, newLine });
ni++;
} else {
result.push({ type: "changed", oldLine, newLine });
oi++;
ni++;
}
}
return result;
}
interface DiffViewerProps {
oldXml: string;
newXml: string;
@@ -57,91 +19,30 @@ export function DiffViewer({
oldLabel,
newLabel,
}: DiffViewerProps) {
const diff = useMemo(() => diffPlistKeys(oldXml, newXml), [oldXml, newXml]);
const oldLines = useMemo(
() => oldXml.split("\n").filter((l) => l.trim()),
[oldXml],
);
const newLines = useMemo(
() => newXml.split("\n").filter((l) => l.trim()),
[newXml],
const keysDiff = useMemo(
() => diffPlistKeys(oldXml, newXml),
[oldXml, newXml],
);
const lineDiff = useMemo(
() => computeLineDiff(oldLines, newLines),
[oldLines, newLines],
const fileDiff = useMemo(
() =>
parseDiffFromFile(
{ name: `${oldLabel}.plist`, contents: oldXml },
{ name: `${newLabel}.plist`, contents: newXml },
),
[oldXml, newXml, oldLabel, newLabel],
);
return (
<div className="space-y-4">
<DiffSummary diff={diff} />
<div className="grid grid-cols-2 gap-2 font-mono text-xs">
<div className="bg-gray-100 dark:bg-gray-800 rounded-t px-3 py-2 font-semibold border-b">
{oldLabel}
</div>
<div className="bg-gray-100 dark:bg-gray-800 rounded-t px-3 py-2 font-semibold border-b">
{newLabel}
</div>
<div className="col-span-2">
<div className="grid grid-cols-2 gap-2">
{lineDiff.map((line, i) => (
<DiffLineRow key={i} line={line} />
))}
</div>
</div>
<DiffSummary diff={keysDiff} />
<div className="rounded-lg overflow-hidden border text-sm">
<FileDiff fileDiff={fileDiff} options={{ diffStyle: "split" }} />
</div>
</div>
);
}
function DiffLineRow({ line }: { line: DiffLine }) {
const baseClasses = "px-3 py-0.5 font-mono text-xs whitespace-pre overflow-x-auto";
switch (line.type) {
case "unchanged":
return (
<>
<div className={`${baseClasses} bg-gray-50 dark:bg-gray-900`}>
{line.oldLine}
</div>
<div className={`${baseClasses} bg-gray-50 dark:bg-gray-900`}>
{line.newLine}
</div>
</>
);
case "removed":
return (
<>
<div className={`${baseClasses} bg-red-100 dark:bg-red-900/30 text-red-800 dark:text-red-200`}>
{line.oldLine}
</div>
<div className={`${baseClasses} bg-gray-100 dark:bg-gray-800`} />
</>
);
case "added":
return (
<>
<div className={`${baseClasses} bg-gray-100 dark:bg-gray-800`} />
<div className={`${baseClasses} bg-green-100 dark:bg-green-900/30 text-green-800 dark:text-green-200`}>
{line.newLine}
</div>
</>
);
case "changed":
return (
<>
<div className={`${baseClasses} bg-yellow-100 dark:bg-yellow-900/30 text-yellow-800 dark:text-yellow-200`}>
{line.oldLine}
</div>
<div className={`${baseClasses} bg-yellow-100 dark:bg-yellow-900/30 text-yellow-800 dark:text-yellow-200`}>
{line.newLine}
</div>
</>
);
}
}
function DiffSummary({ diff }: { diff: PlistDiff }) {
const hasChanges =
diff.added.length > 0 || diff.removed.length > 0 || diff.changed.length > 0;
+16 -3
View File
@@ -32,15 +32,28 @@ export function normalizePlist(xml: string): string {
const lines = [
'<?xml version="1.0" encoding="UTF-8"?>',
'<plist version="1.0">',
"<dict>",
...entries.map((e) => `<key>${e.key}</key>\n${e.value}`),
"</dict>",
" <dict>",
...entries.map((e) => ` <key>${e.key}</key>\n ${indentValue(e.value)}`),
" </dict>",
"</plist>",
];
return lines.join("\n");
}
function indentValue(xml: string): string {
// Simple indentation for single-line values
if (!xml.includes("\n") && !xml.includes("><")) {
return xml;
}
// For complex values, add indentation after each closing >
return xml
.replace(/></g, ">\n <")
.split("\n")
.map((line, i) => (i === 0 ? line : " " + line))
.join("\n");
}
export interface PlistDiff {
added: string[];
removed: string[];