✨ feat(vscode): FastAPI 接口卡片的调用树带上前端调用它的地方
This commit is contained in:
1 parent
d4459e93b3
commit
29cedcb751
30 files changed
+1316
-130
No files matched your search
@@ -1,7 +1,7 @@
|
||||
import type { ReactElement } from 'react'
|
||||
import type { BlockTreeState } from './BlockTree'
|
||||
import type { NameMap, VersionNames } from './displayNames'
|
||||
import type { BlockView, CallTree, DocumentDiff, DocumentMessage, Excluded, Reveal } from './types'
|
||||
import type { BlockView, CallTree, ContractMiss, DocumentDiff, DocumentMessage, Excluded, KindBadge, Reveal } from './types'
|
||||
import type { OccurrencePage } from './useOccurrences'
|
||||
import type { ViewState } from './viewState'
|
||||
import { TriangleAlert } from 'lucide-react'
|
||||
@@ -43,15 +43,15 @@ function savedBlockModes(): BlockModes {
|
||||
return { editing, folds }
|
||||
}
|
||||
|
||||
function blockKind(blocks: readonly BlockView[], anchor: string): string {
|
||||
function blockAt(blocks: readonly BlockView[], anchor: string): BlockView | undefined {
|
||||
for (const block of blocks) {
|
||||
if (block.anchor === anchor)
|
||||
return block.kind
|
||||
const inner = blockKind(block.children, anchor)
|
||||
return block
|
||||
const inner = blockAt(block.children, anchor)
|
||||
if (inner)
|
||||
return inner
|
||||
}
|
||||
return ''
|
||||
return undefined
|
||||
}
|
||||
|
||||
export function App(): ReactElement | null {
|
||||
@@ -86,7 +86,9 @@ export function App(): ReactElement | null {
|
||||
name: string
|
||||
where: string
|
||||
kind: string
|
||||
badge?: KindBadge
|
||||
trees: CallTree[]
|
||||
contractMiss?: ContractMiss
|
||||
} | null>(null)
|
||||
/** 块树所在的页面内容:钉住卡片时在这里面量 */
|
||||
const content = useRef<HTMLDivElement>(null)
|
||||
@@ -157,7 +159,7 @@ export function App(): ReactElement | null {
|
||||
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 }
|
||||
return { ...prev, status: 'ready', name: msg.name, where: msg.where, trees: msg.trees, contractMiss: msg.contractMiss }
|
||||
})
|
||||
return
|
||||
case 'reveal':
|
||||
@@ -192,7 +194,8 @@ export function App(): ReactElement | null {
|
||||
const onOpenCall = useCallback((anchor: string) => {
|
||||
if (!doc)
|
||||
return
|
||||
setOpenCall({ anchor, version: doc.version, status: 'loading', name: '', where: '', kind: blockKind(doc.blocks, anchor) || 'function', trees: [] })
|
||||
const block = blockAt(doc.blocks, anchor)
|
||||
setOpenCall({ anchor, version: doc.version, status: 'loading', name: '', where: '', kind: block?.kind ?? 'function', ...(block?.badge && { badge: block.badge }), trees: [] })
|
||||
postMessage({ type: 'openCallTree', version: doc.version, anchor })
|
||||
}, [doc])
|
||||
const kindNames = useMemo(() => new Map(doc?.attributes.find(attribute => attribute.id === 'kind')?.values.map(value => [value.id, value.name])), [doc])
|
||||
@@ -303,11 +306,17 @@ export function App(): ReactElement | null {
|
||||
{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}
|
||||
contractMiss={openCall.contractMiss}
|
||||
onClose={() => setOpenCall(null)}
|
||||
onJump={(jump) => {
|
||||
setOpenCall(null)
|
||||
postMessage({ type: 'goToCallNode', ...jump })
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</DisplayNameContext>
|
||||
|
||||
@@ -128,7 +128,7 @@ function shown(block: BlockView, tree: BlockTreeState): boolean {
|
||||
* 块的种类色写在哪:内置种类按 data-kind 取 CSS 里的配色;库规则认领的块(带 badge)按规则给的颜色,
|
||||
* 暗色、亮色两个都写在 style 上,CSS 按主题挑一个(见 [data-badge])。
|
||||
*/
|
||||
function kindProps(block: BlockView | undefined): { 'data-kind'?: string, 'data-badge'?: '', 'style'?: CSSProperties } {
|
||||
export function kindProps(block: Pick<BlockView, 'kind' | 'badge'> | undefined): { 'data-kind'?: string, 'data-badge'?: '', 'style'?: CSSProperties } {
|
||||
if (!block)
|
||||
return {}
|
||||
if (!block.badge)
|
||||
@@ -235,10 +235,10 @@ function BlockPreview({ block, tree, marks }: { block: BlockView, tree: BlockTre
|
||||
)
|
||||
}
|
||||
|
||||
/** 这块的「重写注释」按钮,排在右上角那排按钮里:能写注释(commentTarget)、页面上能改代码时才有。 */
|
||||
/** 这块的调用树按钮:扩展端标了能看调用树(BlockView.callTree)的块才有。 */
|
||||
function callTreeTool(block: BlockView, tree: BlockTreeState) {
|
||||
const callTree = tree.callTree
|
||||
if (!callTree || (block.kind !== 'function' && block.kind !== 'class'))
|
||||
if (!callTree || !block.callTree)
|
||||
return undefined
|
||||
return {
|
||||
anchor: block.anchor,
|
||||
|
||||
@@ -36,11 +36,12 @@ async function render(element: ReactElement): Promise<void> {
|
||||
await act(async () => root.render(element))
|
||||
}
|
||||
|
||||
function block(kind: string, id = kind): BlockView {
|
||||
function block(kind: string, id = kind, callTree?: BlockView['callTree']): BlockView {
|
||||
return {
|
||||
id,
|
||||
anchor: id,
|
||||
kind,
|
||||
...(callTree && { callTree }),
|
||||
startLine: 0,
|
||||
endLine: 1,
|
||||
code: '',
|
||||
@@ -79,7 +80,7 @@ function tree(callTree?: BlockTreeState['callTree'], edit = true): BlockTreeStat
|
||||
}
|
||||
|
||||
function node(name: string, extra: Partial<CallTree['root']> = {}): CallTree['root'] {
|
||||
return { name, kind: 'function', where: `${name}.py:1`, current: false, children: [], ...extra }
|
||||
return { name, kind: 'function', where: `${name}.py:1`, jump: { uri: `file:///${name}.py`, line: 0, character: 0 }, current: false, children: [], ...extra }
|
||||
}
|
||||
|
||||
const ONE: CallTree = {
|
||||
@@ -104,21 +105,29 @@ const MANY: CallTree[] = [
|
||||
]
|
||||
|
||||
describe('调用树弹框', () => {
|
||||
it('函数和类都有按钮,棵数为 0 只有图标、大于 0 带数字,语句没有;只读的树一排按钮都没有', async () => {
|
||||
const counts = new Map([['function', 0], ['class', 2], ['statement', 4]])
|
||||
await render(<BlockLevel blocks={[block('function'), block('class'), block('statement')]} tree={tree({ counts, onOpen: () => {} })} />)
|
||||
it('宿主标了能看调用树的块才有按钮(接口卡片也是),棵数为 0 只有图标、大于 0 带数字;只读的树一排按钮都没有', async () => {
|
||||
const counts = new Map([['function', 0], ['class', 2], ['statement', 4], ['route', 1], ['bare', 5]])
|
||||
const endpoint = { method: 'GET', path: '/agents' }
|
||||
await render(
|
||||
<BlockLevel
|
||||
blocks={[block('function', 'function', {}), block('class', 'class', {}), block('statement'), block('fastapi.route', 'route', { endpoint }), block('function', 'bare')]}
|
||||
tree={tree({ counts, onOpen: () => {} })}
|
||||
/>,
|
||||
)
|
||||
expect(container.querySelector('[data-call-tree="function"]')!.textContent).toBe('')
|
||||
expect(container.querySelector('[data-call-tree="statement"]')).toBeNull()
|
||||
expect(container.querySelector('[data-call-tree="class"]')).not.toBeNull()
|
||||
expect(container.querySelector('[data-call-tree="class"]')!.textContent).toBe('2')
|
||||
expect(container.querySelector('[data-call-tree="route"]')!.textContent).toBe('1')
|
||||
expect(container.querySelector('[data-call-tree="bare"]')).toBeNull()
|
||||
|
||||
await render(<BlockLevel blocks={[block('function')]} tree={tree({ counts: new Map([['function', 3]]), onOpen: () => {} }, false)} />)
|
||||
await render(<BlockLevel blocks={[block('function', 'function', {})]} tree={tree({ counts: new Map([['function', 3]]), onOpen: () => {} }, false)} />)
|
||||
expect(container.querySelector('[aria-label="块操作"]')).toBeNull()
|
||||
expect(container.querySelector('[data-call-tree="function"]')).toBeNull()
|
||||
})
|
||||
|
||||
it('弹框打开时原来的卡片还在,关掉之后弹框没了,焦点回到那个按钮', async () => {
|
||||
const card = block('function', 'mask')
|
||||
const card = block('function', 'mask', {})
|
||||
function Page(): ReactElement {
|
||||
const [open, setOpen] = useState(false)
|
||||
return (
|
||||
@@ -135,6 +144,7 @@ describe('调用树弹框', () => {
|
||||
status="ready"
|
||||
trees={[ONE]}
|
||||
onClose={() => setOpen(false)}
|
||||
onJump={() => {}}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
@@ -165,7 +175,7 @@ describe('调用树弹框', () => {
|
||||
? (
|
||||
<>
|
||||
<div data-id="mask">卡片</div>
|
||||
<CallTreeDialog kind="function" name="mask_phone" where="utils/phone.py:28" status="ready" trees={[ONE]} onClose={() => setOpen(false)} />
|
||||
<CallTreeDialog kind="function" name="mask_phone" where="utils/phone.py:28" status="ready" trees={[ONE]} onClose={() => setOpen(false)} onJump={() => {}} />
|
||||
</>
|
||||
)
|
||||
: <div data-id="mask">卡片</div>
|
||||
@@ -184,7 +194,7 @@ describe('调用树弹框', () => {
|
||||
})
|
||||
|
||||
it('多于一棵树才有入口切换,点一项换成那棵;只有一棵没有这排', async () => {
|
||||
await render(<CallTreeDialog kind="function" name="mask_phone" where="utils/phone.py:28" status="ready" trees={MANY} onClose={() => {}} />)
|
||||
await render(<CallTreeDialog kind="function" name="mask_phone" where="utils/phone.py:28" status="ready" trees={MANY} onClose={() => {}} onJump={() => {}} />)
|
||||
const tabs = [...document.body.querySelectorAll('[role="tab"]')]
|
||||
expect(tabs.map(tab => tab.textContent?.replace(/\s+/g, ''))).toEqual(['读取用户资料2处', '夜间对账1处'])
|
||||
const body = () => document.body.querySelector('.cr-call-body')!.textContent ?? ''
|
||||
@@ -196,9 +206,9 @@ describe('调用树弹框', () => {
|
||||
})
|
||||
|
||||
it('正在查、没有调用者,以及类的「新建」节点', async () => {
|
||||
await render(<CallTreeDialog kind="class" name="" where="" status="loading" trees={[]} onClose={() => {}} />)
|
||||
await render(<CallTreeDialog kind="class" name="" where="" status="loading" trees={[]} onClose={() => {}} onJump={() => {}} />)
|
||||
expect(document.body.textContent).toContain('正在查调用关系')
|
||||
await render(<CallTreeDialog kind="class" name="OrderService" where="billing/order.py:14" status="empty" trees={[]} onClose={() => {}} />)
|
||||
await render(<CallTreeDialog kind="class" name="OrderService" where="billing/order.py:14" status="empty" trees={[]} onClose={() => {}} onJump={() => {}} />)
|
||||
expect(document.body.textContent).toContain('没有调用它的地方')
|
||||
const made = node('新建 OrderService', {
|
||||
kind: 'class',
|
||||
@@ -214,6 +224,7 @@ describe('调用树弹框', () => {
|
||||
status="ready"
|
||||
trees={[{ id: 'post', name: 'POST /orders', hits: 1, root: node('POST /orders', { detail: '入口', kind: 'function', children: [made] }) }]}
|
||||
onClose={() => {}}
|
||||
onJump={() => {}}
|
||||
/>,
|
||||
)
|
||||
expect(document.body.textContent).toContain('新建 OrderService')
|
||||
@@ -222,4 +233,48 @@ describe('调用树弹框', () => {
|
||||
expect(document.body.textContent).not.toContain('方法')
|
||||
expect(document.body.querySelector('[role="tablist"]')).toBeNull()
|
||||
})
|
||||
it('接口在契约文件里没对上:写明重新生成、列出找到的契约文件;一份都没有时写没找到;都不说「没有调用它的地方」', async () => {
|
||||
await render(<CallTreeDialog kind="fastapi.route" name="list_agents" where="api/routes.py:40" status="empty" trees={[]} contractMiss={{ files: ['web/src/lib/api/schema.d.ts', 'admin/src/lib/api/schema.d.ts'] }} onClose={() => {}} onJump={() => {}} />)
|
||||
const body = () => document.body.querySelector('.cr-call-body')!.textContent ?? ''
|
||||
expect(body()).toContain('接口契约文件里没有这个接口,重新生成后再看')
|
||||
expect(body()).toContain('web/src/lib/api/schema.d.ts')
|
||||
expect(body()).toContain('admin/src/lib/api/schema.d.ts')
|
||||
expect(body()).not.toContain('没有调用它的地方')
|
||||
|
||||
await render(<CallTreeDialog kind="fastapi.route" name="list_agents" where="api/routes.py:40" status="empty" trees={[]} contractMiss={{ files: [] }} onClose={() => {}} onJump={() => {}} />)
|
||||
expect(body()).toContain('工作区里没找到接口契约文件(openapi-typescript 生成的 .d.ts 或 openapi.json)')
|
||||
expect(body()).not.toContain('没有调用它的地方')
|
||||
|
||||
await render(<CallTreeDialog kind="fastapi.route" name="list_agents" where="api/routes.py:40" status="ready" trees={[ONE]} contractMiss={{ files: [] }} onClose={() => {}} onJump={() => {}} />)
|
||||
expect(body()).toContain('工作区里没找到接口契约文件')
|
||||
expect(body()).toContain('夜间对账')
|
||||
})
|
||||
|
||||
it('契约里几条都对得上、分不出是哪一条时列出那几条,不叫人重新生成', async () => {
|
||||
const miss = { files: ['web/src/lib/api/schema.d.ts'], ambiguous: ['/api/v1/admin/agents', '/api/v1/agents'] }
|
||||
await render(<CallTreeDialog kind="fastapi.route" name="list_agents" where="api/routes.py:40" status="empty" trees={[]} contractMiss={miss} onClose={() => {}} onJump={() => {}} />)
|
||||
const body = document.body.querySelector('.cr-call-body')!.textContent ?? ''
|
||||
expect(body).toContain('接口契约文件里有几条都对得上这个接口,按 operationId 也分不出是哪一条')
|
||||
expect(body).toContain('/api/v1/admin/agents')
|
||||
expect(body).toContain('/api/v1/agents')
|
||||
expect(body).not.toContain('重新生成')
|
||||
expect(body).not.toContain('没有调用它的地方')
|
||||
})
|
||||
|
||||
it('点一行跳到它在的文件和行,「请求」节点跳到发请求的那一行', async () => {
|
||||
const jumps: unknown[] = []
|
||||
const request = node('请求 GET /api/v1/agents/default', {
|
||||
kind: 'request',
|
||||
where: 'web/src/lib/agents.ts:11',
|
||||
jump: { uri: 'file:///web/src/lib/agents.ts', line: 10, character: 20 },
|
||||
children: [node('get_default_agent', { current: true })],
|
||||
})
|
||||
const page: CallTree = { id: 'page', name: 'MyAgentsPage', hits: 1, root: node('MyAgentsPage', { detail: '页面 /my-agents', children: [request] }) }
|
||||
await render(<CallTreeDialog kind="fastapi.route" name="get_default_agent" where="api/routes.py:79" status="ready" trees={[page]} onClose={() => {}} onJump={jump => jumps.push(jump)} />)
|
||||
expect(document.body.textContent).toContain('页面 /my-agents')
|
||||
const rows = [...document.body.querySelectorAll<HTMLButtonElement>('button.cr-call-card')]
|
||||
expect(rows.map(row => row.querySelector('.cr-call-name')!.textContent)).toEqual(['MyAgentsPage', '请求 GET /api/v1/agents/default', 'get_default_agent'])
|
||||
await act(async () => rows[1].click())
|
||||
expect(jumps).toEqual([{ uri: 'file:///web/src/lib/agents.ts', line: 10, character: 20 }])
|
||||
})
|
||||
})
|
||||
@@ -1,21 +1,28 @@
|
||||
/**
|
||||
* 调用树弹框:盖在编辑区上,预览的卡片留在后面。点遮罩、点叉、按 Esc 关掉,焦点回到打开它的按钮。
|
||||
* 点树上的一行跳到它在的文件和行。接口在契约文件里没对上时写明原因,不当成没人调用。
|
||||
*/
|
||||
|
||||
import type { ReactElement } from 'react'
|
||||
import type { CallTree, CallTreeNode } from './types'
|
||||
import { Box, Braces, X } from 'lucide-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, name, where, status, trees, onClose }: {
|
||||
/** 当前块的种类,整条路径用这个种类色 */
|
||||
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))]
|
||||
@@ -49,7 +56,7 @@ export function CallTreeDialog({ kind, name, where, status, trees, onClose }: {
|
||||
aria-modal="true"
|
||||
aria-label={name ? `调用树 ${name}` : '调用树'}
|
||||
className="cr-call-dialog"
|
||||
data-kind={kind}
|
||||
{...kindProps({ kind, badge })}
|
||||
>
|
||||
<div className="cr-call-title">
|
||||
<span className="cr-call-heading">调用树</span>
|
||||
@@ -79,8 +86,9 @@ export function CallTreeDialog({ kind, name, where, status, trees, onClose }: {
|
||||
)}
|
||||
<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} />}
|
||||
{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>,
|
||||
@@ -88,26 +96,48 @@ export function CallTreeDialog({ kind, name, where, status, trees, onClose }: {
|
||||
)
|
||||
}
|
||||
|
||||
function CallNodes({ node, depth, path }: { node: CallTreeNode, depth: number, path: string }): ReactElement {
|
||||
const Icon = node.kind === 'class' ? Box : Braces
|
||||
/**
|
||||
* 接口在契约文件里没对上:分不出是哪一条时列出那几条;没有这一条时让人重新生成、列出找到的契约文件;
|
||||
* 一份契约文件都没有时说没找到。
|
||||
*/
|
||||
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" />
|
||||
<div className="cr-call-card" data-current={node.current ? '' : undefined}>
|
||||
<div className="cr-call-line">
|
||||
<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>}
|
||||
</div>
|
||||
<div className="cr-call-sub">
|
||||
</span>
|
||||
<span className="cr-call-sub">
|
||||
{node.detail && <span>{node.detail}</span>}
|
||||
<span className="cr-call-loc">{node.where}</span>
|
||||
</div>
|
||||
</div>
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
{node.children.map((child, index) => (
|
||||
<CallNodes key={`${path}/${index}`} node={child} depth={depth + 1} path={`${path}/${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 }}>
|
||||
|
||||
@@ -1394,6 +1394,13 @@ del.cr-diff-word {
|
||||
margin: 8px 0;
|
||||
color: var(--vscode-descriptionForeground);
|
||||
}
|
||||
.cr-call-note p {
|
||||
margin: 0;
|
||||
}
|
||||
.cr-call-files {
|
||||
margin: 6px 0 0;
|
||||
padding-left: 16px;
|
||||
}
|
||||
.cr-call-row {
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
@@ -1420,6 +1427,17 @@ del.cr-diff-word {
|
||||
border: 1px solid color-mix(in srgb, var(--kind) 35%, transparent);
|
||||
border-radius: 8px;
|
||||
background: color-mix(in srgb, var(--kind) 7%, transparent);
|
||||
color: inherit;
|
||||
font: inherit;
|
||||
text-align: left;
|
||||
cursor: pointer;
|
||||
}
|
||||
.cr-call-card:hover {
|
||||
background: color-mix(in srgb, var(--kind) 12%, transparent);
|
||||
}
|
||||
.cr-call-card:focus-visible {
|
||||
outline: 1px solid var(--vscode-focusBorder);
|
||||
outline-offset: 1px;
|
||||
}
|
||||
.cr-call-card[data-current] {
|
||||
border-color: var(--kind);
|
||||
|
||||
@@ -149,9 +149,16 @@ interface BlockViewBase {
|
||||
badge?: KindBadge
|
||||
/** 库规则挂在父卡片子行上的子块才有:展开时画在父卡片属性表第 row 行下面,不排在父卡片的子块里;见 BlockSlot */
|
||||
slot?: BlockSlot
|
||||
/** 能看调用树的块才有(有名字的函数、类,讲 HTTP 接口的卡片):有它才摆调用树按钮 */
|
||||
callTree?: CallTreeTarget
|
||||
children: BlockView[]
|
||||
}
|
||||
|
||||
/** 一块能看调用树:接口卡片带上它讲的接口(扩展端查前端调用者用,这边只看有没有 callTree)。 */
|
||||
export interface CallTreeTarget {
|
||||
endpoint?: { method: string, path: string, operationIdPrefix?: string }
|
||||
}
|
||||
|
||||
/**
|
||||
* 子块挂在父卡片的哪儿:属性表第 row 行的第 item 个子行。它默认收着(它的块 id 记成折叠着),点那个子行的第一格才在这一行下面展开、
|
||||
* 同一行一次只展开一个,折叠这一行时跟着收起(Rust 实现块的方法表点方法名)
|
||||
@@ -445,22 +452,41 @@ export interface StickyScrollSettings {
|
||||
* repoLinkBroken:这个仓库链接打不开。没有 symbol 是文件或目录没有;有 symbol 是文件在、这个符号没有;没有 path 是不带路径的引用找不到
|
||||
* repoLinkOpened:这个仓库链接打开了。路径在;带了 symbol 时这个符号也在;没有 path 是不带路径的引用找到了
|
||||
* repoImagesChecked:id 那次 checkRepoImages 的回答。src 是预览里能直接用的图片地址;没有 src 是文件不在
|
||||
* callTreeCounts:version 这份文档里,每个函数、类落在几棵调用树里(只包括大于 0 的)。不是页面上这个版本的不理
|
||||
* callTree:anchor 那块的调用树。trees 空着是没有调用它的地方。不是页面上这个版本的不理
|
||||
* callTreeCounts:version 这份文档里,每个能看调用树的块落在几棵调用树里(只包括大于 0 的)。不是页面上这个版本的不理
|
||||
* callTree:anchor 那块的调用树。trees 空着、又没有 contractMiss 是没有调用它的地方;接口卡片在契约文件里没对上时带 contractMiss。
|
||||
* 不是页面上这个版本的不理
|
||||
*/
|
||||
/** 点节点跳到哪:文件和位置,行和列从 0 起。 */
|
||||
export interface CallJump {
|
||||
uri: string
|
||||
line: number
|
||||
character: number
|
||||
}
|
||||
|
||||
export interface CallTreeNode {
|
||||
name: string
|
||||
/** 块种类;「请求」节点是 request */
|
||||
kind: string
|
||||
/** 第二行。根是「入口」,新建类是「进入这个类」 */
|
||||
/** 第二行。根是「入口」(Next.js 页面是「页面 网址」),新建类是「进入这个类」 */
|
||||
detail?: string
|
||||
/** 文件:行,行从 1 起 */
|
||||
where: string
|
||||
/** 符号是它的名字起点;「新建」「请求」节点是发生调用的那一处 */
|
||||
jump: CallJump
|
||||
current: boolean
|
||||
children: CallTreeNode[]
|
||||
/** 不在通向当前块的路径上的调用次数 */
|
||||
otherCalls?: number
|
||||
}
|
||||
|
||||
/** 接口在契约文件里没对上。 */
|
||||
export interface ContractMiss {
|
||||
/** 找到的契约文件(从工作区文件夹算起的路径),一份都没有时是空的 */
|
||||
files: string[]
|
||||
/** 契约里结尾对得上、按 operationId 也分不出是哪一条的几条完整路径;一条都对不上时没有 */
|
||||
ambiguous?: string[]
|
||||
}
|
||||
|
||||
/** 一棵调用树。hits 是这一棵里当前块出现几次。 */
|
||||
export interface CallTree {
|
||||
id: string
|
||||
@@ -493,7 +519,7 @@ export type ExtensionToWebview
|
||||
| { type: 'repoImagesChecked', id: number, images: { path: string, src?: string }[] }
|
||||
| { type: 'focusPathAsk' }
|
||||
| { type: 'callTreeCounts', version: number, counts: Record<string, number> }
|
||||
| { type: 'callTree', version: number, anchor: string, name: string, where: string, trees: CallTree[] }
|
||||
| { type: 'callTree', version: number, anchor: string, name: string, where: string, trees: CallTree[], contractMiss?: ContractMiss }
|
||||
|
||||
/**
|
||||
* edit:把 startLine..endLine 这几行整行替换成 text(扩展端给每行补回原来第一行的缩进)
|
||||
@@ -519,6 +545,7 @@ export type ExtensionToWebview
|
||||
* checkRepoLinks:预览里这批仓库路径问哪些文件或目录没有,这批不带路径的引用(symbols)问哪些找不到。id 标出是哪一次问
|
||||
* checkRepoImages:预览里这批仓库图片,问哪些文件在,在的要预览里能直接用的地址。id 标出是哪一次问
|
||||
* openCallTree:打开 anchor 那块的调用树。version 是这块所在的文档版本
|
||||
* goToCallNode:点了调用树上的一个节点,打开 uri 那个文件,跳到 line 行 character 列(见 CallTreeNode.jump)
|
||||
*/
|
||||
export type WebviewToExtension
|
||||
= | { type: 'ready' }
|
||||
@@ -543,3 +570,4 @@ export type WebviewToExtension
|
||||
| { type: 'checkRepoLinks', id: number, paths: string[], symbols: string[] }
|
||||
| { type: 'checkRepoImages', id: number, paths: string[] }
|
||||
| { type: 'openCallTree', version: number, anchor: string }
|
||||
| { type: 'goToCallNode', uri: string, line: number, character: number }
|
||||
Reference in new issue
Block a user