101 lines
2.7 KiB
TypeScript
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>
|
|
);
|
|
}
|