源码
packages/ui/src/components/ReviewDiffViewer.tsx
// Adapted from ZCode diff-viewer.tsx and lightweight-diff-preview.tsx (Apache-2.0).
import { MultiFileDiff, PatchDiff } from "@pierre/diffs/react";
import { Component, type CSSProperties, memo, type ReactNode, useMemo } from "react";
import { useAppearance } from "../appearance.ts";
import { type CodePreviewSettings, defaultCodePreview } from "../code-preview.ts";
import { getPatchPreviewLineContent, parseTruncatedMarkerOmittedLineCount } from "../patch-preview.ts";
import { getGitPaneDiffPreviewPlan, type ReviewDiffData } from "../review-diff.ts";
import { findHighlightCSS } from "./use-text-find.ts";
class DiffBoundary extends Component<{ children: ReactNode }, { failed: boolean }> {
state = { failed: false };
static getDerivedStateFromError() {
return { failed: true };
}
render() {
return this.state.failed ? (
<p className="review-diff-fallback">暂时无法预览这个 Diff</p>
) : (
this.props.children
);
}
}
function ReviewDiffViewerComponent({
diff,
id,
searching = false,
codePreview = defaultCodePreview,
}: {
diff: ReviewDiffData;
id: string;
searching?: boolean;
codePreview?: CodePreviewSettings;
}) {
const { theme } = useAppearance();
const plan = useMemo(() => getGitPaneDiffPreviewPlan(diff), [diff]);
const files = useMemo(
() => ({
oldFile: {
name: diff.path,
contents: diff.beforeContent ?? "",
cacheKey: `old:${diff.path}:${diff.beforeContent}`,
},
newFile: {
name: diff.path,
contents: diff.afterContent ?? "",
cacheKey: `new:${diff.path}:${diff.afterContent}`,
},
}),
[diff.path, diff.beforeContent, diff.afterContent],
);
const options = useMemo(
() => ({
diffStyle: "unified" as const,
diffIndicators: "bars" as const,
disableFileHeader: true,
hunkSeparators: plan.kind === "rich" ? ("line-info" as const) : ("simple" as const),
lineDiffType: "word-alt" as const,
overflow: "scroll" as const,
theme: { light: codePreview.lightTheme, dark: codePreview.darkTheme },
themeType: theme,
preferredHighlighter: "shiki-wasm" as const,
expandUnchanged: searching,
unsafeCSS: findHighlightCSS,
onPostRender: (node: HTMLElement) =>
node.dispatchEvent(new Event("zpi-diff-render", { bubbles: true })),
}),
[plan.kind, theme, searching, codePreview.lightTheme, codePreview.darkTheme],
);
const viewerStyle = {
"--diffs-bg": "var(--color-bg)",
"--diffs-light-bg": "var(--color-bg)",
"--diffs-dark-bg": "var(--color-bg)",
"--diffs-font-family": "var(--font-code)",
"--diffs-font-size": `${codePreview.fontSizePx}px`,
} as CSSProperties;
if (diff.availability !== "patch")
return (
<p className="review-diff-fallback">
{diff.availability === "binary"
? "无法预览二进制 Diff"
: diff.availability === "truncated"
? "Diff 已裁剪"
: "暂时无法预览这个 Diff"}
</p>
);
if (plan.kind === "plain-text")
return (
<div className="review-lightweight" data-lightweight-diff-preview>
<div
className="review-lightweight-lines"
data-lightweight-diff-scroll-content
data-wrap={codePreview.wrapLongLines || undefined}
style={{ fontSize: codePreview.fontSizePx }}
>
{plan.lines.map((line, index) => {
const omitted = parseTruncatedMarkerOmittedLineCount(line);
if (omitted !== null)
return (
<div className="review-diff-truncated" key={index}>
Diff 预览已截断:为保持界面流畅,省略了 {omitted} 行。
</div>
);
const kind = line.startsWith("+") ? "added" : line.startsWith("-") ? "removed" : "context";
return (
<div className={`review-lightweight-line ${kind}`} key={index}>
{codePreview.showLineNumbers && (
<span className="review-lightweight-number" aria-hidden="true">
{index + 1}
</span>
)}
<code data-find-key={`${id}:plain:${index}`}>{getPatchPreviewLineContent(line) || " "}</code>
</div>
);
})}
</div>
</div>
);
return (
<DiffBoundary key={diff.patch}>
<div className="review-diff-viewer" data-diff-viewer style={viewerStyle}>
{plan.kind === "patch" && diff.patch ? (
<PatchDiff patch={diff.patch} options={options} style={viewerStyle} />
) : (
<MultiFileDiff {...files} options={options} style={viewerStyle} />
)}
</div>
</DiffBoundary>
);
}
export const ReviewDiffViewer = memo(ReviewDiffViewerComponent);