Files
mdpolish/reviewer/src/client/DiffView.tsx
T

101 lines
2.7 KiB
TypeScript

import { markdown } from "@codemirror/lang-markdown";
import { MergeView } from "@codemirror/merge";
import { EditorSelection, EditorState } from "@codemirror/state";
import { drawSelection, EditorView, lineNumbers } from "@codemirror/view";
import { useEffect, useRef } from "react";
interface DiffViewProps {
before: string;
after: string;
beforeLabel: string;
afterLabel: string;
focusRange?: { start: number; end: number } | null;
}
const editorTheme = EditorView.theme({
"&": {
height: "100%",
backgroundColor: "#fbfaf7",
color: "#262822",
fontSize: "13px",
},
".cm-scroller": {
fontFamily: '"SFMono-Regular", Consolas, "Liberation Mono", monospace',
lineHeight: "1.68",
},
".cm-gutters": {
backgroundColor: "#f2f0ea",
color: "#8a877e",
border: "none",
},
".cm-content": {
padding: "18px 0 36px",
},
".cm-line": {
padding: "0 14px",
},
"&.cm-focused": {
outline: "2px solid #a7b9ac",
outlineOffset: "-2px",
},
});
const readOnlyExtensions = [
lineNumbers(),
markdown(),
EditorState.readOnly.of(true),
EditorView.editable.of(false),
drawSelection(),
EditorView.lineWrapping,
editorTheme,
];
export function DiffView({ before, after, beforeLabel, afterLabel, focusRange }: DiffViewProps) {
const host = useRef<HTMLDivElement>(null);
const merge = useRef<MergeView | null>(null);
useEffect(() => {
if (host.current === null) {
return undefined;
}
const view = new MergeView({
parent: host.current,
a: { doc: before, extensions: readOnlyExtensions },
b: { doc: after, extensions: readOnlyExtensions },
orientation: "a-b",
gutter: true,
highlightChanges: true,
});
merge.current = view;
return () => {
view.destroy();
merge.current = null;
};
}, [before, after]);
// 必须在对应阶段文本重建完成后再次聚焦,不能只依赖 focusRange。
useEffect(() => {
const view = merge.current;
if (view === null || focusRange === null || focusRange === undefined) {
return;
}
const anchor = Math.min(Math.max(focusRange.start, 0), view.a.state.doc.length);
const head = Math.min(Math.max(focusRange.end, anchor), view.a.state.doc.length);
view.a.dispatch({
selection: EditorSelection.range(anchor, head),
effects: EditorView.scrollIntoView(anchor, { y: "center" }),
});
view.a.focus();
}, [focusRange, before, after]);
return (
<section className="diff-shell" aria-label={`${beforeLabel}${afterLabel}对比`}>
<div className="diff-labels" aria-hidden="true">
<span>{beforeLabel}</span>
<span>{afterLabel}</span>
</div>
<div className="diff-host" ref={host} />
</section>
);
}