efc1313d87
- DiffView 聚焦 effect 依赖补上 before/after:stage 文本到达、 MergeView 重建后重新应用选区和滚动,跨组件点击不再停在顶部; - 非可编辑面板加 drawSelection,聚焦范围在浏览器中可见; - 移除 collapseUnchanged,评审始终可读完整未改动段落; - 滚动容器移到 .cm-mergeView 外层,编辑器高度自适应内容; - 新增 3 项 Vitest 覆盖折叠移除与聚焦时序,共 9 项通过; - README 与 explanation 同步当前检查数量。
102 lines
2.8 KiB
TypeScript
102 lines
2.8 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]);
|
|
|
|
// 切换组件时 stage 文本先到、旧视图上的提前滚动会被重建覆盖,所以聚焦
|
|
// 必须等 MergeView 换成目标文本之后再执行。
|
|
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>
|
|
);
|
|
}
|