/** * 问编辑器的调用关系命令,收成调用边;接口卡片另外并上前端发这个请求的地方。 * 语言扩展不提供、或者这一轮已经作废,就当没有。 */ import type { CallHierarchyIncomingCall, CallHierarchyItem, CallHierarchyOutgoingCall, TextDocument } from 'vscode' import type { RuleLocation } from '../contract' import type { IndexedEntry, IndexFind } from '../workspaceIndex' import type { CallEdge, CallRange, CallSymbol } from './model' 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 { 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[], ): Promise<{ symbol: CallSymbol, edges: CallEdge[] } | undefined> { if (cancelled()) return undefined const position = document.positionAt(offset) const items = await prepare(document.uri, position) if (cancelled() || (items.length === 0 && !requests)) return undefined const vscodeItems = new Map() const remember = (item: CallHierarchyItem): CallSymbol => { const symbol = toSymbol(item) vscodeItems.set(symbolKey(symbol), item) return symbol } const starts: CallSymbol[] = [] const seen = new Set() for (const item of items) { const symbol = remember(item) const key = symbolKey(symbol) if (seen.has(key)) continue seen.add(key) starts.push(symbol) } 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 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 { 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), ranges: call.fromRanges.map(toRange), })) } async function outgoing(node: CallSymbol) { return hits(node, 'vscode.provideOutgoingCalls', (call: CallHierarchyOutgoingCall) => ({ to: remember(call.to), ranges: call.fromRanges.map(toRange), })) } /** 发请求的地方所在的函数:编辑器认得它就用编辑器的(往上还能接着问),不在有名字的函数里、编辑器不认得时照索引写一个。 */ async function requestCaller(location: RuleLocation, entry: RequestEntry): Promise { 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(node: CallSymbol, command: string, map: (call: T) => R): Promise { const item = vscodeItems.get(symbolKey(node)) if (!item || cancelled()) return [] try { const calls = await commands.executeCommand(command, item) return (calls ?? []).map(map) } catch { return [] } } } /** 编辑器在这个位置准备出的调用关系符号;语言扩展不提供时是空的。 */ async function prepare(uri: Uri, position: Position): Promise { try { const prepared = await commands.executeCommand('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 { uri: item.uri.toString(), line: start.line, character: start.character, name: item.name, kind: kindName(item.kind), path: workspace.asRelativePath(item.uri), ...(item.detail && { detail: item.detail }), } } function toRange(range: CallHierarchyItem['range']): CallRange { return { line: range.start.line, character: range.start.character, endLine: range.end.line, endCharacter: range.end.character, } } /** 编辑器的符号种类换成块种类,只给图标用。 */ function kindName(kind: SymbolKind): string { switch (kind) { case SymbolKind.Method: case SymbolKind.Function: case SymbolKind.Operator: return 'function' case SymbolKind.Class: case SymbolKind.Struct: case SymbolKind.Constructor: return 'class' case SymbolKind.Interface: case SymbolKind.TypeParameter: return 'type' case SymbolKind.Enum: return 'enum' case SymbolKind.Field: return 'field' case SymbolKind.Variable: return 'variable' case SymbolKind.Constant: return 'const' case SymbolKind.Module: case SymbolKind.Namespace: case SymbolKind.Package: return 'module' default: return 'function' } }