🐛 fix(vscode): 注释里不带路径的引用从被注释的声明起一层层往外找,同类的方法、类的成员都能链到
This commit is contained in:
1 parent
15972c0b35
commit
5120e50228
15 files changed
+450
-167
No files matched your search
@@ -18,6 +18,16 @@ export type { Excluded, ShownName }
|
||||
|
||||
export interface TranslatorOption { id: string, label: string }
|
||||
|
||||
/**
|
||||
* 注释里一个不带路径的引用(`[Name]`、`[Class.member]`):symbol 是写的名字;block 是这段注释属于哪一块(块的 anchor),
|
||||
* 从那块代码的声明起一层层往外找(见 symbolLinks.ts)。不在哪块的注释里的没有 block,只从文件顶层找。
|
||||
* 同一个名字写在不同块的注释里,指到的可以不一样。
|
||||
*/
|
||||
export interface SymbolReference {
|
||||
symbol: string
|
||||
block?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* 让预览滚到包含 line 的块。definition 只在跳到定义时有:定义处那个名字在源码里的下标(Segment.offset),
|
||||
* 预览展开把它折叠住的块和属性行,再让就是这处定义的那一块或属性表里那一行闪几下。
|
||||
@@ -140,9 +150,10 @@ export interface StickyScrollSettings {
|
||||
* diff:这份文件相比对比基准改了什么(审查工作副本里的、从「未提交的更改」点开的有改动的才有,见 [对比基准](vscode/src/codeReader/diff/diffBase.ts#DiffBase)):每发一份文档之后算好了补发,
|
||||
* 基准变了(提交后 HEAD 动了、换了审查的工单、文件有了改动、这个标签页换了从哪儿打开)再发;null 是不比了(退出了审查、改动提交了、从别处点开了)
|
||||
* openedFrom:这个标签页是从哪儿打开的,页面记进自己的状态,VS Code 重启后恢复这个标签页时随 ready 报回来;ready 后发一次,开着时又从侧栏点开、换了从哪儿打开的再发
|
||||
* repoLinksChecked:id 那次 checkRepoLinks 的回答,missing 是仓库里没有的路径,unresolved 是找不到的不带路径的引用
|
||||
* repoLinkBroken:这个仓库链接打不开。没有 symbol 是文件或目录没有;有 symbol 是文件在、这个符号没有;没有 path 是不带路径的引用找不到
|
||||
* repoLinkOpened:这个仓库链接打开了。路径在;带了 symbol 时这个符号也在;没有 path 是不带路径的引用找到了
|
||||
* repoLinksChecked:id 那次 checkRepoLinks 的回答,missing 是仓库里没有的路径,unresolved 是找不到的不带路径的引用(原样带回问的那几条)
|
||||
* repoLinkBroken:这个仓库链接打不开。没有 symbol 是文件或目录没有;有 symbol 是文件在、这个符号没有;没有 path 是不带路径的引用找不到,
|
||||
* block 同 openRepoLink
|
||||
* repoLinkOpened:这个仓库链接打开了。路径在;带了 symbol 时这个符号也在;没有 path 是不带路径的引用找到了,block 同 openRepoLink
|
||||
* repoImagesChecked:id 那次 checkRepoImages 的回答。src 是预览里能直接用的图片地址;没有 src 是文件不在
|
||||
* callTreeCounts:version 这份文档里,每个能看调用树的块(BlockView.callTree)落在几棵调用树里(只包括大于 0 的)。不是页面上这个版本的不理
|
||||
* callTree:anchor 那块的调用树。trees 空着、又没有 contractMiss 是没有调用它的地方;接口卡片在契约文件里没对上时带 contractMiss。
|
||||
@@ -167,9 +178,9 @@ export type ExtensionToWebview
|
||||
| { type: 'rewriteEnd', askId: string, status: 'done' | 'failed' | 'stopped', text?: string, error?: string, entryId?: string }
|
||||
| { type: 'diff', diff: DocumentDiff | null }
|
||||
| { type: 'openedFrom', from: OpenedFrom }
|
||||
| { type: 'repoLinksChecked', id: number, missing: string[], unresolved: string[] }
|
||||
| { type: 'repoLinkBroken', path?: string, symbol?: string }
|
||||
| { type: 'repoLinkOpened', path?: string, symbol?: string }
|
||||
| { type: 'repoLinksChecked', id: number, missing: string[], unresolved: SymbolReference[] }
|
||||
| { type: 'repoLinkBroken', path?: string, symbol?: string, block?: string }
|
||||
| { type: 'repoLinkOpened', path?: string, symbol?: string, block?: string }
|
||||
| { type: 'repoImagesChecked', id: number, images: { path: string, src?: string }[] }
|
||||
| { type: 'focusPathAsk' }
|
||||
| { type: 'callTreeCounts', version: number, counts: Record<string, number> }
|
||||
@@ -183,7 +194,7 @@ export type ExtensionToWebview
|
||||
* version 是这份下标所属的文档版本
|
||||
* goToDefinition:⌘ / Ctrl + 点击,跳到 offset 处名字的定义
|
||||
* peekDefinition:鼠标停在带下标的名字上,要 offset 处名字的定义卡片(几处定义取第一处);id、version 同 probeDefinition
|
||||
* peekLink:鼠标停在注释里指到符号的链接上(带 #符号 的仓库链接、不带路径的引用),要那个符号的定义卡片;path、symbol 同 openRepoLink
|
||||
* peekLink:鼠标停在注释里指到符号的链接上(带 #符号 的仓库链接、不带路径的引用),要那个符号的定义卡片;path、symbol、block 同 openRepoLink
|
||||
* findOccurrences:单击带下标的名字,问它在这份文档里出现在哪几处;id、version 同 probeDefinition
|
||||
* setFilter:改块筛选(整份替换),扩展端记下后发 filter 给所有预览
|
||||
* setOnlyChanges:打开 / 关掉「只看改动」,扩展端记下后发 filter 给所有预览
|
||||
@@ -200,8 +211,9 @@ export type ExtensionToWebview
|
||||
* occurrenceNavigation:眼下能不能用键盘在高亮着的名字的各处出现之间跳(有高亮、页面有焦点、焦点不在输入框里),变了才发;
|
||||
* 扩展端按它设 when 子句用的上下文键
|
||||
* openRepoLink:点了注释里的仓库路径链接,打开 path(从仓库根算起)。symbol 是类名、函数名或「类名.成员名」,没有就只打开文件或在代码阅读侧栏的树上定位目录;
|
||||
* 没有 path 是不带路径的引用(`[Name]`、`[Class.member]`),按这份文件找 symbol(本文件定义的,再是导入的,见 symbolLinks.ts)
|
||||
* checkRepoLinks:预览里这批仓库路径问哪些文件或目录没有,这批不带路径的引用(symbols)问哪些找不到。id 标出是哪一次问
|
||||
* 没有 path 是不带路径的引用(`[Name]`、`[Class.member]`),按这份文件找 symbol:block 是它写在哪块的注释里(见 SymbolReference),
|
||||
* 从那块的声明起一层层往外找本文件定义的,再是导入的(见 symbolLinks.ts)
|
||||
* checkRepoLinks:预览里这批仓库路径问哪些文件或目录没有,这批不带路径的引用(references)问哪些找不到。id 标出是哪一次问
|
||||
* checkRepoImages:预览里这批仓库图片,问哪些文件在,在的要预览里能直接用的地址。id 标出是哪一次问
|
||||
* openCallTree:打开 anchor 那块的调用树。version 是这块所在的文档版本,对不上就不查
|
||||
* goToCallNode:点了调用树上的一个节点,打开 uri 那个文件,跳到 line 行 character 列(都从 0 起,见 CallTreeNode.jump)
|
||||
@@ -220,15 +232,15 @@ export type WebviewToExtension
|
||||
| { type: 'probeDefinition', id: number, version: number, offset: number }
|
||||
| { type: 'goToDefinition', version: number, offset: number }
|
||||
| { type: 'peekDefinition', id: number, version: number, offset: number }
|
||||
| { type: 'peekLink', id: number, path?: string, symbol: string }
|
||||
| { type: 'peekLink', id: number, path?: string, symbol: string, block?: string }
|
||||
| { type: 'findOccurrences', id: number, version: number, offset: number }
|
||||
| { type: 'ask', askId: string, anchor: string, question: string, images: ClaudeImage[] }
|
||||
| { type: 'stopAsk', askId: string }
|
||||
| { type: 'deleteQa', anchor: string, entryId: string }
|
||||
| { type: 'selectProfile', kind: ChatKind, id: string }
|
||||
| { type: 'occurrenceNavigation', available: boolean }
|
||||
| { type: 'openRepoLink', path?: string, symbol?: string }
|
||||
| { type: 'checkRepoLinks', id: number, paths: string[], symbols: string[] }
|
||||
| { type: 'openRepoLink', path?: string, symbol?: string, block?: string }
|
||||
| { type: 'checkRepoLinks', id: number, paths: string[], references: SymbolReference[] }
|
||||
| { type: 'checkRepoImages', id: number, paths: string[] }
|
||||
| { type: 'openCallTree', version: number, anchor: string }
|
||||
| { type: 'goToCallNode', uri: string, line: number, character: number }
|
||||
@@ -1,9 +1,13 @@
|
||||
import type { BlockView } from '../blockView'
|
||||
import type { DiffBase, OpenedFrom } from '../diff/diffBase'
|
||||
import type { ProfileChoice } from '../qa/askProfiles'
|
||||
import type { RewriteRequest } from '../qa/codeQa'
|
||||
import type { SymbolNode } from '../symbolLinks'
|
||||
import type { ExtensionToWebview, WebviewToExtension } from './messages'
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import { window, workspace } from 'vscode'
|
||||
import { documentMessage } from './documentMessage'
|
||||
import { documentSymbolNodes } from './documentSymbols'
|
||||
import { PreviewEditorProvider } from './previewEditor'
|
||||
|
||||
vi.mock('vscode', () => ({
|
||||
@@ -40,6 +44,8 @@ vi.mock('./documentMessage', () => ({
|
||||
imports: [],
|
||||
})),
|
||||
}))
|
||||
/** 文档符号由各个测试给 */
|
||||
vi.mock('./documentSymbols', () => ({ documentSymbolNodes: vi.fn(async () => []) }))
|
||||
|
||||
const SOURCE = ['// 旧说明', 'export function load() {}']
|
||||
|
||||
@@ -345,3 +351,40 @@ describe('问 AI 和重写注释的 profile 下拉各管各的', () => {
|
||||
expect(page.posted()).not.toContainEqual(expect.objectContaining({ type: 'profiles', kind: 'rewrite' }))
|
||||
})
|
||||
})
|
||||
|
||||
describe('注释里不带路径的引用按它写在哪块的注释里找', () => {
|
||||
const METHOD = 'class AgentService > function get_default_agent'
|
||||
const HELPER = 'function helper'
|
||||
|
||||
/** 一块有名字的代码:名字在源码里的下标是 at */
|
||||
function declared(anchor: string, at: number, children: BlockView[] = []): BlockView {
|
||||
return { id: anchor, anchor, kind: 'function', startLine: 0, endLine: 0, headerEndLine: 0, code: '', codeTokens: [], defines: { startIndex: at, endIndex: at + 1 }, attributes: { kind: 'function' }, concepts: [], children, translation: [] }
|
||||
}
|
||||
function symbol(name: string, start: number, end: number, children: SymbolNode[] = []): SymbolNode {
|
||||
return { name, range: { startIndex: start, endIndex: end }, selection: { startIndex: start, endIndex: start + name.length }, children }
|
||||
}
|
||||
|
||||
it('同一个名字:方法说明里的找得到同类的另一个方法,顶层函数说明里的找不到;标坏链接、点了打开都按各自那块找', async () => {
|
||||
const blocks = [declared('class AgentService', 100, [declared(METHOD, 400)]), declared(HELPER, 950)]
|
||||
vi.mocked(documentMessage).mockImplementationOnce(async input => ({
|
||||
message: { type: 'document', version: input.version, languageId: input.languageId, blocks, attributes: [], translators: [], translatorId: '', names: [] },
|
||||
displayNames: [],
|
||||
symbols: [],
|
||||
imports: [],
|
||||
}) as never)
|
||||
vi.mocked(documentSymbolNodes).mockResolvedValue([
|
||||
symbol('AgentService', 100, 900, [symbol('get_agent_admitted', 200, 300), symbol('get_default_agent', 400, 500)]),
|
||||
symbol('helper', 950, 990),
|
||||
])
|
||||
const { provider, document } = opening()
|
||||
const page = tab()
|
||||
provider.resolveCustomTextEditor(document as never, page.panel)
|
||||
await page.send({ type: 'ready' })
|
||||
|
||||
await page.send({ type: 'checkRepoLinks', id: 1, paths: [], references: [{ symbol: 'get_agent_admitted', block: METHOD }, { symbol: 'get_agent_admitted', block: HELPER }] })
|
||||
expect(page.posted()).toContainEqual({ type: 'repoLinksChecked', id: 1, missing: [], unresolved: [{ symbol: 'get_agent_admitted', block: HELPER }] })
|
||||
|
||||
await page.send({ type: 'openRepoLink', symbol: 'get_agent_admitted', block: HELPER })
|
||||
expect(page.posted()).toContainEqual({ type: 'repoLinkBroken', symbol: 'get_agent_admitted', block: HELPER })
|
||||
})
|
||||
})
|
||||
@@ -14,7 +14,7 @@ import type { TreeSitterRuntime } from '../runtime'
|
||||
import type { ImportedName, LinkTarget, SymbolNode } from '../symbolLinks'
|
||||
import type { BlockFilter } from './blockFilter'
|
||||
import type { FoundLocation } from './definitions'
|
||||
import type { ChatKind, DefinitionPeek, ExtensionToWebview, Occurrence, Reveal, WebviewToExtension } from './messages'
|
||||
import type { ChatKind, DefinitionPeek, ExtensionToWebview, Occurrence, Reveal, SymbolReference, WebviewToExtension } from './messages'
|
||||
import type { TargetSpot } from './openTarget'
|
||||
import type { TopSubject } from './pathPreview'
|
||||
import { realpath } from 'node:fs/promises'
|
||||
@@ -33,7 +33,7 @@ import { DocumentNames, resolveNames } from '../nameResolution'
|
||||
import { PATH_ASK_ANCHOR } from '../qa/pathQa'
|
||||
import { askPrompt, followUpPrompt } from '../qa/prompt'
|
||||
import { DocumentLookups } from '../ruleLookups'
|
||||
import { resolveReference } from '../symbolLinks'
|
||||
import { referenceScope, resolveReference } from '../symbolLinks'
|
||||
import { definitionAction } from './definitionAction'
|
||||
import { definitionSiteNames, namesOnCard, peekBlock } from './definitionPeek'
|
||||
import { definitionKind, findDefinitions, languageDefinitions, pickDefinition, ruleDefinitions } from './definitions'
|
||||
@@ -669,16 +669,16 @@ class Preview {
|
||||
await this.peekDefinition(msg.id, msg.version, msg.offset)
|
||||
return
|
||||
case 'peekLink':
|
||||
await this.peekLink(msg.id, msg.path, msg.symbol)
|
||||
await this.peekLink(msg.id, msg.path, msg.symbol, msg.block)
|
||||
return
|
||||
case 'findOccurrences':
|
||||
await this.findOccurrences(msg.id, msg.version, msg.offset)
|
||||
return
|
||||
case 'openRepoLink':
|
||||
await this.openRepoLink(msg.path, msg.symbol)
|
||||
await this.openRepoLink(msg.path, msg.symbol, msg.block)
|
||||
return
|
||||
case 'checkRepoLinks':
|
||||
await this.checkRepoLinks(msg.id, msg.paths, msg.symbols)
|
||||
await this.checkRepoLinks(msg.id, msg.paths, msg.references)
|
||||
return
|
||||
case 'checkRepoImages':
|
||||
await this.checkRepoImages(msg.id, msg.paths)
|
||||
@@ -770,10 +770,10 @@ class Preview {
|
||||
}
|
||||
|
||||
/** 回答 webview 第 id 次要注释里的链接指的符号的卡片:符号同点这个链接时找的那个(symbolTarget),找不到没有。 */
|
||||
private async peekLink(id: number, rel: string | undefined, symbol: string): Promise<void> {
|
||||
private async peekLink(id: number, rel: string | undefined, symbol: string, block: string | undefined): Promise<void> {
|
||||
let peek: DefinitionPeek | undefined
|
||||
try {
|
||||
const target = await this.symbolTarget(rel, symbol)
|
||||
const target = await this.symbolTarget(rel, symbol, block)
|
||||
peek = target && await this.peekAt(Uri.parse(target.uri), target.selection.startIndex)
|
||||
}
|
||||
catch (err) {
|
||||
@@ -843,8 +843,8 @@ class Preview {
|
||||
}
|
||||
}
|
||||
|
||||
/** 预览里这批仓库路径,哪些文件或目录不在;这批不带路径的引用,哪些找不到。 */
|
||||
private async checkRepoLinks(id: number, paths: string[], symbols: string[]): Promise<void> {
|
||||
/** 预览里这批仓库路径,哪些文件或目录不在;这批不带路径的引用,各按写在哪块找,哪些找不到。 */
|
||||
private async checkRepoLinks(id: number, paths: string[], references: SymbolReference[]): Promise<void> {
|
||||
const root = await repoRootOf(this.document.uri.fsPath)
|
||||
const asked = new Map<string, Promise<SymbolNode[]>>()
|
||||
const symbolsOf = (uri: string): Promise<SymbolNode[]> => {
|
||||
@@ -858,10 +858,10 @@ class Preview {
|
||||
return rel
|
||||
return undefined
|
||||
})),
|
||||
Promise.all(symbols.map(async symbol => await this.resolveSymbolLink(symbol, symbolsOf) ? undefined : symbol)),
|
||||
Promise.all(references.map(async reference => await this.resolveSymbolLink(reference, symbolsOf) ? undefined : reference)),
|
||||
])
|
||||
if (this.ready)
|
||||
this.post({ type: 'repoLinksChecked', id, missing: missing.filter(rel => rel !== undefined), unresolved: unresolved.filter(symbol => symbol !== undefined) })
|
||||
this.post({ type: 'repoLinksChecked', id, missing: missing.filter(rel => rel !== undefined), unresolved: unresolved.filter(reference => reference !== undefined) })
|
||||
}
|
||||
|
||||
/** 仓库根放进这个预览能读的本地目录,图片地址才能加载。真实位置和 git 给的路径不一样时两个都放。 */
|
||||
@@ -905,19 +905,19 @@ class Preview {
|
||||
|
||||
/**
|
||||
* 打开注释里的仓库链接:目录在代码阅读侧栏的树上定位,文件用编辑器打开。
|
||||
* 带了符号就按文档符号的名字跳过去;文件或符号没有就不打开。没有路径的是不带路径的引用,按这份文件找(见 resolveSymbolLink)。
|
||||
* 带了符号就按文档符号的名字跳过去;文件或符号没有就不打开。没有路径的是不带路径的引用,按这份文件、从它写在的那块(block)找(见 resolveSymbolLink)。
|
||||
*/
|
||||
private async openRepoLink(rel: string | undefined, symbol?: string): Promise<void> {
|
||||
private async openRepoLink(rel: string | undefined, symbol?: string, block?: string): Promise<void> {
|
||||
const link = rel ?? `[${symbol}]`
|
||||
try {
|
||||
if (symbol !== undefined) {
|
||||
const target = await this.symbolTarget(rel, symbol)
|
||||
const target = await this.symbolTarget(rel, symbol, block)
|
||||
if (!target) {
|
||||
this.markRepoLinkBroken(rel, symbol)
|
||||
this.markRepoLinkBroken(rel, symbol, block)
|
||||
return
|
||||
}
|
||||
await this.showLinkTarget(target)
|
||||
this.markRepoLinkOpened(rel, symbol)
|
||||
this.markRepoLinkOpened(rel, symbol, block)
|
||||
return
|
||||
}
|
||||
if (rel === undefined)
|
||||
@@ -946,12 +946,12 @@ class Preview {
|
||||
}
|
||||
|
||||
/**
|
||||
* 链接指的符号在哪:没有 path 的是不带路径的引用,按这份文件找(resolveSymbolLink);有 path 的在那个文件的文档符号里找(locateRepoSymbol)。
|
||||
* 文件没有、是目录、符号没有时没有。点链接、悬停链接看定义卡片都按它找。
|
||||
* 链接指的符号在哪:没有 path 的是不带路径的引用,按这份文件、从它写在的那块(block)找(resolveSymbolLink);
|
||||
* 有 path 的在那个文件的文档符号里找(locateRepoSymbol)。文件没有、是目录、符号没有时没有。点链接、悬停链接看定义卡片都按它找。
|
||||
*/
|
||||
private async symbolTarget(rel: string | undefined, symbol: string): Promise<LinkTarget | undefined> {
|
||||
private async symbolTarget(rel: string | undefined, symbol: string, block: string | undefined): Promise<LinkTarget | undefined> {
|
||||
if (rel === undefined)
|
||||
return this.resolveSymbolLink(symbol)
|
||||
return this.resolveSymbolLink({ symbol, block })
|
||||
const root = await repoRootOf(this.document.uri.fsPath)
|
||||
const entry = root === undefined ? undefined : await resolveRepoEntry(root, rel)
|
||||
return entry && !entry.isDirectory ? this.locateRepoSymbol(Uri.file(entry.fsPath), symbol) : undefined
|
||||
@@ -964,13 +964,15 @@ class Preview {
|
||||
}
|
||||
|
||||
/**
|
||||
* 注释里不带路径的引用(`[Name]`、`[Class.member]`)指到哪:本文件定义的优先,再是导入进来的(见 symbolLinks.ts)。
|
||||
* 注释里不带路径的引用(`[Name]`、`[Class.member]`)指到哪:从它写在的那块(block,在眼下这份块树里找)的声明起一层层往外找本文件定义的,
|
||||
* 再是导入进来的(见 symbolLinks.ts)。检查坏链接、点击、悬停看定义卡片都走这里。
|
||||
* 文档符号同仓库链接找符号那一套(symbols,一批引用一起问时共用一份),导入的名字问语言扩展它的定义(同跳到定义),定义几处取第一处。
|
||||
*/
|
||||
private async resolveSymbolLink(symbol: string, symbols: (uri: string) => Promise<SymbolNode[]> = documentSymbolNodes): Promise<LinkTarget | undefined> {
|
||||
const { document } = this
|
||||
private async resolveSymbolLink({ symbol, block }: SymbolReference, symbols: (uri: string) => Promise<SymbolNode[]> = documentSymbolNodes): Promise<LinkTarget | undefined> {
|
||||
const { document, sent } = this
|
||||
const at = block === undefined || !sent ? undefined : referenceScope(sent.blocks, block)
|
||||
try {
|
||||
return await resolveReference(symbol, this.imports, {
|
||||
return await resolveReference(symbol, at, this.imports, {
|
||||
uri: document.uri.toString(),
|
||||
symbols,
|
||||
definition: async (offset) => {
|
||||
@@ -998,14 +1000,14 @@ class Preview {
|
||||
await this.openElsewhere(doc.uri, { range: new Range(doc.positionAt(selection.startIndex), doc.positionAt(selection.endIndex)), offset: selection.startIndex })
|
||||
}
|
||||
|
||||
private markRepoLinkBroken(path: string | undefined, symbol?: string): void {
|
||||
private markRepoLinkBroken(path: string | undefined, symbol?: string, block?: string): void {
|
||||
if (this.ready)
|
||||
this.post({ type: 'repoLinkBroken', ...(path !== undefined && { path }), ...(symbol !== undefined && { symbol }) })
|
||||
this.post({ type: 'repoLinkBroken', ...(path !== undefined && { path }), ...(symbol !== undefined && { symbol }), ...(block !== undefined && { block }) })
|
||||
}
|
||||
|
||||
private markRepoLinkOpened(path: string | undefined, symbol?: string): void {
|
||||
private markRepoLinkOpened(path: string | undefined, symbol?: string, block?: string): void {
|
||||
if (this.ready)
|
||||
this.post({ type: 'repoLinkOpened', ...(path !== undefined && { path }), ...(symbol !== undefined && { symbol }) })
|
||||
this.post({ type: 'repoLinkOpened', ...(path !== undefined && { path }), ...(symbol !== undefined && { symbol }), ...(block !== undefined && { block }) })
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import type { BlockView } from './blockView'
|
||||
import type { ReferenceSources, SymbolNode } from './symbolLinks'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { createPythonAdapter } from './languages/python'
|
||||
import { createTypeScriptAdapter } from './languages/typescript'
|
||||
import { importsIn } from './libraryRules'
|
||||
import { importedNames, resolveReference, splitReference } from './symbolLinks'
|
||||
import { importedNames, referenceScope, resolveReference, splitReference } from './symbolLinks'
|
||||
import { readFixture, WASM_DIR, withBlocks } from './testSupport'
|
||||
|
||||
const py = createPythonAdapter(WASM_DIR)
|
||||
@@ -40,14 +41,14 @@ describe('不带路径的引用:指到哪', () => {
|
||||
|
||||
it('本文件定义的优先于导入的;不问语言扩展', async () => {
|
||||
const found = sources({ 'file:///here.py': [symbol('Agent', 100, 200)] }, { 20: { uri: 'file:///models.py', at: 5, length: 5 } })
|
||||
expect(await resolveReference('Agent', imports, found)).toEqual({ uri: 'file:///here.py', selection: { startIndex: 100, endIndex: 105 } })
|
||||
expect(await resolveReference('Agent', undefined, imports, found)).toEqual({ uri: 'file:///here.py', selection: { startIndex: 100, endIndex: 105 } })
|
||||
expect(found.asked).toEqual([])
|
||||
})
|
||||
|
||||
it('本文件的 Class.member 在类底下找成员;类有、成员没有就是找不到,不再去导入里找', async () => {
|
||||
const here = sources({ 'file:///here.py': [symbol('Agent', 100, 200, [symbol('owner_id', 120, 140)])] }, { 20: { uri: 'file:///models.py', at: 5, length: 5 } })
|
||||
expect(await resolveReference('Agent.owner_id', imports, here)).toEqual({ uri: 'file:///here.py', selection: { startIndex: 120, endIndex: 128 } })
|
||||
expect(await resolveReference('Agent.missing', imports, here)).toBeUndefined()
|
||||
expect(await resolveReference('Agent.owner_id', undefined, imports, here)).toEqual({ uri: 'file:///here.py', selection: { startIndex: 120, endIndex: 128 } })
|
||||
expect(await resolveReference('Agent.missing', undefined, imports, here)).toBeUndefined()
|
||||
expect(here.asked).toEqual([])
|
||||
})
|
||||
|
||||
@@ -57,21 +58,101 @@ describe('不带路径的引用:指到哪', () => {
|
||||
'file:///models.py': [symbol('Other', 0, 4, [symbol('owner_id', 1, 3)]), symbol('Agent', 10, 90, [symbol('owner_id', 30, 50)])],
|
||||
}
|
||||
const found = sources(files, { 20: { uri: 'file:///models.py', at: 10, length: 5 } })
|
||||
expect(await resolveReference('Agent', imports, found)).toEqual({ uri: 'file:///models.py', selection: { startIndex: 10, endIndex: 15 } })
|
||||
expect(await resolveReference('Agent.owner_id', imports, found)).toEqual({ uri: 'file:///models.py', selection: { startIndex: 30, endIndex: 38 } })
|
||||
expect(await resolveReference('Agent', undefined, imports, found)).toEqual({ uri: 'file:///models.py', selection: { startIndex: 10, endIndex: 15 } })
|
||||
expect(await resolveReference('Agent.owner_id', undefined, imports, found)).toEqual({ uri: 'file:///models.py', selection: { startIndex: 30, endIndex: 38 } })
|
||||
expect(found.asked).toEqual([20, 20])
|
||||
})
|
||||
|
||||
it('导入的是整个模块(定义落在文件开头)时成员从顶层找', async () => {
|
||||
const files = { 'file:///errors.py': [symbol('NotFound', 40, 80)] }
|
||||
const found = sources(files, { 7: { uri: 'file:///errors.py', at: 0, length: 0 } })
|
||||
expect(await resolveReference('errors.NotFound', [{ name: 'errors', offset: 7, range: { startIndex: 7, endIndex: 13 } }], found))
|
||||
expect(await resolveReference('errors.NotFound', undefined, [{ name: 'errors', offset: 7, range: { startIndex: 7, endIndex: 13 } }], found))
|
||||
.toEqual({ uri: 'file:///errors.py', selection: { startIndex: 40, endIndex: 48 } })
|
||||
})
|
||||
|
||||
it('本文件没定义、也没导入,或语言扩展答不上:找不到', async () => {
|
||||
expect(await resolveReference('Nowhere', imports, sources({}))).toBeUndefined()
|
||||
expect(await resolveReference('Agent', imports, sources({}))).toBeUndefined()
|
||||
expect(await resolveReference('Nowhere', undefined, imports, sources({}))).toBeUndefined()
|
||||
expect(await resolveReference('Agent', undefined, imports, sources({}))).toBeUndefined()
|
||||
})
|
||||
})
|
||||
|
||||
describe('不带路径的引用:从被注释的声明起一层层往外找(KDoc)', () => {
|
||||
const imports = [{ name: 'Agent', offset: 20, range: { startIndex: 20, endIndex: 25 } }]
|
||||
/** services.py 的样子:AgentService 里套着类 Config 和几个方法,get_default_agent 里有个局部变量;顶层还有个同名的 helper。 */
|
||||
const here = (): ReturnType<typeof sources> => sources({
|
||||
'file:///here.py': [
|
||||
symbol('Agent', 20, 25),
|
||||
symbol('AgentService', 100, 900, [
|
||||
symbol('Config', 120, 180, [symbol('timeout', 140, 150)]),
|
||||
symbol('get_agent_admitted', 200, 300),
|
||||
symbol('get_default_agent', 400, 500, [symbol('agent', 450, 460)]),
|
||||
symbol('helper', 600, 700),
|
||||
]),
|
||||
symbol('helper', 950, 990),
|
||||
],
|
||||
}, { 20: { uri: 'file:///models.py', at: 10, length: 5 } })
|
||||
/** 各个声明的名字在源码里的下标:注释写在它的说明里时从这儿往外找。 */
|
||||
const SERVICE = 100
|
||||
const DEFAULT_AGENT = 400
|
||||
const HELPER = 950
|
||||
const at = (uri: string, start: number, name: string): unknown => ({ uri, selection: { startIndex: start, endIndex: start + name.length } })
|
||||
|
||||
it('方法说明里的 [同类的另一个方法] 找得到', async () => {
|
||||
expect(await resolveReference('get_agent_admitted', DEFAULT_AGENT, imports, here())).toEqual(at('file:///here.py', 200, 'get_agent_admitted'))
|
||||
})
|
||||
|
||||
it('类说明里的 [成员] 找得到;方法说明里的 [自己里面的名字] 也找得到', async () => {
|
||||
expect(await resolveReference('get_default_agent', SERVICE, imports, here())).toEqual(at('file:///here.py', 400, 'get_default_agent'))
|
||||
expect(await resolveReference('agent', DEFAULT_AGENT, imports, here())).toEqual(at('file:///here.py', 450, 'agent'))
|
||||
})
|
||||
|
||||
it('近的先认:方法说明里的 [helper] 是同类的方法,顶层函数说明里的是顶层函数', async () => {
|
||||
expect(await resolveReference('helper', DEFAULT_AGENT, imports, here())).toEqual(at('file:///here.py', 600, 'helper'))
|
||||
expect(await resolveReference('helper', HELPER, imports, here())).toEqual(at('file:///here.py', 950, 'helper'))
|
||||
})
|
||||
|
||||
it('顶层的、导入的照样找得到;导入语句里的名字不算本文件定义的', async () => {
|
||||
const found = here()
|
||||
expect(await resolveReference('AgentService', DEFAULT_AGENT, imports, found)).toEqual(at('file:///here.py', 100, 'AgentService'))
|
||||
expect(await resolveReference('Agent', DEFAULT_AGENT, imports, found)).toEqual(at('file:///models.py', 10, 'Agent'))
|
||||
expect(found.asked).toEqual([20])
|
||||
})
|
||||
|
||||
it('同一个名字写在两块的说明里:方法说明里找得到同类的方法,顶层函数说明里、不在哪个声明的注释里都找不到', async () => {
|
||||
expect(await resolveReference('get_agent_admitted', DEFAULT_AGENT, imports, here())).toEqual(at('file:///here.py', 200, 'get_agent_admitted'))
|
||||
expect(await resolveReference('get_agent_admitted', HELPER, imports, here())).toBeUndefined()
|
||||
expect(await resolveReference('get_agent_admitted', undefined, imports, here())).toBeUndefined()
|
||||
})
|
||||
|
||||
it('写成 Class.member 的:第一段同样一层层找,再往下找成员', async () => {
|
||||
expect(await resolveReference('Config.timeout', DEFAULT_AGENT, imports, here())).toEqual(at('file:///here.py', 140, 'timeout'))
|
||||
expect(await resolveReference('AgentService.get_agent_admitted', HELPER, imports, here())).toEqual(at('file:///here.py', 200, 'get_agent_admitted'))
|
||||
expect(await resolveReference('Config.timeout', HELPER, imports, here())).toBeUndefined()
|
||||
})
|
||||
})
|
||||
|
||||
describe('注释写在哪块里:从哪个声明起往外找', () => {
|
||||
function block(anchor: string, defines: number | undefined, children: BlockView[] = []): BlockView {
|
||||
return { anchor, children, ...(defines !== undefined && { defines: { startIndex: defines, endIndex: defines + 1 } }) } as BlockView
|
||||
}
|
||||
const blocks = [
|
||||
block('class AgentService', 100, [
|
||||
block('class AgentService > function get_default_agent', 400, [
|
||||
block('class AgentService > function get_default_agent > if @3fa1c2d0', undefined),
|
||||
]),
|
||||
]),
|
||||
block('comment @0b1c2d3e', undefined),
|
||||
]
|
||||
|
||||
it('有名字的块(声明)是它自己的名字;没名字的(注释、语句)取外层最近的有名字的块', () => {
|
||||
expect(referenceScope(blocks, 'class AgentService')).toBe(100)
|
||||
expect(referenceScope(blocks, 'class AgentService > function get_default_agent')).toBe(400)
|
||||
expect(referenceScope(blocks, 'class AgentService > function get_default_agent > if @3fa1c2d0')).toBe(400)
|
||||
})
|
||||
|
||||
it('顶层没名字的块、块树里没有的块:没有,只从文件顶层找', () => {
|
||||
expect(referenceScope(blocks, 'comment @0b1c2d3e')).toBeUndefined()
|
||||
expect(referenceScope(blocks, 'class Gone')).toBeUndefined()
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
import type { BlockView } from './blockView'
|
||||
import type { ImportFacts, ImportItem, Node, SourceRange } from './contract'
|
||||
import { findBlock } from './qa/prompt'
|
||||
|
||||
/**
|
||||
* 注释里不带路径的引用(KDoc 的 `[Name]`、`[Class.member]`)指到哪:先找本文件定义的,再找本文件导入进来的,都没有就找不到。
|
||||
* 符号、定义问语言扩展,交给调用方(预览)去问;这里只管按什么顺序找、怎么拆名字(不碰 VS Code)。
|
||||
* 注释里不带路径的引用(KDoc 的 `[Name]`、`[Class.member]`)指到哪:从这段注释说明的那个声明起,一层层往外找本文件定义的,
|
||||
* 再找本文件导入进来的,都没有就找不到。
|
||||
* 符号、定义问语言扩展,交给调用方(预览)去问;这里只管从哪儿起、按什么顺序找、怎么拆名字(不碰 VS Code)。
|
||||
*/
|
||||
|
||||
/** 本文件某条导入引进来的一个名字:名字、问定义时问的位置(导入语句里那个名字的下标)、这一项在源码里的范围。 */
|
||||
@@ -86,23 +89,35 @@ function deepFind(symbols: readonly SymbolNode[], name: string | undefined): Sym
|
||||
return undefined
|
||||
}
|
||||
|
||||
/** 盖住 offset 的最里层的符号;定义落在文件开头(导入的是整个模块)时没有。 */
|
||||
function innermost(symbols: readonly SymbolNode[], offset: number): SymbolNode | undefined {
|
||||
/** 盖住 offset 的各层符号,从最外层到最里层;offset 不在哪个符号里(比如落在文件开头)时是空的。 */
|
||||
function enclosing(symbols: readonly SymbolNode[], offset: number): SymbolNode[] {
|
||||
const outer = symbols.find(symbol => covers(symbol.range, offset))
|
||||
return outer && (innermost(outer.children, offset) ?? outer)
|
||||
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
|
||||
}
|
||||
|
||||
/**
|
||||
* 引用指到哪,按 KDoc 的规矩:
|
||||
* 1. 本文件定义的:顶层的符号(类、函数、常量、变量)名字对上,`Class.member` 再在它底下找成员;导入语句里的名字不算本文件定义的。
|
||||
* 引用指到哪,按 KDoc 的规矩(不带限定的名字,当它写在这段注释说明的那个声明里面来认):
|
||||
* 1. 本文件定义的,由近到远一层层找:at 是注释说明的那个声明的名字在源码里的下标(见 referenceScope)。先看盖住 at 的最里层符号
|
||||
* (就是这个声明)的直接子符号,再一层层往外看包着它的符号的直接子符号,最后看文件顶层;哪层先有这个名字(`Class.member` 看第一段)
|
||||
* 就是哪个,`Class.member` 再在它底下找成员。导入语句里的名字不算本文件定义的。没有 at(不在哪个声明的注释里)只看文件顶层。
|
||||
* 2. 本文件导入进来的:名字(`Class.member` 的第一段)是某条导入引进来的,问导入语句里那个名字的定义;`Class.member` 再在定义所在文件的
|
||||
* 符号里找:定义落在哪个符号里就在它底下找,落在文件开头(导入的是模块)就从顶层找。
|
||||
* 3. 都没有:找不到。本文件定义了这个名字、却没有这个成员时也是找不到,不再去导入里找。
|
||||
* 3. 都没有:找不到。本文件某层有这个名字、却没有这个成员时也是找不到,不再去更外层、导入里找。
|
||||
*/
|
||||
export async function resolveReference(reference: string, imports: readonly ImportedName[], sources: ReferenceSources): Promise<LinkTarget | undefined> {
|
||||
export async function resolveReference(reference: string, at: number | undefined, imports: readonly ImportedName[], sources: ReferenceSources): Promise<LinkTarget | undefined> {
|
||||
const { head, members } = splitReference(reference)
|
||||
const local = await sources.symbols(sources.uri)
|
||||
const own = local.find(symbol => symbol.name === head && !imports.some(item => covers(item.range, symbol.selection.startIndex)))
|
||||
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 }
|
||||
@@ -112,7 +127,17 @@ export async function resolveReference(reference: string, imports: readonly Impo
|
||||
if (!target || members.length === 0)
|
||||
return target
|
||||
const symbols = await sources.symbols(target.uri)
|
||||
const holder = innermost(symbols, target.selection.startIndex)
|
||||
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 }
|
||||
}
|
||||
|
||||
/**
|
||||
* 注释写在 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
|
||||
}
|
||||
Reference in new issue
Block a user