✨ feat(vscode): 代码阅读的函数和类可以看调用树

This commit is contained in:
cruldra committed 2026-10-11 00:37:24 -04:00
1 parent 8dc572bf94
commit 645542eb5c
17 files changed
+1679 -5

No files matched your search

@@ -0,0 +1,120 @@
/**
* 调用树弹框:盖在编辑区上,预览的卡片留在后面。点遮罩、点叉、按 Esc 关掉,焦点回到打开它的按钮。
*/
import type { ReactElement } from 'react'
import type { CallTree, CallTreeNode } from './types'
import { Box, Braces, X } from 'lucide-react'
import { useEffect, useState } from 'react'
import { createPortal } from 'react-dom'
export function CallTreeDialog({ kind, name, where, status, trees, onClose }: {
/** 当前块的种类,整条路径用这个种类色 */
kind: string
name: string
where: string
status: 'loading' | 'empty' | 'ready'
trees: readonly CallTree[]
onClose: () => void
}): ReactElement {
const [selected, setSelected] = useState(0)
const tree = trees[Math.min(selected, Math.max(trees.length - 1, 0))]
useEffect(() => {
const onKey = (event: KeyboardEvent) => {
if (event.key !== 'Escape')
return
document.querySelector<HTMLButtonElement>('[data-call-tree][data-active]')?.focus()
onClose()
}
window.addEventListener('keydown', onKey)
return () => window.removeEventListener('keydown', onKey)
}, [onClose])
function close(): void {
document.querySelector<HTMLButtonElement>('[data-call-tree][data-active]')?.focus()
onClose()
}
return createPortal(
<div
className="cr-call-overlay"
onClick={(event) => {
if (event.target === event.currentTarget)
close()
}}
>
<div
role="dialog"
aria-modal="true"
aria-label={name ? `调用树 ${name}` : '调用树'}
className="cr-call-dialog"
data-kind={kind}
>
<div className="cr-call-title">
<span className="cr-call-heading">调用树</span>
{name && <span className="cr-call-symbol">{name}</span>}
{where && <span className="cr-call-loc">{where}</span>}
<button type="button" className="cr-tool cr-call-close" aria-label="关闭" onClick={close}>
<X size={16} strokeWidth={1.8} />
</button>
</div>
{status === 'ready' && trees.length > 1 && (
<div className="cr-call-entries" role="tablist" aria-label="调用树入口">
{trees.map((item, index) => (
<button
key={item.id}
type="button"
role="tab"
aria-selected={index === selected}
data-active={index === selected ? '' : undefined}
className="cr-call-entry"
onClick={() => setSelected(index)}
>
<span>{item.name}</span>
<span className="cr-call-hits">{`${item.hits} 处`}</span>
</button>
))}
</div>
)}
<div className="cr-call-body">
{status === 'loading' && <p className="cr-call-note">正在查调用关系</p>}
{status === 'empty' && <p className="cr-call-note">没有调用它的地方</p>}
{status === 'ready' && tree && <CallNodes node={tree.root} depth={0} path={tree.id} />}
</div>
</div>
</div>,
document.body,
)
}
function CallNodes({ node, depth, path }: { node: CallTreeNode, depth: number, path: string }): ReactElement {
const Icon = node.kind === 'class' ? Box : Braces
return (
<>
<div className="cr-call-row" style={{ paddingLeft: depth * 22 }}>
<span className="cr-call-guide" />
<div className="cr-call-card" data-current={node.current ? '' : undefined}>
<div className="cr-call-line">
<Icon size={14} strokeWidth={1.8} aria-hidden />
<span className="cr-call-name">{node.name}</span>
{node.current && <span className="cr-call-pill">当前</span>}
</div>
<div className="cr-call-sub">
{node.detail && <span>{node.detail}</span>}
<span className="cr-call-loc">{node.where}</span>
</div>
</div>
</div>
{node.children.map((child, index) => (
<CallNodes key={`${path}/${index}`} node={child} depth={depth + 1} path={`${path}/${index}`} />
))}
{node.otherCalls !== undefined && node.otherCalls > 0 && (
<div className="cr-call-row" style={{ paddingLeft: (depth + 1) * 22 }}>
<span className="cr-call-guide" data-folded="" />
<span className="cr-call-folded">{`另外 ${node.otherCalls} 处调用`}</span>
</div>
)}
</>
)
}