import type { ReactElement } from 'react' import type { BlockTreeHandle } from './BlockTree' import type { OpenCall } from './pageState' import type { TextMarks } from './SegmentText' import type { BlockView, DocumentMessage } from './types' import type { OccurrencePage } from './useOccurrences' import { TriangleAlert } from 'lucide-react' import { useCallback, useEffect, useMemo, useRef, useState } from 'react' import { AskThread, FEATURE_THREAD } from './AskThread' import { BlockTree } from './BlockTree' import { CallTreeDialog } from './CallTreeDialog' import { applyCodeColors } from './codeColors' import { DefinitionPeekPopup } from './DefinitionPeekPopup' import { DiffBar, NewFileBanner } from './DiffBar' import { nameMap } from './displayNames' import { startEditing } from './editing' import { keptWhileEditing } from './filter' import { FilterBar } from './FilterBar' import { OverviewRuler } from './OverviewRuler' import { createPageStore } from './pageState' import { chatOf, PATH_ASK_ANCHOR, sameChat } from './qa' import { TextMarksContext } from './SegmentText' import { shareUnchanged } from './sharing' import { StickyLines } from './StickyLines' import { createStore, useStore } from './store' import { useChangeNav } from './useChangeNav' import { useDefinitionLinks } from './useDefinitionLinks' import { useDefinitionPeek } from './useDefinitionPeek' import { useOccurrences } from './useOccurrences' import { useQa } from './useQa' import { useRepoImages } from './useRepoImages' import { useRepoLinks } from './useRepoLinks' import { useReveal } from './useReveal' import { useRewrite } from './useRewrite' import { useScrollPosition } from './useScrollPosition' import { useStickySettings } from './useStickyScroll' import { onMessage, postMessage, viewState } from './vscode' const NONE: ReadonlySet = new Set() /** 打开 doc 里 anchor 那块的调用树弹框,先是「正在查」,等扩展端发树来;这块不在了、或者它看不了调用树时是 undefined。 */ function openingCall(doc: DocumentMessage, anchor: string, entry: number): OpenCall | undefined { const block = blockAt(doc.blocks, anchor) if (!block?.callTree) return undefined return { anchor, version: doc.version, entry, status: 'loading', name: '', where: '', kind: block.kind, ...(block.badge && { badge: block.badge }), trees: [] } } function blockAt(blocks: readonly BlockView[], anchor: string): BlockView | undefined { for (const block of blocks) { if (block.anchor === anchor) return block const inner = blockAt(block.children, anchor) if (inner) return inner } return undefined } /** * 代码阅读预览页。页面状态放在一份 store 里(createPageStore),扩展端的消息、用户的点击都改它; * 块树的卡片各自订阅自己那部分(见 BlockTree),一条消息只重画受影响的卡片;这一层重画时,块树顶层各块照 memo 跳过。 */ export function App(): ReactElement | null { // 先于下面发 ready 的那个 effect 挂上收消息:ready 后扩展端马上发问答 const qa = useQa() /** 侧栏「问 AI」要聚焦顶部输入框时加一 */ const [pathFocus, setPathFocus] = useState(0) /** 按注释规范重写 */ const rewrite = useRewrite() const [page] = useState(() => createPageStore(viewState.get())) /** 页面上文字的标记:这个文档版本的中文名(文档带来的,扩展端查到了再换)和单击高亮的名字;各段文字按自己那几段订阅 */ const [marks] = useState(() => createStore({ names: new Map() })) /** 页面重新加载前开着的调用树弹框:第一份文档到了,那块还在就重新打开;到之前记着的不动 */ const restoreCall = useRef(viewState.get().callTree) /** 块树所在的页面内容:钉住卡片时在这里面量 */ const content = useRef(null) const { doc, editing, folds, reveal, excluded, diff, onlyChanges, openCall, changes, collapsed, filter, covered, unshown, stops } = useStore(page, current => current) const pathChat = useStore(qa.store, state => chatOf(state, PATH_ASK_ANCHOR), sameChat) const qaAvailable = useStore(qa.store, state => state.available) const qaSubject = useStore(qa.store, state => state.subject) /** 记下这几块、属性行、子行折叠还是展开,从此不再按默认 */ const setFolded = useCallback((ids: string[], folded: boolean) => page.update(current => ({ folds: { ...current.folds, ...Object.fromEntries(ids.map(id => [id, folded])) }, })), [page]) const expand = useCallback((ids: string[]) => setFolded(ids, false), [setFolded]) const show = useCallback((ids: string[]) => page.update(current => ({ keep: new Set([...current.keep, ...ids]) })), [page]) const revealed = useCallback(() => page.update({ reveal: null }), [page]) useScrollPosition(doc) useReveal(doc, reveal, collapsed, unshown, covered, show, expand, revealed) useDefinitionLinks(doc?.version) const peek = useDefinitionPeek(doc?.version) const sticky = useStickySettings() useRepoLinks() useRepoImages() const nav = useChangeNav(stops, collapsed, expand, sticky) const occurrencePage = useMemo((): OccurrencePage | undefined => doc ? { blocks: doc.blocks, skip: id => unshown.has(id) || covered.has(id) || editing.includes(id), collapsed, expand, } : undefined, [doc, unshown, covered, editing, collapsed, expand]) const occurrences = useOccurrences(doc?.version, doc?.translatorId, occurrencePage, marks) /** 块树:状态是页面 store 里的 tree,动作改页面 store;整页一份,卡片拿到的回调一直是同一个 */ const [tree] = useState((): BlockTreeHandle => ({ state: { get: () => page.get().tree, subscribe: page.subscribe }, onToggleFold: id => page.update(current => ({ folds: { ...current.folds, [id]: !current.collapsed.has(id) } })), onSetFolds: next => page.update(current => ({ folds: { ...current.folds, ...next } })), onOpenRun: id => page.update(current => ({ openedRuns: new Set([...current.openedRuns, id]) })), edit: { onToggleEdit: id => page.update((current) => { const leaving = current.editing.includes(id) return { focusId: leaving ? null : id, editing: leaving ? current.editing.filter(other => other !== id) : startEditing(current.doc?.blocks ?? [], current.editing, id), } }), onFocused: () => page.update({ focusId: null }), }, onOpenCall: (anchor) => { const current = page.get().doc const opening = current && openingCall(current, anchor, 0) if (!opening) return page.update({ openCall: opening }) postMessage({ type: 'openCallTree', version: opening.version, anchor }) }, qa, rewrite, })) useEffect(() => { const off = onMessage((msg) => { switch (msg.type) { case 'document': { const restoring = restoreCall.current restoreCall.current = undefined // 和上一份内容一样的块还是原来的对象:没变的卡片不重画 const next = shareUnchanged(page.get().doc, msg) const reopened = restoring && openingCall(next, restoring.anchor, restoring.entry) marks.set(current => ({ ...current, names: nameMap(msg.names) })) page.update(current => ({ doc: next, keep: keptWhileEditing(current.keep, current.editing), callCounts: current.callCounts?.version === msg.version ? current.callCounts : undefined, openCall: current.openCall?.version === msg.version ? current.openCall : reopened ?? null, ...(msg.reveal && { reveal: msg.reveal }), })) if (reopened) postMessage({ type: 'openCallTree', version: msg.version, anchor: reopened.anchor }) else if (restoring) viewState.update({ callTree: undefined }) return } case 'callTreeCounts': if (msg.version !== page.get().doc?.version) return page.update({ callCounts: { version: msg.version, counts: msg.counts } }) return case 'callTree': if (msg.version !== page.get().doc?.version) return page.update(({ openCall: prev }) => ({ openCall: !prev || prev.anchor !== msg.anchor || prev.version !== msg.version ? prev : { ...prev, status: 'ready', name: msg.name, where: msg.where, trees: msg.trees, contractMiss: msg.contractMiss }, })) return case 'reveal': page.update({ reveal: { line: msg.line, definition: msg.definition } }) return case 'displayNames': // 查到的是页面上这个版本的才换上,迟到的旧版本的不理 if (msg.version === page.get().doc?.version) marks.set(current => ({ ...current, names: nameMap(msg.names) })) return case 'filter': page.update({ excluded: msg.excluded, onlyChanges: msg.onlyChanges, keep: NONE, openedRuns: NONE }) return case 'codeColors': applyCodeColors(document.body.style, msg) return case 'diff': page.update(current => ({ diff: shareUnchanged(current.diff, msg.diff) })) return case 'openedFrom': viewState.update({ openedFrom: msg.from }) return case 'focusPathAsk': setPathFocus(n => n + 1) } }) // 重启 VS Code 后恢复的标签页,扩展端不知道它是从哪儿打开的,按页面记着的 const { openedFrom } = viewState.get() postMessage({ type: 'ready', ...(openedFrom && { openedFrom }) }) return off }, [page, marks]) useEffect(() => { viewState.update({ editing, folds }) }, [editing, folds]) useEffect(() => { if (restoreCall.current) return viewState.update({ callTree: openCall ? { anchor: openCall.anchor, entry: openCall.entry } : undefined }) }, [openCall]) if (!doc) 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 && }
{qaAvailable && (
qa.onAsk(PATH_ASK_ANCHOR, question, images)} onStop={qa.onStop} onDelete={entryId => qa.onDelete(PATH_ASK_ANCHOR, entryId)} onClose={() => undefined} />
)} {diff?.newFile !== undefined && doc.error === undefined && } {doc.error !== undefined && (
解析失败: {doc.error}
点编辑器右上角的「编辑」查看源码。
)} {doc.blocks.length === 0 ? doc.error === undefined && qaSubject === 'file' &&
这个文件里没有代码。
: doc.blocks.some(block => !filter.hidden.has(block.id)) ? :
所有块都被筛选隐藏了。
}
{sticky?.enabled && } {openCall && ( page.update(({ openCall: prev }) => ({ openCall: prev && { ...prev, entry } }))} onClose={() => page.update({ openCall: null })} onJump={(jump) => { page.update({ openCall: null }) postMessage({ type: 'goToCallNode', ...jump }) }} /> )}
) }