import type { ReactElement } from 'react' import type { BlockTreeState } from './BlockTree' import type { NameMap, VersionNames } from './displayNames' import type { DocumentDiff, DocumentMessage, Excluded, Reveal } from './types' import type { OccurrencePage } from './useOccurrences' import type { ViewState } from './viewState' import { TriangleAlert } from 'lucide-react' import { useCallback, useEffect, useMemo, useRef, useState } from 'react' import { BlockLevel } from './BlockTree' import { DiffBar, NewFileBanner } from './DiffBar' import { changeStops, diffTree, foldUnchanged, openedByDiff } from './diffView' import { acceptNames, DisplayNameContext, documentNames } from './displayNames' import { coveredByEditing, startEditing } from './editing' import { applyFilter, excludedBy, keptWhileEditing } from './filter' import { FilterBar } from './FilterBar' import { OverviewRuler } from './OverviewRuler' import { useChangeNav } from './useChangeNav' import { useDefinitionLinks } from './useDefinitionLinks' import { HighlightContext, useOccurrences } from './useOccurrences' import { useQa } from './useQa' import { useReveal } from './useReveal' import { useScrollPosition } from './useScrollPosition' import { collapsedIds } from './viewState' import { onMessage, postMessage, viewState } from './vscode' type BlockModes = Pick const NONE: ReadonlySet = new Set() const NO_NAMES: NameMap = new Map() function savedBlockModes(): BlockModes { const { editing, folds } = viewState.get() return { editing, folds } } export function App(): ReactElement | null { // 先于下面发 ready 的那个 effect 挂上收消息:ready 后扩展端马上发问答 const qa = useQa() const [doc, setDoc] = useState(null) const [view, setView] = useState(savedBlockModes) const [focusId, setFocusId] = useState(null) /** 等着滚过去的位置:新预览第一份文档带的,或已开着的预览收到的 reveal */ const [reveal, setReveal] = useState(null) /** 扩展端发来的块筛选(整个工作区共用);本地不改,改了发给扩展端、等它发回来 */ const [excluded, setExcluded] = useState({}) /** 跳到定义时临时露出来的块(目标和它的各层祖先),筛选一变、文档一刷新就回到筛选状态 */ const [keep, setKeep] = useState>(NONE) /** 页面上这个文档版本的中文名:文档带来的,扩展端查到了再换 */ const [names, setNames] = useState() /** 这份文件相比对比基准改了什么:侧栏开着「未提交的更改」或审查工单时扩展端算好了补来,不比时是 null */ const [diff, setDiff] = useState(null) /** 收到消息时读眼下哪些块在编辑(消息回调只在挂载时注册一次) */ const editing = useRef(view.editing) // 解析失败的文档没有块,不标差异;删掉的块按块筛选同样藏 const changes = useMemo(() => doc && doc.error === undefined ? diffTree(doc.blocks, diff, doc.version, removed => !excludedBy(removed.block, excluded)) : undefined, [doc, diff, excluded]) const collapsed = useMemo(() => collapsedIds(doc?.blocks ?? [], view.folds, openedByDiff(changes)), [doc, view.folds, changes]) const filter = useMemo(() => applyFilter(doc?.blocks ?? [], excluded, keep), [doc, excluded, keep]) const covered = useMemo(() => coveredByEditing(doc?.blocks ?? [], new Set(view.editing)), [doc, view.editing]) /** 「只看改动」开着(整个工作区共用,扩展端记着);本地不改,改了发给扩展端、等它发回来 */ const [onlyChanges, setOnlyChanges] = useState(false) /** 「只看改动」时点开过的几段没变的块(按第一块的 id),筛选一变就重新折 */ const [openedRuns, setOpenedRuns] = useState>(NONE) const folded = useMemo(() => doc && changes && onlyChanges ? foldUnchanged(doc.blocks, changes, block => !filter.hidden.has(block.id) && !covered.has(block.id), openedRuns, keep) : undefined, [doc, changes, onlyChanges, filter, covered, openedRuns, keep]) /** 页面上没画出来的块:被筛掉的,和「只看改动」折起来的 */ const unshown = useMemo(() => folded ? new Set([...filter.hidden, ...folded.folded]) : filter.hidden, [filter, folded]) const stops = useMemo(() => doc && changes ? changeStops(doc.blocks, changes, filter.hidden) : [], [doc, changes, filter]) /** 记下这几块、属性行、子行折叠还是展开,从此不再按默认 */ const setFolded = useCallback((ids: string[], folded: boolean) => setView(prev => ({ ...prev, folds: { ...prev.folds, ...Object.fromEntries(ids.map(id => [id, folded])) }, })), []) const expand = useCallback((ids: string[]) => setFolded(ids, false), [setFolded]) const show = useCallback((ids: string[]) => setKeep(prev => new Set([...prev, ...ids])), []) const revealed = useCallback(() => setReveal(null), []) useScrollPosition(doc) useReveal(doc, reveal, collapsed, unshown, covered, show, expand, revealed) useDefinitionLinks(doc?.version) const nav = useChangeNav(stops, collapsed, expand) const occurrencePage = useMemo((): OccurrencePage | undefined => doc ? { blocks: doc.blocks, skip: id => unshown.has(id) || covered.has(id) || view.editing.includes(id), collapsed, expand, } : undefined, [doc, unshown, covered, view.editing, collapsed, expand]) const highlight = useOccurrences(doc?.version, doc?.translatorId, occurrencePage) useEffect(() => { const off = onMessage((msg) => { switch (msg.type) { case 'document': setDoc(msg) setNames(documentNames(msg.version, msg.names)) setKeep(prev => keptWhileEditing(prev, editing.current)) if (msg.reveal) setReveal(msg.reveal) return case 'reveal': setReveal({ line: msg.line, definition: msg.definition }) return case 'displayNames': setNames(prev => acceptNames(prev, msg.version, msg.names)) return case 'filter': setExcluded(msg.excluded) setOnlyChanges(msg.onlyChanges) setKeep(NONE) setOpenedRuns(NONE) return case 'diff': setDiff(msg.diff) } }) postMessage({ type: 'ready' }) return off }, []) useEffect(() => { viewState.update(view) editing.current = view.editing }, [view]) const onFocused = useCallback(() => setFocusId(null), []) const kindNames = useMemo(() => new Map(doc?.attributes.find(attribute => attribute.id === 'kind')?.values.map(value => [value.id, value.name])), [doc]) const kindName = useCallback((kind: string) => kindNames.get(kind) ?? kind, [kindNames]) const tree = useMemo((): BlockTreeState | null => doc && { version: doc.version, editing: new Set(view.editing), collapsed, hidden: filter.hidden, temporary: filter.temporary, covered, kindName, focusId, onFocused, onToggleEdit: (id) => { setFocusId(view.editing.includes(id) ? null : id) setView(prev => ({ ...prev, editing: prev.editing.includes(id) ? prev.editing.filter(other => other !== id) : startEditing(doc.blocks, prev.editing, id), })) }, onToggleFold: id => setFolded([id], !collapsed.has(id)), onSetFolds: folds => setView(prev => ({ ...prev, folds: { ...prev.folds, ...folds } })), ...(qa.state.available && { qa }), ...(changes && { diff: changes }), ...(folded && { folded }), onOpenRun: id => setOpenedRuns(prev => new Set([...prev, id])), }, [doc, view, collapsed, filter, covered, kindName, focusId, onFocused, setFolded, qa, changes, folded]) if (!doc || !tree) return null return ( postMessage({ type: 'setFilter', excluded: next })} translators={doc.translators} translatorId={doc.translatorId} onSelectTranslator={id => postMessage({ type: 'selectTranslator', id })} below={diff && doc.error === undefined && ( postMessage({ type: 'setOnlyChanges', on: !onlyChanges })} canMove={stops.length > 0} onMove={nav.move} /> )} /> {changes && diff && }
{diff?.newFile !== undefined && doc.error === undefined && } {doc.error !== undefined && (
解析失败: {doc.error}
点编辑器右上角的「编辑」查看源码。
)} {doc.blocks.length === 0 ? doc.error === undefined &&
这个文件里没有代码。
: doc.blocks.some(block => !filter.hidden.has(block.id)) ? :
所有块都被筛选隐藏了。
}
) }