🐛 fix(vscode): 代码阅读注释里引用的语言内置名不再标成找不到:代码里写到的(raise ValueError)问那处的定义、点了跳到语言自带的定义,没写到的当普通文字不能点;内置名单由语言插件给(Python 补全 dir(builtins),TypeScript 是 ECMAScript 标准全局对象)
This commit is contained in:
1 parent
49a3854390
commit
e3c8c1b118
14 files changed
+544
-98
No files matched your search
@@ -1,10 +1,10 @@
|
||||
import type { BlockView } from './blockView'
|
||||
import type { ImportFacts, ImportItem, Node, SourceRange } from './contract'
|
||||
import type { ImportFacts, ImportItem, NameBinding, Node, SourceRange } from './contract'
|
||||
import { findBlock } from './qa/prompt'
|
||||
|
||||
/**
|
||||
* 注释里不带路径的引用(KDoc 的 `[Name]`、`[Class.member]`)指到哪:从这段注释说明的那个声明起,一层层往外找本文件定义的,
|
||||
* 再找本文件导入进来的,都没有就找不到。
|
||||
* 再找本文件导入进来的,再找本文件代码里用到的语言自带的名字;语言自带、又问不到定义的当普通文字,别的都没有就找不到。
|
||||
* 符号、定义问语言扩展,交给调用方(预览)去问;这里只管从哪儿起、按什么顺序找、怎么拆名字(不碰 VS Code)。
|
||||
*/
|
||||
|
||||
@@ -29,13 +29,21 @@ export interface LinkTarget {
|
||||
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)) }
|
||||
@@ -63,6 +71,22 @@ 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('.')
|
||||
@@ -107,24 +131,11 @@ function definedNear(symbols: readonly SymbolNode[], at: number | undefined, nam
|
||||
}
|
||||
|
||||
/**
|
||||
* 引用指到哪,按 KDoc 的规矩(不带限定的名字,当它写在这段注释说明的那个声明里面来认):
|
||||
* 1. 本文件定义的,由近到远一层层找:at 是注释说明的那个声明的名字在源码里的下标(见 referenceScope)。先看盖住 at 的最里层符号
|
||||
* (就是这个声明)的直接子符号,再一层层往外看包着它的符号的直接子符号,最后看文件顶层;哪层先有这个名字(`Class.member` 看第一段)
|
||||
* 就是哪个,`Class.member` 再在它底下找成员。导入语句里的名字不算本文件定义的。没有 at(不在哪个声明的注释里)只看文件顶层。
|
||||
* 2. 本文件导入进来的:名字(`Class.member` 的第一段)是某条导入引进来的,问导入语句里那个名字的定义;`Class.member` 再在定义所在文件的
|
||||
* 符号里找:定义落在哪个符号里就在它底下找,落在文件开头(导入的是模块)就从顶层找。
|
||||
* 3. 都没有:找不到。本文件某层有这个名字、却没有这个成员时也是找不到,不再去更外层、导入里找。
|
||||
* 问到的定义 `target` 底下的成员 `members`,没有成员就是 `target` 自己:在定义所在文件的符号里找,定义落在哪个符号里就在它底下找,
|
||||
* 落在文件开头(导入的是模块)就从顶层找。
|
||||
*/
|
||||
export async function resolveReference(reference: string, at: number | undefined, imports: readonly ImportedName[], sources: ReferenceSources): Promise<LinkTarget | undefined> {
|
||||
const { head, members } = splitReference(reference)
|
||||
const own = definedNear(await sources.symbols(sources.uri), 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)
|
||||
const target = imported && await sources.definition(imported.offset)
|
||||
if (!target || members.length === 0)
|
||||
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)
|
||||
@@ -132,6 +143,43 @@ export async function resolveReference(reference: string, at: number | undefined
|
||||
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):这块有名字(是个声明)就是它的名字在源码里的下标;
|
||||
* 没名字的(注释、语句……)取外层最近的有名字的块。外层也都没名字、块树里没有这块时没有,只从文件顶层找。
|
||||
|
||||
Reference in new issue
Block a user