⚡ perf(vscode): 代码阅读预览按卡片订阅状态:中文名、调用树棵数、单击高亮、问答一条消息只重画受影响的卡片和文字,新文档里内容没变的块沿用原来的对象不重画,没展开又没说过的块不画问答区
This commit is contained in:
1 parent
6e2c541558
commit
0ad4748cc6
27 files changed
+1359
-620
No files matched your search
@@ -1,29 +1,32 @@
|
||||
import type { ReactElement } from 'react'
|
||||
import type { BlockTreeState } from './BlockTree'
|
||||
import type { NameMap, VersionNames } from './displayNames'
|
||||
import type { BlockView, CallTree, ContractMiss, DocumentDiff, DocumentMessage, Excluded, KindBadge, Reveal } from './types'
|
||||
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 type { ViewState } from './viewState'
|
||||
import { TriangleAlert } from 'lucide-react'
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { AskThread, FEATURE_THREAD } from './AskThread'
|
||||
import { BlockLevel } from './BlockTree'
|
||||
import { BlockTree } from './BlockTree'
|
||||
import { CallTreeDialog } from './CallTreeDialog'
|
||||
import { applyCodeColors } from './codeColors'
|
||||
import { DefinitionPeekPopup } from './DefinitionPeekPopup'
|
||||
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 { nameMap } from './displayNames'
|
||||
import { startEditing } from './editing'
|
||||
import { keptWhileEditing } from './filter'
|
||||
import { FilterBar } from './FilterBar'
|
||||
import { OverviewRuler } from './OverviewRuler'
|
||||
import { asksOf, historyOf, PATH_ASK_ANCHOR } from './qa'
|
||||
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 { HighlightContext, useOccurrences } from './useOccurrences'
|
||||
import { useOccurrences } from './useOccurrences'
|
||||
import { useQa } from './useQa'
|
||||
import { useRepoImages } from './useRepoImages'
|
||||
import { useRepoLinks } from './useRepoLinks'
|
||||
@@ -31,32 +34,9 @@ import { useReveal } from './useReveal'
|
||||
import { useRewrite } from './useRewrite'
|
||||
import { useScrollPosition } from './useScrollPosition'
|
||||
import { useStickySettings } from './useStickyScroll'
|
||||
import { collapsedIds } from './viewState'
|
||||
import { onMessage, postMessage, viewState } from './vscode'
|
||||
|
||||
type BlockModes = Pick<ViewState, 'editing' | 'folds'>
|
||||
|
||||
const NONE: ReadonlySet<string> = new Set()
|
||||
const NO_NAMES: NameMap = new Map()
|
||||
|
||||
function savedBlockModes(): BlockModes {
|
||||
const { editing, folds } = viewState.get()
|
||||
return { editing, folds }
|
||||
}
|
||||
|
||||
/** 开着的调用树弹框:哪一版文档的哪一块、选中第几个入口,扩展端发来的树 */
|
||||
interface OpenCall {
|
||||
anchor: string
|
||||
version: number
|
||||
entry: number
|
||||
status: 'loading' | 'ready'
|
||||
name: string
|
||||
where: string
|
||||
kind: string
|
||||
badge?: KindBadge
|
||||
trees: CallTree[]
|
||||
contractMiss?: ContractMiss
|
||||
}
|
||||
|
||||
/** 打开 doc 里 anchor 那块的调用树弹框,先是「正在查」,等扩展端发树来;这块不在了、或者它看不了调用树时是 undefined。 */
|
||||
function openingCall(doc: DocumentMessage, anchor: string, entry: number): OpenCall | undefined {
|
||||
@@ -77,6 +57,10 @@ function blockAt(blocks: readonly BlockView[], anchor: string): BlockView | unde
|
||||
return undefined
|
||||
}
|
||||
|
||||
/**
|
||||
* 代码阅读预览页。页面状态放在一份 store 里(createPageStore),扩展端的消息、用户的点击都改它;
|
||||
* 块树的卡片各自订阅自己那部分(见 BlockTree),一条消息只重画受影响的卡片;这一层重画时,块树顶层各块照 memo 跳过。
|
||||
*/
|
||||
export function App(): ReactElement | null {
|
||||
// 先于下面发 ready 的那个 effect 挂上收消息:ready 后扩展端马上发问答
|
||||
const qa = useQa()
|
||||
@@ -84,54 +68,25 @@ export function App(): ReactElement | null {
|
||||
const [pathFocus, setPathFocus] = useState(0)
|
||||
/** 按注释规范重写 */
|
||||
const rewrite = useRewrite()
|
||||
const [doc, setDoc] = useState<DocumentMessage | null>(null)
|
||||
const [view, setView] = useState<BlockModes>(savedBlockModes)
|
||||
const [focusId, setFocusId] = useState<string | null>(null)
|
||||
/** 等着滚过去的位置:新预览第一份文档带的,或已开着的预览收到的 reveal */
|
||||
const [reveal, setReveal] = useState<Reveal | null>(null)
|
||||
/** 扩展端发来的块筛选(整个工作区共用);本地不改,改了发给扩展端、等它发回来 */
|
||||
const [excluded, setExcluded] = useState<Excluded>({})
|
||||
/** 跳到定义时临时露出来的块(目标和它的各层祖先),筛选一变、文档一刷新就回到筛选状态 */
|
||||
const [keep, setKeep] = useState<ReadonlySet<string>>(NONE)
|
||||
/** 页面上这个文档版本的中文名:文档带来的,扩展端查到了再换 */
|
||||
const [names, setNames] = useState<VersionNames | undefined>()
|
||||
/** 这份文件相比对比基准改了什么:审查工作副本里的、从「未提交的更改」点开的有改动的文件扩展端算好了补来,不比时是 null */
|
||||
const [diff, setDiff] = useState<DocumentDiff | null>(null)
|
||||
/** 收到消息时读眼下哪些块在编辑(消息回调只在挂载时注册一次) */
|
||||
const editing = useRef(view.editing)
|
||||
/** 页面上这份文档的版本:调用树的回答不是这一版就丢掉 */
|
||||
const docVersion = useRef<number | undefined>(undefined)
|
||||
const [callCounts, setCallCounts] = useState<{ version: number, counts: Record<string, number> }>()
|
||||
const [openCall, setOpenCall] = useState<OpenCall | null>(null)
|
||||
const [page] = useState(() => createPageStore(viewState.get()))
|
||||
/** 页面上文字的标记:这个文档版本的中文名(文档带来的,扩展端查到了再换)和单击高亮的名字;各段文字按自己那几段订阅 */
|
||||
const [marks] = useState(() => createStore<TextMarks>({ names: new Map() }))
|
||||
/** 页面重新加载前开着的调用树弹框:第一份文档到了,那块还在就重新打开;到之前记着的不动 */
|
||||
const restoreCall = useRef(viewState.get().callTree)
|
||||
/** 块树所在的页面内容:钉住卡片时在这里面量 */
|
||||
const content = useRef<HTMLDivElement>(null)
|
||||
// 解析失败的文档没有块,不标差异;删掉的块按块筛选同样藏
|
||||
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<ReadonlySet<string>>(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 { 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) => setView(prev => ({
|
||||
...prev,
|
||||
folds: { ...prev.folds, ...Object.fromEntries(ids.map(id => [id, folded])) },
|
||||
})), [])
|
||||
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[]) => setKeep(prev => new Set([...prev, ...ids])), [])
|
||||
const revealed = useCallback(() => setReveal(null), [])
|
||||
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)
|
||||
@@ -143,12 +98,40 @@ export function App(): ReactElement | null {
|
||||
const occurrencePage = useMemo((): OccurrencePage | undefined => doc
|
||||
? {
|
||||
blocks: doc.blocks,
|
||||
skip: id => unshown.has(id) || covered.has(id) || view.editing.includes(id),
|
||||
skip: id => unshown.has(id) || covered.has(id) || editing.includes(id),
|
||||
collapsed,
|
||||
expand,
|
||||
}
|
||||
: undefined, [doc, unshown, covered, view.editing, collapsed, expand])
|
||||
const { highlight, moveOccurrence } = useOccurrences(doc?.version, doc?.translatorId, occurrencePage)
|
||||
: 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) => {
|
||||
@@ -156,52 +139,53 @@ export function App(): ReactElement | null {
|
||||
case 'document': {
|
||||
const restoring = restoreCall.current
|
||||
restoreCall.current = undefined
|
||||
const reopened = restoring && openingCall(msg, restoring.anchor, restoring.entry)
|
||||
docVersion.current = msg.version
|
||||
setDoc(msg)
|
||||
setNames(documentNames(msg.version, msg.names))
|
||||
setKeep(prev => keptWhileEditing(prev, editing.current))
|
||||
setCallCounts(prev => prev?.version === msg.version ? prev : undefined)
|
||||
setOpenCall(prev => prev?.version === msg.version ? prev : reopened ?? null)
|
||||
// 和上一份内容一样的块还是原来的对象:没变的卡片不重画
|
||||
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 })
|
||||
if (msg.reveal)
|
||||
setReveal(msg.reveal)
|
||||
return
|
||||
}
|
||||
case 'callTreeCounts':
|
||||
if (msg.version !== docVersion.current)
|
||||
if (msg.version !== page.get().doc?.version)
|
||||
return
|
||||
setCallCounts({ version: msg.version, counts: msg.counts })
|
||||
page.update({ callCounts: { version: msg.version, counts: msg.counts } })
|
||||
return
|
||||
case 'callTree':
|
||||
if (msg.version !== docVersion.current)
|
||||
if (msg.version !== page.get().doc?.version)
|
||||
return
|
||||
setOpenCall((prev) => {
|
||||
if (!prev || prev.anchor !== msg.anchor || prev.version !== msg.version)
|
||||
return prev
|
||||
return { ...prev, status: 'ready', name: msg.name, where: msg.where, trees: msg.trees, contractMiss: msg.contractMiss }
|
||||
})
|
||||
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':
|
||||
setReveal({ line: msg.line, definition: msg.definition })
|
||||
page.update({ reveal: { line: msg.line, definition: msg.definition } })
|
||||
return
|
||||
case 'displayNames':
|
||||
setNames(prev => acceptNames(prev, msg.version, msg.names))
|
||||
// 查到的是页面上这个版本的才换上,迟到的旧版本的不理
|
||||
if (msg.version === page.get().doc?.version)
|
||||
marks.set(current => ({ ...current, names: nameMap(msg.names) }))
|
||||
return
|
||||
case 'filter':
|
||||
setExcluded(msg.excluded)
|
||||
setOnlyChanges(msg.onlyChanges)
|
||||
setKeep(NONE)
|
||||
setOpenedRuns(NONE)
|
||||
page.update({ excluded: msg.excluded, onlyChanges: msg.onlyChanges, keep: NONE, openedRuns: NONE })
|
||||
return
|
||||
case 'codeColors':
|
||||
applyCodeColors(document.body.style, msg)
|
||||
return
|
||||
case 'diff':
|
||||
setDiff(msg.diff)
|
||||
page.update(current => ({ diff: shareUnchanged(current.diff, msg.diff) }))
|
||||
return
|
||||
case 'openedFrom':
|
||||
viewState.update({ openedFrom: msg.from })
|
||||
@@ -214,12 +198,11 @@ export function App(): ReactElement | null {
|
||||
const { openedFrom } = viewState.get()
|
||||
postMessage({ type: 'ready', ...(openedFrom && { openedFrom }) })
|
||||
return off
|
||||
}, [])
|
||||
}, [page, marks])
|
||||
|
||||
useEffect(() => {
|
||||
viewState.update(view)
|
||||
editing.current = view.editing
|
||||
}, [view])
|
||||
viewState.update({ editing, folds })
|
||||
}, [editing, folds])
|
||||
|
||||
useEffect(() => {
|
||||
if (restoreCall.current)
|
||||
@@ -227,137 +210,92 @@ export function App(): ReactElement | null {
|
||||
viewState.update({ callTree: openCall ? { anchor: openCall.anchor, entry: openCall.entry } : undefined })
|
||||
}, [openCall])
|
||||
|
||||
const onFocused = useCallback(() => setFocusId(null), [])
|
||||
const onOpenCall = useCallback((anchor: string) => {
|
||||
const opening = doc && openingCall(doc, anchor, 0)
|
||||
if (!opening)
|
||||
return
|
||||
setOpenCall(opening)
|
||||
postMessage({ type: 'openCallTree', version: opening.version, anchor })
|
||||
}, [doc])
|
||||
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 && {
|
||||
edit: {
|
||||
version: doc.version,
|
||||
editing: new Set(view.editing),
|
||||
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),
|
||||
}))
|
||||
},
|
||||
},
|
||||
collapsed,
|
||||
hidden: filter.hidden,
|
||||
temporary: filter.temporary,
|
||||
covered,
|
||||
kindName,
|
||||
onToggleFold: id => setFolded([id], !collapsed.has(id)),
|
||||
onSetFolds: folds => setView(prev => ({ ...prev, folds: { ...prev.folds, ...folds } })),
|
||||
...(qa.state.available && { qa }),
|
||||
...(rewrite.state.available && { rewrite }),
|
||||
...(changes && { diff: changes }),
|
||||
...(folded && { folded }),
|
||||
onOpenRun: id => setOpenedRuns(prev => new Set([...prev, id])),
|
||||
callTree: {
|
||||
counts: callCounts?.version === doc.version ? new Map(Object.entries(callCounts.counts)) : new Map(),
|
||||
...(openCall?.version === doc.version && { openAnchor: openCall.anchor }),
|
||||
onOpen: onOpenCall,
|
||||
},
|
||||
}, [doc, view, collapsed, filter, covered, kindName, focusId, onFocused, setFolded, rewrite, qa, changes, folded, callCounts, openCall, onOpenCall])
|
||||
|
||||
if (!doc || !tree)
|
||||
if (!doc)
|
||||
return null
|
||||
|
||||
return (
|
||||
<HighlightContext value={highlight}>
|
||||
<DisplayNameContext value={names?.names ?? NO_NAMES}>
|
||||
<FilterBar
|
||||
attributes={doc.attributes}
|
||||
excluded={excluded}
|
||||
view={filter}
|
||||
highlight={highlight}
|
||||
onMoveOccurrence={moveOccurrence}
|
||||
onChange={next => postMessage({ type: 'setFilter', excluded: next })}
|
||||
translators={doc.translators}
|
||||
translatorId={doc.translatorId}
|
||||
onSelectTranslator={id => postMessage({ type: 'selectTranslator', id })}
|
||||
below={diff && doc.error === undefined && (
|
||||
<DiffBar
|
||||
diff={diff}
|
||||
onlyChanges={onlyChanges}
|
||||
onToggleOnly={() => postMessage({ type: 'setOnlyChanges', on: !onlyChanges })}
|
||||
canMove={stops.length > 0}
|
||||
onMove={nav.move}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
{changes && diff && <OverviewRuler stops={stops} lineCount={diff.lineCount} onPick={nav.goTo} />}
|
||||
<div ref={content} className="mx-auto box-content max-w-[880px] px-6 py-5">
|
||||
{qa.state.available && (
|
||||
<div className="mb-4">
|
||||
<AskThread
|
||||
history={historyOf(qa.state, PATH_ASK_ANCHOR)}
|
||||
asks={asksOf(qa.state, PATH_ASK_ANCHOR)}
|
||||
open
|
||||
hint={qa.state.subject === 'folder' ? '这个文件夹' : '这个文件'}
|
||||
{...(qa.state.subject === 'feature' && { wording: FEATURE_THREAD })}
|
||||
autoFocus={false}
|
||||
focusToken={pathFocus}
|
||||
closable={false}
|
||||
profiles={qa.state.profiles}
|
||||
onSelectProfile={qa.onSelectProfile}
|
||||
onAsk={(question, images) => qa.onAsk(PATH_ASK_ANCHOR, question, images)}
|
||||
onStop={qa.onStop}
|
||||
onDelete={entryId => qa.onDelete(PATH_ASK_ANCHOR, entryId)}
|
||||
onClose={() => undefined}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{diff?.newFile !== undefined && doc.error === undefined && <NewFileBanner detail={diff.newFile} />}
|
||||
{doc.error !== undefined && (
|
||||
<div className="mb-3 flex items-start gap-2 rounded-lg border border-[var(--vscode-inputValidation-errorBorder)] bg-[var(--vscode-inputValidation-errorBackground)] px-3 py-2">
|
||||
<TriangleAlert size={16} strokeWidth={1.8} className="mt-px shrink-0 text-[var(--vscode-errorForeground)]" />
|
||||
<div className="min-w-0">
|
||||
<div className="break-words">
|
||||
解析失败:
|
||||
{doc.error}
|
||||
</div>
|
||||
<div className="mt-0.5 text-xs text-[var(--vscode-descriptionForeground)]">点编辑器右上角的「编辑」查看源码。</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{doc.blocks.length === 0
|
||||
? doc.error === undefined && qa.state.subject === 'file' && <div className="text-[var(--vscode-descriptionForeground)]">这个文件里没有代码。</div>
|
||||
: doc.blocks.some(block => !filter.hidden.has(block.id))
|
||||
? <BlockLevel blocks={doc.blocks} tree={tree} />
|
||||
: <div className="text-[var(--vscode-descriptionForeground)]">所有块都被筛选隐藏了。</div>}
|
||||
</div>
|
||||
{sticky?.enabled && <StickyLines content={content} blocks={doc.blocks} settings={sticky} />}
|
||||
<DefinitionPeekPopup shown={peek.shown} onJump={peek.jump} />
|
||||
{openCall && (
|
||||
<CallTreeDialog
|
||||
kind={openCall.kind}
|
||||
badge={openCall.badge}
|
||||
name={openCall.name}
|
||||
where={openCall.where}
|
||||
status={openCall.status === 'loading' ? 'loading' : openCall.trees.length === 0 ? 'empty' : 'ready'}
|
||||
trees={openCall.trees}
|
||||
selected={openCall.entry}
|
||||
contractMiss={openCall.contractMiss}
|
||||
onSelect={entry => setOpenCall(prev => prev && { ...prev, entry })}
|
||||
onClose={() => setOpenCall(null)}
|
||||
onJump={(jump) => {
|
||||
setOpenCall(null)
|
||||
postMessage({ type: 'goToCallNode', ...jump })
|
||||
}}
|
||||
<TextMarksContext value={marks}>
|
||||
<FilterBar
|
||||
attributes={doc.attributes}
|
||||
excluded={excluded}
|
||||
view={filter}
|
||||
highlight={occurrences.highlight}
|
||||
onMoveOccurrence={occurrences.moveOccurrence}
|
||||
onChange={next => postMessage({ type: 'setFilter', excluded: next })}
|
||||
translators={doc.translators}
|
||||
translatorId={doc.translatorId}
|
||||
onSelectTranslator={id => postMessage({ type: 'selectTranslator', id })}
|
||||
below={diff && doc.error === undefined && (
|
||||
<DiffBar
|
||||
diff={diff}
|
||||
onlyChanges={onlyChanges}
|
||||
onToggleOnly={() => postMessage({ type: 'setOnlyChanges', on: !onlyChanges })}
|
||||
canMove={stops.length > 0}
|
||||
onMove={nav.move}
|
||||
/>
|
||||
)}
|
||||
</DisplayNameContext>
|
||||
</HighlightContext>
|
||||
/>
|
||||
{changes && diff && <OverviewRuler stops={stops} lineCount={diff.lineCount} onPick={nav.goTo} />}
|
||||
<div ref={content} className="mx-auto box-content max-w-[880px] px-6 py-5">
|
||||
{qaAvailable && (
|
||||
<div className="mb-4">
|
||||
<AskThread
|
||||
history={pathChat.history}
|
||||
asks={pathChat.asks}
|
||||
open
|
||||
hint={qaSubject === 'folder' ? '这个文件夹' : '这个文件'}
|
||||
{...(qaSubject === 'feature' && { wording: FEATURE_THREAD })}
|
||||
autoFocus={false}
|
||||
focusToken={pathFocus}
|
||||
closable={false}
|
||||
profiles={pathChat.profiles}
|
||||
onSelectProfile={qa.onSelectProfile}
|
||||
onAsk={(question, images) => qa.onAsk(PATH_ASK_ANCHOR, question, images)}
|
||||
onStop={qa.onStop}
|
||||
onDelete={entryId => qa.onDelete(PATH_ASK_ANCHOR, entryId)}
|
||||
onClose={() => undefined}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{diff?.newFile !== undefined && doc.error === undefined && <NewFileBanner detail={diff.newFile} />}
|
||||
{doc.error !== undefined && (
|
||||
<div className="mb-3 flex items-start gap-2 rounded-lg border border-[var(--vscode-inputValidation-errorBorder)] bg-[var(--vscode-inputValidation-errorBackground)] px-3 py-2">
|
||||
<TriangleAlert size={16} strokeWidth={1.8} className="mt-px shrink-0 text-[var(--vscode-errorForeground)]" />
|
||||
<div className="min-w-0">
|
||||
<div className="break-words">
|
||||
解析失败:
|
||||
{doc.error}
|
||||
</div>
|
||||
<div className="mt-0.5 text-xs text-[var(--vscode-descriptionForeground)]">点编辑器右上角的「编辑」查看源码。</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{doc.blocks.length === 0
|
||||
? doc.error === undefined && qaSubject === 'file' && <div className="text-[var(--vscode-descriptionForeground)]">这个文件里没有代码。</div>
|
||||
: doc.blocks.some(block => !filter.hidden.has(block.id))
|
||||
? <BlockTree blocks={doc.blocks} tree={tree} />
|
||||
: <div className="text-[var(--vscode-descriptionForeground)]">所有块都被筛选隐藏了。</div>}
|
||||
</div>
|
||||
{sticky?.enabled && <StickyLines content={content} blocks={doc.blocks} settings={sticky} />}
|
||||
<DefinitionPeekPopup shown={peek.shown} onJump={peek.jump} />
|
||||
{openCall && (
|
||||
<CallTreeDialog
|
||||
kind={openCall.kind}
|
||||
badge={openCall.badge}
|
||||
name={openCall.name}
|
||||
where={openCall.where}
|
||||
status={openCall.status === 'loading' ? 'loading' : openCall.trees.length === 0 ? 'empty' : 'ready'}
|
||||
trees={openCall.trees}
|
||||
selected={openCall.entry}
|
||||
contractMiss={openCall.contractMiss}
|
||||
onSelect={entry => page.update(({ openCall: prev }) => ({ openCall: prev && { ...prev, entry } }))}
|
||||
onClose={() => page.update({ openCall: null })}
|
||||
onJump={(jump) => {
|
||||
page.update({ openCall: null })
|
||||
postMessage({ type: 'goToCallNode', ...jump })
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</TextMarksContext>
|
||||
)
|
||||
}
|
||||
Reference in new issue
Block a user