Files
superwork/vscode/src/codeReader/symbolLinks.ts
T

192 lines
11 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 { BlockView } from './blockView'
import type { ImportFacts, ImportItem, NameBinding, Node, SourceRange } from './contract'
import { findBlock } from './qa/prompt'
/**
* 注释里不带路径的引用(KDoc 的 `[Name]`、`[Class.member]`)指到哪:从这段注释说明的那个声明起,一层层往外找本文件定义的,
* 再找本文件导入进来的,再找本文件代码里用到的语言自带的名字;语言自带、又问不到定义的当普通文字,别的都没有就找不到。
* 符号、定义问语言扩展,交给调用方(预览)去问;这里只管从哪儿起、按什么顺序找、怎么拆名字(不碰 VS Code)。
*/
/** 本文件某条导入引进来的一个名字:名字、问定义时问的位置(导入语句里那个名字的下标)、这一项在源码里的范围。 */
export interface ImportedName {
name: string
offset: number
range: SourceRange
}
/** 一个文件里的一个文档符号:名字、整个定义的范围(看定义落在哪个符号里)、名字的范围、子符号。下标都是那个文件里的。 */
export interface SymbolNode {
name: string
range: SourceRange
selection: SourceRange
children: readonly SymbolNode[]
}
/** 引用指到的地方:文件(uri 字符串)和定义处那个名字的范围。 */
export interface LinkTarget {
uri: string
selection: SourceRange
}
/**
* 解析一个引用要问的:本文件是哪个、它的文档符号,本文件某处名字的定义(几处取第一处),别的文件的文档符号,
* 本文件代码里用到某个本文件没定义的名字的各处(见 [outsideUses]),某个名字是不是这门语言自带的。
*/
export interface ReferenceSources {
uri: string
symbols: (uri: string) => Promise<readonly SymbolNode[]>
definition: (offset: number) => Promise<LinkTarget | undefined>
uses: (name: string) => readonly SourceRange[]
builtin: (name: string) => boolean
}
/** 一个引用解析出来是什么:指到的地方;`'builtin'` 是这门语言自带、又问不到定义的名字,预览当普通文字显示;找不到是 `undefined`。 */
export type ReferenceTarget = LinkTarget | 'builtin' | undefined
function spanOf(nodes: readonly (Node | undefined)[]): SourceRange {
const present = nodes.filter((node): node is Node => node !== undefined)
return { startIndex: Math.min(...present.map(node => node.startIndex)), endIndex: Math.max(...present.map(node => node.endIndex)) }
}
/** 一项导入引进来的名字:起了别名的是别名;问定义时问原来那个名字(模块问最后一节,那才是这个模块)。整模块导入(`from m import *`)没有。 */
function importedName(item: ImportItem): ImportedName | undefined {
switch (item.kind) {
case 'named':
return { name: (item.alias ?? item.name).text, offset: item.name.startIndex, range: spanOf([item.name, item.alias]) }
case 'default':
return { name: item.alias.text, offset: item.alias.startIndex, range: spanOf([item.alias]) }
case 'namespace':
return item.alias && { name: item.alias.text, offset: item.alias.startIndex, range: spanOf([item.alias]) }
case 'module': {
const last = item.module.namedChildren.at(-1) ?? item.module
const name = item.alias?.text ?? item.module.text.split('.')[0]
return { name, offset: item.alias ? last.startIndex : item.module.startIndex, range: spanOf([item.module, item.alias]) }
}
}
}
/** 本文件各条导入(预览已经读出的 ImportFacts)引进来的名字,照源码顺序。 */
export function importedNames(imports: readonly ImportFacts[]): ImportedName[] {
return imports.flatMap(facts => facts.items.flatMap(item => importedName(item) ?? []))
}
/**
* 本文件代码里用到、本文件又没定义的名字(语言自带的、导入的,见 [NameBinding] 的 `local`)各在哪几处:名字 → 各处的范围,照源码顺序。
* 绑定是语言插件照语法树认的,注释、字符串里的字和 `obj.name` 里的 `name` 都不在里面;导入的名字含导入语句里那一处。
*/
export function outsideUses(bindings: readonly NameBinding[], source: string): Map<string, SourceRange[]> {
const uses = new Map<string, SourceRange[]>()
for (const { occurrences, local } of bindings) {
const [first] = occurrences
if (local || !first)
continue
const name = source.slice(first.startIndex, first.endIndex)
uses.set(name, [...uses.get(name) ?? [], ...occurrences].sort((a, b) => a.startIndex - b.startIndex))
}
return uses
}
/** 引用拆成名字和成员:`Agent.owner_id` 是 Agent 和 [owner_id]。 */
export function splitReference(reference: string): { head: string, members: string[] } {
const [head = '', ...members] = reference.split('.')
return { head, members }
}
function covers(range: SourceRange, offset: number): boolean {
return range.startIndex <= offset && offset < range.endIndex
}
/** 符号底下按名字一层层找成员:先找直接的子符号,没有再往深处找。 */
function memberOf(symbols: readonly SymbolNode[], members: readonly string[]): SymbolNode | undefined {
const [first, ...rest] = members
const found = symbols.find(symbol => symbol.name === first) ?? deepFind(symbols, first)
return found && (rest.length > 0 ? memberOf(found.children, rest) : found)
}
function deepFind(symbols: readonly SymbolNode[], name: string | undefined): SymbolNode | undefined {
for (const symbol of symbols) {
const found = symbol.children.find(child => child.name === name) ?? deepFind(symbol.children, name)
if (found)
return found
}
return undefined
}
/** 盖住 offset 的各层符号,从最外层到最里层;offset 不在哪个符号里(比如落在文件开头)时是空的。 */
function enclosing(symbols: readonly SymbolNode[], offset: number): SymbolNode[] {
const outer = symbols.find(symbol => covers(symbol.range, offset))
return outer ? [outer, ...enclosing(outer.children, offset)] : []
}
/** 本文件里叫 name 的定义:从 at 那个声明起由近到远一层层看(见 resolveReference 第 1 步),导入语句里的名字不算。 */
function definedNear(symbols: readonly SymbolNode[], at: number | undefined, name: string, imports: readonly ImportedName[]): SymbolNode | undefined {
const scopes = at === undefined ? [] : enclosing(symbols, at).reverse()
for (const layer of [...scopes.map(scope => scope.children), symbols]) {
const found = layer.find(symbol => symbol.name === name && !imports.some(item => covers(item.range, symbol.selection.startIndex)))
if (found)
return found
}
return undefined
}
/**
* 问到的定义 `target` 底下的成员 `members`,没有成员就是 `target` 自己:在定义所在文件的符号里找,定义落在哪个符号里就在它底下找,
* 落在文件开头(导入的是模块)就从顶层找。
*/
async function memberAt(target: LinkTarget, members: readonly string[], sources: ReferenceSources): Promise<LinkTarget | undefined> {
if (members.length === 0)
return target
const symbols = await sources.symbols(target.uri)
const holder = enclosing(symbols, target.selection.startIndex).at(-1)
const found = memberOf(holder ? holder.children : symbols, members)
return found && { uri: target.uri, selection: found.selection }
}
/** 名字用在代码里的各处 `uses` 里挑一处问定义:先挑用在 `at` 那个声明(盖住 `at` 的最里层符号)里面的第一处,没有再挑整个文件的第一处。 */
function useNear(symbols: readonly SymbolNode[], at: number | undefined, uses: readonly SourceRange[]): SourceRange | undefined {
const declared = at === undefined ? undefined : enclosing(symbols, at).at(-1)
return (declared && uses.find(range => covers(declared.range, range.startIndex))) ?? uses[0]
}
/**
* 引用指到哪,按 KDoc 的规矩(不带限定的名字,当它写在这段注释说明的那个声明里面来认):
* 1. 本文件定义的,由近到远一层层找:`at` 是注释说明的那个声明的名字在源码里的下标(见 [referenceScope])。先看盖住 `at` 的最里层符号
* (就是这个声明)的直接子符号,再一层层往外看包着它的符号的直接子符号,最后看文件顶层;哪层先有这个名字(`Class.member` 看第一段)
* 就是哪个,`Class.member` 再在它底下找成员。导入语句里的名字不算本文件定义的。没有 `at`(不在哪个声明的注释里)只看文件顶层。
* 2. 本文件导入进来的:名字(第一段)是某条导入引进来的,问导入语句里那个名字的定义;`Class.member` 再在定义所在文件的符号里找成员(见 [memberAt])。
* 3. 本文件代码里用到、本文件又没定义的(语言自带的 `ValueError`、`str`……):在名字(第一段)用在代码里的各处([ReferenceSources.uses])挑一处
* 问定义(见 [useNear]),同在代码里 ⌘ + 点击它,Python 的内置名跳到 Pylance 自带的 builtins.pyi;`Class.member` 同第 2 步再找成员。
* 4. 前面都没问到定义、名字(第一段)又是这门语言自带的([ReferenceSources.builtin]):是 `'builtin'`,预览当普通文字,不标成找不到。
* 5. 都不是就找不到。本文件某层有这个名字、或问到了定义,却没有这个成员时也是找不到,不再往后找。
*/
export async function resolveReference(reference: string, at: number | undefined, imports: readonly ImportedName[], sources: ReferenceSources): Promise<ReferenceTarget> {
const { head, members } = splitReference(reference)
const symbols = await sources.symbols(sources.uri)
const own = definedNear(symbols, at, head, imports)
if (own) {
const found = members.length > 0 ? memberOf(own.children, members) : own
return found && { uri: sources.uri, selection: found.selection }
}
const imported = imports.find(item => item.name === head)
if (imported) {
const target = await sources.definition(imported.offset)
return target && memberAt(target, members, sources)
}
const use = useNear(symbols, at, sources.uses(head))
const target = use && await sources.definition(use.startIndex)
if (target)
return memberAt(target, members, sources)
return sources.builtin(head) ? 'builtin' : undefined
}
/**
* 注释写在 anchor 那块里时,引用从哪个声明起找(resolveReference 的 at):这块有名字(是个声明)就是它的名字在源码里的下标;
* 没名字的(注释、语句……)取外层最近的有名字的块。外层也都没名字、块树里没有这块时没有,只从文件顶层找。
*/
export function referenceScope(blocks: BlockView[], anchor: string): number | undefined {
const found = findBlock(blocks, anchor)
const declared = found && [...found.outer, found.block].reverse().find(block => block.defines !== undefined)
return declared?.defines?.startIndex
}