mirror of
https://github.com/ChiChou/entdb.git
synced 2026-06-10 23:07:47 +02:00
Enhance diff viewer with side-by-side mode and theme support
- Add toggle between unified and split (side-by-side) view - Make diff viewer theme-aware for light/dark modes - Add context lines around changes - Make diff summary collapsible for large change sets
This commit is contained in:
+230
-57
@@ -1,7 +1,9 @@
|
||||
"use client";
|
||||
|
||||
import { useMemo } from "react";
|
||||
import { useMemo, useState } from "react";
|
||||
import { diffPlistKeys, type PlistDiff } from "@/lib/plist";
|
||||
import { Columns2, Rows3, ChevronRight } from "lucide-react";
|
||||
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible";
|
||||
|
||||
interface DiffViewerProps {
|
||||
oldXml: string;
|
||||
@@ -17,14 +19,19 @@ type DiffLine = {
|
||||
newNum?: number;
|
||||
};
|
||||
|
||||
function computeDiff(oldText: string, newText: string): DiffLine[] {
|
||||
type SplitRow = {
|
||||
left?: { num?: number; content: string; type: "context" | "remove" };
|
||||
right?: { num?: number; content: string; type: "context" | "add" };
|
||||
};
|
||||
|
||||
function computeDiff(oldText: string, newText: string, contextLines = 3): DiffLine[] {
|
||||
const oldLines = oldText.split("\n");
|
||||
const newLines = newText.split("\n");
|
||||
|
||||
const oldSet = new Set(oldLines);
|
||||
const newSet = new Set(newLines);
|
||||
|
||||
const result: DiffLine[] = [];
|
||||
const rawDiff: DiffLine[] = [];
|
||||
let oi = 0,
|
||||
ni = 0;
|
||||
|
||||
@@ -33,30 +40,104 @@ function computeDiff(oldText: string, newText: string): DiffLine[] {
|
||||
const newLine = newLines[ni];
|
||||
|
||||
if (oi < oldLines.length && ni < newLines.length && oldLine === newLine) {
|
||||
// Skip context lines - only show changes
|
||||
rawDiff.push({ type: "context", content: oldLine, oldNum: oi + 1, newNum: ni + 1 });
|
||||
oi++;
|
||||
ni++;
|
||||
} else if (oi < oldLines.length && !newSet.has(oldLine)) {
|
||||
result.push({ type: "remove", content: oldLine, oldNum: oi + 1 });
|
||||
rawDiff.push({ type: "remove", content: oldLine, oldNum: oi + 1 });
|
||||
oi++;
|
||||
} else if (ni < newLines.length && !oldSet.has(newLine)) {
|
||||
result.push({ type: "add", content: newLine, newNum: ni + 1 });
|
||||
rawDiff.push({ type: "add", content: newLine, newNum: ni + 1 });
|
||||
ni++;
|
||||
} else {
|
||||
rawDiff.push({ type: "context", content: oldLine, oldNum: oi + 1, newNum: ni + 1 });
|
||||
oi++;
|
||||
ni++;
|
||||
}
|
||||
}
|
||||
|
||||
const includeLines = new Set<number>();
|
||||
rawDiff.forEach((line, idx) => {
|
||||
if (line.type !== "context") {
|
||||
for (let i = Math.max(0, idx - contextLines); i <= Math.min(rawDiff.length - 1, idx + contextLines); i++) {
|
||||
includeLines.add(i);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const result: DiffLine[] = [];
|
||||
let lastIncluded = -1;
|
||||
|
||||
rawDiff.forEach((line, idx) => {
|
||||
if (includeLines.has(idx)) {
|
||||
if (lastIncluded !== -1 && idx - lastIncluded > 1) {
|
||||
result.push({ type: "context", content: "···", oldNum: undefined, newNum: undefined });
|
||||
}
|
||||
result.push(line);
|
||||
lastIncluded = idx;
|
||||
}
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
function computeSplitDiff(diffLines: DiffLine[]): SplitRow[] {
|
||||
const rows: SplitRow[] = [];
|
||||
let i = 0;
|
||||
|
||||
while (i < diffLines.length) {
|
||||
const line = diffLines[i];
|
||||
|
||||
if (line.type === "context") {
|
||||
rows.push({
|
||||
left: { num: line.oldNum, content: line.content, type: "context" },
|
||||
right: { num: line.newNum, content: line.content, type: "context" },
|
||||
});
|
||||
i++;
|
||||
} else if (line.type === "remove") {
|
||||
// Collect consecutive removes
|
||||
const removes: DiffLine[] = [];
|
||||
while (i < diffLines.length && diffLines[i].type === "remove") {
|
||||
removes.push(diffLines[i]);
|
||||
i++;
|
||||
}
|
||||
// Collect consecutive adds
|
||||
const adds: DiffLine[] = [];
|
||||
while (i < diffLines.length && diffLines[i].type === "add") {
|
||||
adds.push(diffLines[i]);
|
||||
i++;
|
||||
}
|
||||
// Pair them up
|
||||
const maxLen = Math.max(removes.length, adds.length);
|
||||
for (let j = 0; j < maxLen; j++) {
|
||||
const row: SplitRow = {};
|
||||
if (j < removes.length) {
|
||||
row.left = { num: removes[j].oldNum, content: removes[j].content, type: "remove" };
|
||||
}
|
||||
if (j < adds.length) {
|
||||
row.right = { num: adds[j].newNum, content: adds[j].content, type: "add" };
|
||||
}
|
||||
rows.push(row);
|
||||
}
|
||||
} else if (line.type === "add") {
|
||||
rows.push({
|
||||
right: { num: line.newNum, content: line.content, type: "add" },
|
||||
});
|
||||
i++;
|
||||
}
|
||||
}
|
||||
|
||||
return rows;
|
||||
}
|
||||
|
||||
export function DiffViewer({
|
||||
oldXml,
|
||||
newXml,
|
||||
oldLabel,
|
||||
newLabel,
|
||||
}: DiffViewerProps) {
|
||||
const [viewMode, setViewMode] = useState<"unified" | "split">("split");
|
||||
|
||||
const keysDiff = useMemo(
|
||||
() => diffPlistKeys(oldXml, newXml),
|
||||
[oldXml, newXml],
|
||||
@@ -67,6 +148,11 @@ export function DiffViewer({
|
||||
[oldXml, newXml],
|
||||
);
|
||||
|
||||
const splitRows = useMemo(
|
||||
() => computeSplitDiff(diffLines),
|
||||
[diffLines],
|
||||
);
|
||||
|
||||
const hasChanges =
|
||||
keysDiff.added.length > 0 ||
|
||||
keysDiff.removed.length > 0 ||
|
||||
@@ -83,65 +169,152 @@ export function DiffViewer({
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<DiffSummary diff={keysDiff} />
|
||||
<div className="rounded-lg overflow-hidden border bg-gray-900 text-gray-100">
|
||||
<div className="flex justify-between px-4 py-2 bg-gray-800 text-sm font-medium border-b border-gray-700">
|
||||
<span className="text-red-400">- {oldLabel}</span>
|
||||
<span className="text-green-400">+ {newLabel}</span>
|
||||
<div className="rounded-lg overflow-hidden border bg-muted/30">
|
||||
<div className="flex items-center justify-between px-4 py-2 bg-muted text-sm font-medium border-b">
|
||||
{viewMode === "unified" ? (
|
||||
<>
|
||||
<span className="text-red-400">- {oldLabel}</span>
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
onClick={() => setViewMode("split")}
|
||||
className="p-1.5 rounded hover:bg-accent transition-colors"
|
||||
title="Split view"
|
||||
>
|
||||
<Columns2 className="h-4 w-4" />
|
||||
</button>
|
||||
</div>
|
||||
<span className="text-green-400">+ {newLabel}</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<span className="text-red-400 flex-1">- {oldLabel}</span>
|
||||
<button
|
||||
onClick={() => setViewMode("unified")}
|
||||
className="p-1.5 rounded hover:bg-accent transition-colors mx-2"
|
||||
title="Unified view"
|
||||
>
|
||||
<Rows3 className="h-4 w-4" />
|
||||
</button>
|
||||
<span className="text-green-400 flex-1 text-right">+ {newLabel}</span>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<pre className="p-4 overflow-x-auto text-xs font-mono">
|
||||
{diffLines.map((line, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className={
|
||||
line.type === "remove"
|
||||
? "bg-red-900/40 text-red-200"
|
||||
: line.type === "add"
|
||||
? "bg-green-900/40 text-green-200"
|
||||
: ""
|
||||
}
|
||||
>
|
||||
<span className="select-none opacity-50 mr-2">
|
||||
{line.type === "remove" ? "-" : line.type === "add" ? "+" : " "}
|
||||
</span>
|
||||
{line.content}
|
||||
|
||||
{viewMode === "unified" ? (
|
||||
<pre className="p-4 overflow-x-auto text-sm font-mono">
|
||||
{diffLines.map((line, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className={
|
||||
line.type === "remove"
|
||||
? "bg-red-100 dark:bg-red-900/40 text-red-800 dark:text-red-200"
|
||||
: line.type === "add"
|
||||
? "bg-green-100 dark:bg-green-900/40 text-green-800 dark:text-green-200"
|
||||
: ""
|
||||
}
|
||||
>
|
||||
<span className="select-none opacity-50 mr-2">
|
||||
{line.type === "remove" ? "-" : line.type === "add" ? "+" : " "}
|
||||
</span>
|
||||
{line.content}
|
||||
</div>
|
||||
))}
|
||||
</pre>
|
||||
) : (
|
||||
<div className="overflow-x-auto">
|
||||
<div className="grid grid-cols-2 text-sm font-mono min-w-[600px]">
|
||||
{splitRows.map((row, i) => (
|
||||
<div key={i} className="contents">
|
||||
<div
|
||||
className={`px-4 py-0.5 border-r ${
|
||||
row.left?.type === "remove"
|
||||
? "bg-red-100 dark:bg-red-900/40 text-red-800 dark:text-red-200"
|
||||
: row.left
|
||||
? ""
|
||||
: "bg-muted/50"
|
||||
}`}
|
||||
>
|
||||
{row.left && (
|
||||
<>
|
||||
<span className="select-none opacity-50 mr-2 inline-block w-8 text-right">
|
||||
{row.left.num ?? ""}
|
||||
</span>
|
||||
{row.left.content}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<div
|
||||
className={`px-4 py-0.5 ${
|
||||
row.right?.type === "add"
|
||||
? "bg-green-100 dark:bg-green-900/40 text-green-800 dark:text-green-200"
|
||||
: row.right
|
||||
? ""
|
||||
: "bg-muted/50"
|
||||
}`}
|
||||
>
|
||||
{row.right && (
|
||||
<>
|
||||
<span className="select-none opacity-50 mr-2 inline-block w-8 text-right">
|
||||
{row.right.num ?? ""}
|
||||
</span>
|
||||
{row.right.content}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
))}
|
||||
</pre>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function DiffSummary({ diff }: { diff: PlistDiff }) {
|
||||
const totalChanges = diff.added.length + diff.removed.length + diff.changed.length;
|
||||
|
||||
const summaryParts = [];
|
||||
if (diff.added.length > 0) summaryParts.push(`${diff.added.length} added`);
|
||||
if (diff.removed.length > 0) summaryParts.push(`${diff.removed.length} removed`);
|
||||
if (diff.changed.length > 0) summaryParts.push(`${diff.changed.length} changed`);
|
||||
|
||||
return (
|
||||
<div className="flex flex-wrap gap-4 text-sm">
|
||||
{diff.added.length > 0 && (
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="inline-block w-3 h-3 rounded bg-green-500" />
|
||||
<span>
|
||||
<strong>{diff.added.length}</strong> added:{" "}
|
||||
<code className="text-xs">{diff.added.join(", ")}</code>
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
{diff.removed.length > 0 && (
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="inline-block w-3 h-3 rounded bg-red-500" />
|
||||
<span>
|
||||
<strong>{diff.removed.length}</strong> removed:{" "}
|
||||
<code className="text-xs">{diff.removed.join(", ")}</code>
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
{diff.changed.length > 0 && (
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="inline-block w-3 h-3 rounded bg-yellow-500" />
|
||||
<span>
|
||||
<strong>{diff.changed.length}</strong> changed:{" "}
|
||||
<code className="text-xs">{diff.changed.join(", ")}</code>
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<Collapsible>
|
||||
<CollapsibleTrigger className="flex items-center gap-2 text-sm hover:bg-accent px-2 py-1.5 -mx-2 rounded transition-colors group w-full text-left">
|
||||
<ChevronRight className="h-4 w-4 text-muted-foreground transition-transform group-data-[state=open]:rotate-90" />
|
||||
<span className="font-medium">{totalChanges} changes</span>
|
||||
<span className="text-muted-foreground">({summaryParts.join(", ")})</span>
|
||||
</CollapsibleTrigger>
|
||||
<CollapsibleContent className="pt-2 pl-6 space-y-2">
|
||||
{diff.added.length > 0 && (
|
||||
<div className="flex items-start gap-2 text-sm">
|
||||
<span className="inline-block w-3 h-3 rounded bg-green-500 mt-1 shrink-0" />
|
||||
<span>
|
||||
<strong>{diff.added.length}</strong> added:{" "}
|
||||
<code className="text-xs break-all">{diff.added.join(", ")}</code>
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
{diff.removed.length > 0 && (
|
||||
<div className="flex items-start gap-2 text-sm">
|
||||
<span className="inline-block w-3 h-3 rounded bg-red-500 mt-1 shrink-0" />
|
||||
<span>
|
||||
<strong>{diff.removed.length}</strong> removed:{" "}
|
||||
<code className="text-xs break-all">{diff.removed.join(", ")}</code>
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
{diff.changed.length > 0 && (
|
||||
<div className="flex items-start gap-2 text-sm">
|
||||
<span className="inline-block w-3 h-3 rounded bg-yellow-500 mt-1 shrink-0" />
|
||||
<span>
|
||||
<strong>{diff.changed.length}</strong> changed:{" "}
|
||||
<code className="text-xs break-all">{diff.changed.join(", ")}</code>
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</CollapsibleContent>
|
||||
</Collapsible>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user