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(null); const merge = useRef(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 (
); }