✨ feat(vscode): FastAPI 接口卡片的调用树带上前端调用它的地方

This commit is contained in:
cruldra committed 2026-10-12 12:22:51 +11:00
1 parent d4459e93b3
commit 29cedcb751
30 files changed
+1316 -130

No files matched your search

+94 -17
View File
@@ -1,37 +1,60 @@
/**
* 问编辑器的调用关系命令,收成调用边。
* 问编辑器的调用关系命令,收成调用边;接口卡片另外并上前端发这个请求的地方。
* 语言扩展不提供、或者这一轮已经作废,就当没有。
*/
import type { CallHierarchyIncomingCall, CallHierarchyItem, CallHierarchyOutgoingCall, TextDocument } from 'vscode'
import type { RuleLocation } from '../contract'
import type { IndexedEntry } from '../workspaceIndex'
import type { IndexFind } from './endpoint'
import type { CallEdge, CallRange, CallSymbol } from './model'
import { commands, SymbolKind, workspace } from 'vscode'
import type { RequestEntry } from './requests'
import type { CallTreeBlock, LoadedCalls } from './session'
import type { IncomingHit } from './walk'
import * as path from 'node:path'
import { commands, Position, SymbolKind, Uri, workspace } from 'vscode'
import { endpointCalls } from './endpoint'
import { symbolKey } from './model'
import { collectCallEdges } from './walk'
/**
* 问一块的调用关系。接口卡片(callTree.endpoint)先到契约里找前端发这个请求的地方(见 endpoint.ts),并进起点的调用者;
* 契约里对不上时带上 contractMiss,Python 里的调用者照常问。
*/
export async function loadBlockCalls(document: TextDocument, block: CallTreeBlock, find: IndexFind, cancelled: () => boolean): Promise<LoadedCalls | undefined> {
const offset = block.defines?.startIndex
const endpoint = block.callTree?.endpoint
if (offset === undefined)
return undefined
if (!endpoint)
return loadCallEdges(document, offset, cancelled)
const found = await endpointCalls(find, endpoint)
if (cancelled())
return undefined
if ('requests' in found)
return loadCallEdges(document, offset, cancelled, found.requests)
const loaded = await loadCallEdges(document, offset, cancelled, [])
const files = found.contracts.map(uri => workspace.asRelativePath(Uri.parse(uri)))
return loaded && { ...loaded, contractMiss: { files, ...(found.ambiguous && { ambiguous: found.ambiguous }) } }
}
/**
* 在 offset 这个名字起点上问调用关系。
* 先准备符号,再往上问谁调用了它;路径上的调用者再问它还调用了谁,用来收「另外 N 处」。
* 给了 requests(接口卡片,可以是空的)时,它们并进起点的调用者,从它们往上照常问编辑器;
* 这时编辑器认不出起点也照样收前端这一路,起点按名字起点自己写一个。
*/
export async function loadCallEdges(
document: TextDocument,
offset: number,
cancelled: () => boolean,
requests?: readonly IndexedEntry<RequestEntry>[],
): Promise<{ symbol: CallSymbol, edges: CallEdge[] } | undefined> {
if (cancelled())
return undefined
const position = document.positionAt(offset)
let prepared: CallHierarchyItem | CallHierarchyItem[] | null | undefined
try {
prepared = await commands.executeCommand('vscode.prepareCallHierarchy', document.uri, position)
}
catch {
return undefined
}
if (cancelled() || prepared == null)
return undefined
const items = (Array.isArray(prepared) ? prepared : [prepared]).filter(item => item)
if (items.length === 0)
const items = await prepare(document.uri, position)
if (cancelled() || (items.length === 0 && !requests))
return undefined
const vscodeItems = new Map<string, CallHierarchyItem>()
@@ -50,15 +73,27 @@ export async function loadCallEdges(
seen.add(key)
starts.push(symbol)
}
const symbol = starts.find(item => item.line === position.line && item.character === position.character) ?? starts[0]!
const edges: CallEdge[] = []
for (const start of starts) {
const symbol = starts.find(item => item.line === position.line && item.character === position.character) ?? starts[0] ?? symbolAt(document, position)
const sent: IncomingHit[] = []
for (const { location, entry } of requests ?? []) {
if (cancelled())
return undefined
edges.push(...await collectCallEdges(start, incoming, outgoing, cancelled))
sent.push({ from: await requestCaller(location, entry), ranges: [entry.at], request: `${entry.key} ${entry.path}` })
}
const edges: CallEdge[] = []
for (const start of starts.length > 0 ? starts : [symbol]) {
if (cancelled())
return undefined
edges.push(...await collectCallEdges(start, withRequests, outgoing, cancelled))
}
return cancelled() ? undefined : { symbol, edges }
/** 起点的调用者里并上前端发请求的地方;别的节点照常问编辑器。 */
async function withRequests(node: CallSymbol): Promise<IncomingHit[]> {
const calls = await incoming(node)
return symbolKey(node) === symbolKey(symbol) ? [...calls, ...sent] : calls
}
async function incoming(node: CallSymbol) {
return hits(node, 'vscode.provideIncomingCalls', (call: CallHierarchyIncomingCall) => ({
from: remember(call.from),
@@ -73,6 +108,24 @@ export async function loadCallEdges(
}))
}
/** 发请求的地方所在的函数:编辑器认得它就用编辑器的(往上还能接着问),不在有名字的函数里、编辑器不认得时照索引写一个。 */
async function requestCaller(location: RuleLocation, entry: RequestEntry): Promise<CallSymbol> {
const uri = Uri.parse(location.uri)
const at = new Position(location.start.line, location.start.character)
const found = entry.caller ? await prepare(uri, at) : []
const item = found.find(candidate => candidate.selectionRange.start.isEqual(at)) ?? found[0]
if (item)
return remember(item)
return {
uri: location.uri,
line: at.line,
character: at.character,
name: entry.caller ?? path.posix.basename(uri.path),
kind: entry.caller ? 'function' : 'module',
path: workspace.asRelativePath(uri),
}
}
async function hits<T, R>(node: CallSymbol, command: string, map: (call: T) => R): Promise<R[]> {
const item = vscodeItems.get(symbolKey(node))
if (!item || cancelled())
@@ -87,6 +140,30 @@ export async function loadCallEdges(
}
}
/** 编辑器在这个位置准备出的调用关系符号;语言扩展不提供时是空的。 */
async function prepare(uri: Uri, position: Position): Promise<CallHierarchyItem[]> {
try {
const prepared = await commands.executeCommand<CallHierarchyItem | CallHierarchyItem[] | null | undefined>('vscode.prepareCallHierarchy', uri, position)
return prepared == null ? [] : (Array.isArray(prepared) ? prepared : [prepared]).filter(item => item)
}
catch {
return []
}
}
/** 编辑器认不出起点时按名字起点自己写的符号。 */
function symbolAt(document: TextDocument, position: Position): CallSymbol {
const word = document.getWordRangeAtPosition(position)
return {
uri: document.uri.toString(),
line: position.line,
character: position.character,
name: word ? document.getText(word) : '',
kind: 'function',
path: workspace.asRelativePath(document.uri),
}
}
function toSymbol(item: CallHierarchyItem): CallSymbol {
const start = item.selectionRange.start
return {