Files
superwork/vscode/src/codeReader/callTree/query.ts
T

218 lines
8.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.
/**
* 问编辑器的调用关系命令,收成调用边;接口卡片另外并上前端发这个请求的地方。
* 语言扩展不提供、或者这一轮已经作废,就当没有。
*/
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'
}
}