🐛 fix(vscode): 代码阅读注释里引用的语言内置名不再标成找不到:代码里写到的(raise ValueError)问那处的定义、点了跳到语言自带的定义,没写到的当普通文字不能点;内置名单由语言插件给(Python 补全 dir(builtins),TypeScript 是 ECMAScript 标准全局对象)

This commit is contained in:
cruldra committed 2026-10-14 15:10:48 +11:00
1 parent 49a3854390
commit e3c8c1b118
14 files changed
+544 -98

No files matched your search

+5 -2
View File
@@ -211,9 +211,11 @@ export interface StickyScrollSettings {
* diff:这份文件相比对比基准改了什么(审查工作副本里的、用「未提交的更改」文件行上的「打开更改」打开的有改动的才有,见 [对比基准](vscode/src/codeReader/diff/diffBase.ts#DiffBase)):每发一份文档之后算好了补发,
* 基准变了(提交后 HEAD 动了、换了审查的工单、文件有了改动、这个标签页换了从哪儿打开)再发;null 是不比了(退出了审查、改动提交了、从别处点开了)
* openedFrom:这个标签页是从哪儿打开的,页面记进自己的状态,VS Code 重启后恢复这个标签页时随 ready 报回来;ready 后发一次,开着时又从侧栏点开、换了从哪儿打开的再发
* repoLinksChecked:id 那次 checkRepoLinks 的回答,missing 是仓库里没有的路径,unresolved 是找不到的不带路径的引用(原样带回问的那几条)
* repoLinksChecked:id 那次 checkRepoLinks 的回答,missing 是仓库里没有的路径,unresolved 是找不到的不带路径的引用,
* builtin 是语言自带、又问不到定义的不带路径的引用(Python 的 `ValueError`、`str`,页面当普通文字);两样都原样带回问的那几条
* repoLinkBroken:这个仓库链接打不开。没有 symbol 是文件或目录没有;有 symbol 是文件在、这个符号没有;没有 path 是不带路径的引用找不到,
* block 同 openRepoLink
* repoLinkBuiltin:点的不带路径的引用是语言自带、又问不到定义的名字,没打开,页面当普通文字;symbol、block 同 openRepoLink
* repoLinkOpened:这个仓库链接打开了。路径在;带了 symbol 时这个符号也在;没有 path 是不带路径的引用找到了,block 同 openRepoLink
* repoImagesChecked:id 那次 checkRepoImages 的回答。src 是预览里能直接用的图片地址;没有 src 是文件不在
* callTreeCounts:version 这份文档里,每个能看调用树的块(BlockView.callTree)落在几棵调用树里(只包括大于 0 的)。不是页面上这个版本的不理
@@ -242,8 +244,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: SymbolReference[] }
| { type: 'repoLinksChecked', id: number, missing: string[], unresolved: SymbolReference[], builtin: SymbolReference[] }
| { type: 'repoLinkBroken', path?: string, symbol?: string, block?: string }
| ({ type: 'repoLinkBuiltin' } & SymbolReference)
| { type: 'repoLinkOpened', path?: string, symbol?: string, block?: string }
| { type: 'repoImagesChecked', id: number, images: { path: string, src?: string }[] }
| { type: 'focusPathAsk' }
@@ -567,11 +567,26 @@ describe('注释里不带路径的引用按它写在哪块的注释里找', () =
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 }] })
expect(page.posted()).toContainEqual({ type: 'repoLinksChecked', id: 1, missing: [], unresolved: [{ symbol: 'get_agent_admitted', block: HELPER }], builtin: [] })
await page.send({ type: 'openRepoLink', symbol: 'get_agent_admitted', block: HELPER })
expect(page.posted()).toContainEqual({ type: 'repoLinkBroken', symbol: 'get_agent_admitted', block: HELPER })
})
it('语言自带、代码里没写到的名字:检查时报成 builtin,点了报 repoLinkBuiltin,不标坏', async () => {
vi.mocked(documentSymbolNodes).mockResolvedValue([])
const { provider, document } = opening(undefined, undefined, () => ({ builtins: new Set(['Error']) }))
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: 'Error', block: HELPER }, { symbol: 'Missing', block: HELPER }] })
expect(page.posted()).toContainEqual({ type: 'repoLinksChecked', id: 1, missing: [], unresolved: [{ symbol: 'Missing', block: HELPER }], builtin: [{ symbol: 'Error', block: HELPER }] })
await page.send({ type: 'openRepoLink', symbol: 'Error', block: HELPER })
expect(page.posted()).toContainEqual({ type: 'repoLinkBuiltin', symbol: 'Error', block: HELPER })
expect(page.posted()).not.toContainEqual(expect.objectContaining({ type: 'repoLinkBroken' }))
})
})
describe('点了带几行的仓库链接', () => {
@@ -624,7 +639,7 @@ describe('点了带几行的仓库链接', () => {
it('问哪些路径没有时,带几行的链接按文件问', async () => {
const { page } = await linked()
await page.send({ type: 'checkRepoLinks', id: 3, paths: ['web/a.ts', 'web/gone.ts'], references: [] })
expect(page.posted()).toEqual([{ type: 'repoLinksChecked', id: 3, missing: ['web/gone.ts'], unresolved: [] }])
expect(page.posted()).toEqual([{ type: 'repoLinksChecked', id: 3, missing: ['web/gone.ts'], unresolved: [], builtin: [] }])
})
})
+35 -17
View File
@@ -1,7 +1,7 @@
import type { CustomTextEditorProvider, Disposable, TextDocument, TextDocumentShowOptions, WebviewPanel } from 'vscode'
import type { ClaudeImage } from '../../cc/spawnClaude'
import type { CommentEdit, CommentJob, WriteResult } from '../commentRewrite'
import type { BlockKind, LocalSymbol, RuleLocation, RuleReference } from '../contract'
import type { BlockKind, LocalSymbol, RuleLocation, RuleReference, SourceRange } from '../contract'
import type { OpenedFrom, PreviewDiffBases } from '../diff/diffBase'
import type { OldDocument, SentDocument } from '../diff/previewDiff'
import type { WorkspaceEntries } from '../entries/workspaceEntries'
@@ -10,7 +10,7 @@ import type { CodeQa, TopRequest } from '../qa/codeQa'
import type { CodeReaderRegistry } from '../registry'
import type { LookupSources } from '../ruleLookups'
import type { TreeSitterRuntime } from '../runtime'
import type { ImportedName, LinkTarget, SymbolNode } from '../symbolLinks'
import type { ImportedName, LinkTarget, ReferenceTarget, SymbolNode } from '../symbolLinks'
import type { ThemeColors } from '../theme/codeColors'
import type { IndexFind } from '../workspaceIndex'
import type { BlockFilter } from './blockFilter'
@@ -37,7 +37,7 @@ import { NOT_REWRITABLE } from '../qa/codeQa'
import { PATH_ASK_ANCHOR } from '../qa/pathQa'
import { askPrompt, followUpPrompt } from '../qa/prompt'
import { DocumentLookups } from '../ruleLookups'
import { referenceScope, resolveReference } from '../symbolLinks'
import { outsideUses, referenceScope, resolveReference } from '../symbolLinks'
import { definitionAction } from './definitionAction'
import { definitionSiteNames, namesOnCard, pagePeekBlock, peekBlock } from './definitionPeek'
import { definitionKind, findDefinitions, languageDefinitions, pickDefinition, ruleDefinitions } from './definitions'
@@ -245,6 +245,8 @@ class Preview {
private symbols: { version: number, list: LocalSymbol[] } | undefined
/** 最近一次发出去的文档里各条导入引进来的名字:注释里不带路径的引用按它找导入的定义 */
private imports: ImportedName[] = []
/** 最近一次发出去的文档里,代码用到、本文件又没定义的名字各在哪几处(见 [outsideUses]):注释里的引用按它找语言自带的名字的定义 */
private uses = new Map<string, SourceRange[]>()
/** 这份文档各版本显示成中文名的名字 */
private readonly names: DocumentNames
/** 库规则问的名字的定义查到了什么:查到的变了就重发文档 */
@@ -443,6 +445,7 @@ class Preview {
this.sent = message.error === undefined ? { version: message.version, blocks: message.blocks, source } : undefined
this.symbols = { version, list: symbols }
this.imports = imports
this.uses = outsideUses(bindings, source)
const reveal = this.pendingReveal
this.pendingReveal = undefined
this.shown = version
@@ -852,12 +855,12 @@ class Preview {
this.post({ type: 'definitionPeeked', id, ...(peek && { peek }) })
}
/** 回答 webview 第 id 次要注释里的链接指的符号的卡片:符号同点这个链接时找的那个(symbolTarget),找不到没有。 */
/** 回答 webview 第 id 次要注释里的链接指的符号的卡片:符号同点这个链接时找的那个(symbolTarget),找不到、是语言自带的名字时没有。 */
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, block)
peek = target && await this.peekAt(Uri.parse(target.uri), target.selection.startIndex)
peek = target && target !== 'builtin' ? await this.peekAt(Uri.parse(target.uri), target.selection.startIndex) : undefined
}
catch (err) {
const link = rel === undefined ? `[${symbol}]` : `${rel}#${symbol}`
@@ -927,7 +930,7 @@ class Preview {
}
}
/** 预览里这批仓库路径,哪些文件或目录不在;这批不带路径的引用,各按写在哪块找,哪些找不到。 */
/** 预览里这批仓库路径,哪些文件或目录不在;这批不带路径的引用,各按写在哪块找,哪些找不到、哪些是语言自带的名字(页面当普通文字)。 */
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[]>>()
@@ -936,15 +939,21 @@ class Preview {
asked.set(uri, known)
return known
}
const [missing, unresolved] = await Promise.all([
const [missing, resolved] = await Promise.all([
Promise.all(paths.map(async (rel) => {
if (root === undefined || await resolveRepoEntry(root, rel) === undefined)
return rel
return undefined
})),
Promise.all(references.map(async reference => await this.resolveSymbolLink(reference, symbolsOf) ? undefined : reference)),
Promise.all(references.map(async reference => ({ reference, target: await this.resolveSymbolLink(reference, symbolsOf) }))),
])
this.post({ type: 'repoLinksChecked', id, missing: missing.filter(rel => rel !== undefined), unresolved: unresolved.filter(reference => reference !== undefined) })
this.post({
type: 'repoLinksChecked',
id,
missing: missing.filter(rel => rel !== undefined),
unresolved: resolved.filter(({ target }) => target === undefined).map(({ reference }) => reference),
builtin: resolved.filter(({ target }) => target === 'builtin').map(({ reference }) => reference),
})
}
/** 仓库根放进这个预览能读的本地目录,图片地址才能加载。真实位置和 git 给的路径不一样时两个都放。 */
@@ -988,13 +997,17 @@ class Preview {
/**
* 打开注释、问 AI 的回答里的仓库链接:目录在代码阅读侧栏的树上定位,文件用编辑器打开。
* 带了符号就按文档符号的名字跳过去,带了几行(`lines`)就跳到这几行(见 [linesSpot]);文件或符号没有就不打开。
* 没有路径的是不带路径的引用,按这份文件、从它写在的那块(`block`)找(见 [resolveSymbolLink])。
* 没有路径的是不带路径的引用,按这份文件、从它写在的那块(`block`)找(见 [resolveSymbolLink]);它是语言自带的名字时不打开,告诉页面当普通文字。
*/
private async openRepoLink(rel: string | undefined, symbol?: string, block?: string, lines?: LineRange): Promise<void> {
const link = rel ?? `[${symbol}]`
try {
if (symbol !== undefined) {
const target = await this.symbolTarget(rel, symbol, block)
if (target === 'builtin') {
this.post({ type: 'repoLinkBuiltin', symbol, ...(block !== undefined && { block }) })
return
}
if (!target) {
this.markRepoLinkBroken(rel, symbol, block)
return
@@ -1031,9 +1044,10 @@ class Preview {
/**
* 链接指的符号在哪:没有 path 的是不带路径的引用,按这份文件、从它写在的那块(block)找(resolveSymbolLink);
* 有 path 的在那个文件的文档符号里找(locateRepoSymbol)。文件没有、是目录、符号没有时没有。点链接、悬停链接看定义卡片都按它找。
* 有 path 的在那个文件的文档符号里找(locateRepoSymbol)。文件没有、是目录、符号没有时没有;不带路径的引用是语言自带的名字时是 `'builtin'`。
* 点链接、悬停链接看定义卡片都按它找。
*/
private async symbolTarget(rel: string | undefined, symbol: string, block: string | undefined): Promise<LinkTarget | undefined> {
private async symbolTarget(rel: string | undefined, symbol: string, block: string | undefined): Promise<ReferenceTarget> {
if (rel === undefined)
return this.resolveSymbolLink({ symbol, block })
const root = await repoRootOf(this.document.uri.fsPath)
@@ -1048,13 +1062,15 @@ class Preview {
}
/**
* 注释里不带路径的引用(`[Name]`、`[Class.member]`)指到哪:从它写在的那块(block,在眼下这份块树里找)的声明起一层层往外找本文件定义的,
* 再是导入进来的(见 symbolLinks.ts)。检查坏链接、点击、悬停看定义卡片都走这里。
* 文档符号同仓库链接找符号那一套(symbols,一批引用一起问时共用一份),导入的名字问语言扩展它的定义(同跳到定义),定义几处取第一处。
* 注释里不带路径的引用(`[Name]`、`[Class.member]`)指到哪:从它写在的那块(`block`,在眼下这份块树里找)的声明起一层层往外找本文件定义的,
* 再是导入进来的、代码里用到的语言自带的名字,语言自带又问不到定义的是 `'builtin'`(见 [resolveReference])。检查坏链接、点击、悬停看定义卡片都走这里。
* 文档符号同仓库链接找符号那一套(`symbols`,一批引用一起问时共用一份),导入的、代码里用到的名字问语言扩展它的定义(同跳到定义),定义几处取第一处;
* 哪些名字是语言自带的问这门语言的语言插件([LanguageAdapter.builtins](vscode/src/codeReader/contract.ts#LanguageAdapter.builtins))。
*/
private async resolveSymbolLink({ symbol, block }: SymbolReference, symbols: (uri: string) => Promise<SymbolNode[]> = documentSymbolNodes): Promise<LinkTarget | undefined> {
const { document, sent } = this
private async resolveSymbolLink({ symbol, block }: SymbolReference, symbols: (uri: string) => Promise<SymbolNode[]> = documentSymbolNodes): Promise<ReferenceTarget> {
const { document, sent, uses } = this
const at = block === undefined || !sent ? undefined : referenceScope(sent.blocks, block)
const builtins = this.registry.languageFor(document.languageId)?.builtins
try {
return await resolveReference(symbol, at, this.imports, {
uri: document.uri.toString(),
@@ -1066,6 +1082,8 @@ class Preview {
const doc = await workspace.openTextDocument(found.uri)
return { uri: found.uri.toString(), selection: { startIndex: doc.offsetAt(found.range.start), endIndex: doc.offsetAt(found.range.end) } }
},
uses: name => uses.get(name) ?? [],
builtin: name => builtins?.has(name) ?? false,
})
}
catch (err) {