218 lines
8.0 KiB
TypeScript
218 lines
8.0 KiB
TypeScript
/**
|
||
* 问编辑器的调用关系命令,收成调用边;接口卡片另外并上前端发这个请求的地方。
|
||
* 语言扩展不提供、或者这一轮已经作废,就当没有。
|
||
*/
|
||
|
||
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<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)
|
||
const items = await prepare(document.uri, position)
|
||
if (cancelled() || (items.length === 0 && !requests))
|
||
return undefined
|
||
|
||
const vscodeItems = new Map<string, CallHierarchyItem>()
|
||
const remember = (item: CallHierarchyItem): CallSymbol => {
|
||
const symbol = toSymbol(item)
|
||
vscodeItems.set(symbolKey(symbol), item)
|
||
return symbol
|
||
}
|
||
const starts: CallSymbol[] = []
|
||
const seen = new Set<string>()
|
||
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<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),
|
||
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<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())
|
||
return []
|
||
try {
|
||
const calls = await commands.executeCommand<T[]>(command, item)
|
||
return (calls ?? []).map(map)
|
||
}
|
||
catch {
|
||
return []
|
||
}
|
||
}
|
||
}
|
||
|
||
/** 编辑器在这个位置准备出的调用关系符号;语言扩展不提供时是空的。 */
|
||
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 {
|
||
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'
|
||
}
|
||
}
|