Files
superwork/vscode/webview-ui/src/codeReader/CallTreeDialog.tsx
T

151 lines
6.0 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 调用树弹框:盖在编辑区上,预览的卡片留在后面。点遮罩、点叉、按 Esc 关掉,焦点回到打开它的按钮。
* 点树上的一行跳到它在的文件和行。接口在契约文件里没对上时写明原因,不当成没人调用。
*/
import type { ReactElement } from 'react'
import type { CallJump, CallTree, CallTreeNode, ContractMiss, KindBadge } from './types'
import { Box, Braces, Send, X } from 'lucide-react'
import { useEffect, useState } from 'react'
import { createPortal } from 'react-dom'
import { kindProps } from './BlockTree'
export function CallTreeDialog({ kind, badge, name, where, status, trees, contractMiss, onClose, onJump }: {
/** 当前块的种类,整条路径用这个种类色;库规则的卡片按 badge 的颜色 */
kind: string
badge?: KindBadge
name: string
where: string
status: 'loading' | 'empty' | 'ready'
trees: readonly CallTree[]
/** 接口在契约文件里没对上时有 */
contractMiss?: ContractMiss
onClose: () => void
/** 点了树上的一行 */
onJump: (jump: CallJump) => 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"
{...kindProps({ kind, badge })}
>
<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 !== 'loading' && contractMiss && <ContractNote miss={contractMiss} />}
{status === 'empty' && !contractMiss && <p className="cr-call-note">没有调用它的地方</p>}
{status === 'ready' && tree && <CallNodes node={tree.root} depth={0} path={tree.id} onJump={onJump} />}
</div>
</div>
</div>,
document.body,
)
}
/**
* 接口在契约文件里没对上:分不出是哪一条时列出那几条;没有这一条时让人重新生成、列出找到的契约文件;
* 一份契约文件都没有时说没找到。
*/
function ContractNote({ miss }: { miss: ContractMiss }): ReactElement {
if (miss.files.length === 0)
return <p className="cr-call-note">工作区里没找到接口契约文件(openapi-typescript 生成的 .d.ts 或 openapi.json)</p>
const [text, items] = miss.ambiguous
? ['接口契约文件里有几条都对得上这个接口,按 operationId 也分不出是哪一条', miss.ambiguous]
: ['接口契约文件里没有这个接口,重新生成后再看', miss.files]
return (
<div className="cr-call-note">
<p>{text}</p>
<ul className="cr-call-files">
{items.map(item => <li key={item} className="cr-call-loc">{item}</li>)}
</ul>
</div>
)
}
const NODE_ICONS: Record<string, typeof Braces> = { class: Box, request: Send }
function CallNodes({ node, depth, path, onJump }: { node: CallTreeNode, depth: number, path: string, onJump: (jump: CallJump) => void }): ReactElement {
const Icon = NODE_ICONS[node.kind] ?? Braces
return (
<>
<div className="cr-call-row" style={{ paddingLeft: depth * 22 }}>
<span className="cr-call-guide" />
<button type="button" className="cr-call-card" data-current={node.current ? '' : undefined} onClick={() => onJump(node.jump)}>
<span 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>}
</span>
<span className="cr-call-sub">
{node.detail && <span>{node.detail}</span>}
<span className="cr-call-loc">{node.where}</span>
</span>
</button>
</div>
{node.children.map((child, index) => (
<CallNodes key={`${path}/${index}`} node={child} depth={depth + 1} path={`${path}/${index}`} onJump={onJump} />
))}
{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>
)}
</>
)
}