🐛 fix(vscode): 定义卡片里注释的 [Name] 渲染成链接,按卡片那份文件解析

This commit is contained in:
cruldra committed 2026-10-15 17:36:50 +11:00
1 parent af91b03857
commit bea1f2e679
19 files changed
+319 -151

No files matched your search

+85 -48
View File
@@ -15,7 +15,8 @@ import type { ThemeColors } from '../theme/codeColors'
import type { IndexFind } from '../workspaceIndex'
import type { BlockFilter } from './blockFilter'
import type { FoundLocation } from './definitions'
import type { ChatKind, DefinitionPeek, ExtensionToWebview, LineRange, Occurrence, Reveal, SymbolReference, WebviewToExtension } from './messages'
import type { PeekRead } from './documentReads'
import type { ChatKind, DefinitionPeek, ExtensionToWebview, Occurrence, Reveal, SymbolReference, WebviewToExtension } from './messages'
import type { TargetSpot } from './openTarget'
import type { LinkBase, TopSubject } from './pathPreview'
import type { RepoEntry } from './repoPath'
@@ -89,7 +90,9 @@ export class PreviewEditorProvider implements CustomTextEditorProvider {
/** 调用的名字的定义是哪种块:各预览共用,定义所在的文件同一版只解析一次 */
readonly definitionKinds: DefinitionKinds
/** 悬停看别的文件里的定义时那份文件读成的文档:各预览共用,同一版不再重新解析 */
readonly documentReads: DocumentReads
private readonly documentReads: DocumentReads
/** 别的文件的注释里不带路径的引用解析过的指到哪,一份文件一套(见 [referenceTargets]):各预览共用,文件关掉、没人再用时跟着丢 */
private readonly elsewhereReferences = new WeakMap<TextDocument, ReferenceTargets>()
constructor(
private readonly extensionUri: Uri,
@@ -127,6 +130,23 @@ export class PreviewEditorProvider implements CustomTextEditorProvider {
return window.registerCustomEditorProvider(PREVIEW_VIEW_TYPE, this, { webviewOptions: { retainContextWhenHidden: true } })
}
/** 别的文件 `document` 按它的语言读成的文档(见 [DocumentReads]):悬停看定义时卡片画它的块,卡片里注释中不带路径的引用按它找。 */
readElsewhere(document: TextDocument): Promise<PeekRead> {
const { uri, languageId, version, fileName } = document
return this.documentReads.read(uri.toString(), { languageId, version, getText: () => document.getText(), fileName, entryNames: this.entries.namesOf(uri) })
}
/** 别的文件 `document` 的注释里不带路径的引用 `reference` 指到哪:按那份文件读成的文档找,和它自己的预览找的一样(见 [referenceTargets])。 */
async resolveElsewhere(document: TextDocument, reference: SymbolReference): Promise<ReferenceTarget> {
const { references } = await this.readElsewhere(document)
let targets = this.elsewhereReferences.get(document)
if (!targets) {
targets = referenceTargets(document, this.registry)
this.elsewhereReferences.set(document, targets)
}
return targets.resolve(references, reference)
}
/**
* 用 `show` 打开 `uri` 的预览,把 `request` 交给它:新建的预览解析时带走;`show` 只是切到了已经开着的预览(没有新建)时,交给切到的那个。
*
@@ -266,10 +286,7 @@ class Preview {
offset => this.requests.distance(this.document.positionAt(offset).line),
)
/**
* 注释里不带路径的引用解析过的指到哪(见 [ReferenceTargets])。文档符号问这种语言的扩展(同仓库链接找符号),导入的、代码里用到的名字问语言扩展它的定义
* (同跳到定义,几处取第一处),哪些名字是语言自带的问语言插件([LanguageAdapter.builtins](vscode/src/codeReader/contract.ts#LanguageAdapter.builtins))
*/
/** 这份文档的注释里不带路径的引用解析过的指到哪(见 [referenceTargets]) */
private readonly references: ReferenceTargets
/** 最近一次发出去的文档里语言插件自己认得出的名字(LanguageAdapter.symbols):语言扩展查不到定义、各处出现时用 */
private symbols: { version: number, list: LocalSymbol[] } | undefined
@@ -313,19 +330,7 @@ class Preview {
return found.length > 0 ? found : this.localDefinitions(offset)
},
})
this.references = new ReferenceTargets({
uri: document.uri.toString(),
symbols: documentSymbolNodes,
definition: async (offset) => {
const [found] = await languageDefinitions(document.uri, document.positionAt(offset))
if (!found)
return undefined
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) } }
},
builtin: name => registry.languageFor(document.languageId)?.builtins?.has(name) ?? false,
versionOf: uri => workspace.textDocuments.find(doc => doc.uri.toString() === uri)?.version,
})
this.references = referenceTargets(document, registry)
this.names = new DocumentNames((version, names) => {
if (version === document.version)
this.post({ type: 'displayNames', version, names })
@@ -795,13 +800,13 @@ class Preview {
await this.peekDefinition(msg.id, msg.version, msg.offset)
return
case 'peekLink':
await this.peekLink(msg.id, msg.path, msg.symbol, msg.block)
await this.peekLink(msg.id, msg.path, referenceOf(msg))
return
case 'findOccurrences':
await this.findOccurrences(msg.id, msg.version, msg.offset)
return
case 'openRepoLink':
await this.openRepoLink(msg.path, msg.symbol, msg.block, msg.lines)
await this.openRepoLink(msg)
return
case 'checkRepoLinks':
await this.checkRepoLinks(msg.id, msg.paths, msg.references)
@@ -899,14 +904,14 @@ class Preview {
}
/** 回答 webview 第 id 次要注释里的链接指的符号的卡片:符号同点这个链接时找的那个(symbolTarget),找不到、是语言自带的名字时没有。 */
private async peekLink(id: number, rel: string | undefined, symbol: string, block: string | undefined): Promise<void> {
private async peekLink(id: number, rel: string | undefined, reference: SymbolReference): Promise<void> {
let peek: DefinitionPeek | undefined
try {
const target = await this.symbolTarget(rel, symbol, block)
const target = await this.symbolTarget(rel, reference)
peek = target && target !== 'builtin' ? await this.peekAt(Uri.parse(target.uri), target.selection.startIndex) : undefined
}
catch (err) {
const link = rel === undefined ? `[${symbol}]` : `${rel}#${symbol}`
const link = rel === undefined ? `[${reference.symbol}]` : `${rel}#${reference.symbol}`
logger.add({ level: 'warn', source: 'codeReader', message: `没能找 ${link} 指的符号:${err instanceof Error ? err.message : String(err)}` })
}
this.post({ type: 'definitionPeeked', id, ...(peek && { peek }) })
@@ -922,19 +927,17 @@ class Preview {
const target = await workspace.openTextDocument(uri)
const line = target.positionAt(offset).line
const path = workspace.asRelativePath(uri)
if (uri.toString() === this.document.uri.toString()) {
const file = uri.toString()
if (file === this.document.uri.toString()) {
const { sent } = this
if (sent?.version !== target.version)
return undefined
const block = peekBlock(sent.blocks, offset, line)
return block && { path, line, block: pagePeekBlock(block, sent.source), names: namesOnCard(block, this.names.known(target.version)) }
return block && { path, uri: file, line, block: pagePeekBlock(block, sent.source), names: namesOnCard(block, this.names.known(target.version)) }
}
const { message, displayNames } = await this.owner.documentReads.read(
uri.toString(),
{ languageId: target.languageId, version: target.version, getText: () => target.getText(), fileName: target.fileName, entryNames: this.owner.entries.namesOf(uri) },
)
const { message, displayNames } = await this.owner.readElsewhere(target)
const block = message.error === undefined ? peekBlock(message.blocks, offset, line) : undefined
return block && { path, line, block: pagePeekBlock(block, message.source), names: namesOnCard(block, definitionSiteNames(displayNames)) }
return block && { path, uri: file, line, block: pagePeekBlock(block, message.source), names: namesOnCard(block, definitionSiteNames(displayNames)) }
}
catch (err) {
logger.add({ level: 'warn', source: 'codeReader', message: `没能读出 ${uri.toString()} 里定义的那块:${err instanceof Error ? err.message : String(err)}` })
@@ -970,7 +973,10 @@ class Preview {
}
}
/** 预览里这批仓库路径(从哪儿算见 [linkBaseOf]),哪些文件或目录不在;这批不带路径的引用,各按写在哪块找,哪些找不到、哪些是语言自带的名字(页面当普通文字)。 */
/**
* 预览里这批仓库路径(从哪儿算见 [linkBaseOf]),哪些文件或目录不在;这批不带路径的引用,各按写在哪份文件、哪块找(见 [resolveSymbolLink]),
* 哪些找不到、哪些是语言自带的名字(页面当普通文字)。
*/
private async checkRepoLinks(id: number, paths: string[], references: SymbolReference[]): Promise<void> {
const base = await linkBaseOf(this.document.uri)
const [missing, resolved] = await Promise.all([
@@ -1005,23 +1011,24 @@ class Preview {
/**
* 打开注释、问 AI 的回答里的仓库链接:目录在代码阅读侧栏的树上定位,文件用编辑器打开。
* 带了符号就按文档符号的名字跳过去,带了几行(`lines`)就跳到这几行(见 [linesSpot]);文件或符号没有就不打开。
* 没有路径的是不带路径的引用,按这份文件、从它写在的那块(`block`)找(见 [resolveSymbolLink]);它是语言自带的名字时不打开,告诉页面当普通文字。
* 没有路径的是不带路径的引用,按它写在的那份文件(`file`)、那块(`block`)找(见 [resolveSymbolLink]);它是语言自带的名字时不打开,告诉页面当普通文字。
*/
private async openRepoLink(rel: string | undefined, symbol?: string, block?: string, lines?: LineRange): Promise<void> {
private async openRepoLink({ path: rel, symbol, block, file, lines }: Extract<WebviewToExtension, { type: 'openRepoLink' }>): Promise<void> {
const link = rel ?? `[${symbol}]`
try {
if (symbol !== undefined) {
const target = await this.symbolTarget(rel, symbol, block)
const reference = referenceOf({ symbol, block, file })
const target = await this.symbolTarget(rel, reference)
if (target === 'builtin') {
this.post({ type: 'repoLinkBuiltin', symbol, ...(block !== undefined && { block }) })
this.post({ type: 'repoLinkBuiltin', ...reference })
return
}
if (!target) {
this.markRepoLinkBroken(rel, symbol, block)
this.markRepoLinkBroken(rel, reference)
return
}
await this.showLinkTarget(target)
this.markRepoLinkOpened(rel, symbol, block)
this.markRepoLinkOpened(rel, reference)
return
}
if (rel === undefined)
@@ -1050,15 +1057,15 @@ class Preview {
}
/**
* 链接指的符号在哪:没有 path 的是不带路径的引用,按这份文件、从它写在的那块(block)找(resolveSymbolLink);
* 链接指的符号在哪:没有 path 的是不带路径的引用,按它写在的那份文件、那块找([resolveSymbolLink]);
* 有 path 的在那个文件的文档符号里找(locateRepoSymbol)。文件没有、是目录、符号没有时没有;不带路径的引用是语言自带的名字时是 `'builtin'`。
* 点链接、悬停链接看定义卡片都按它找。
*/
private async symbolTarget(rel: string | undefined, symbol: string, block: string | undefined): Promise<ReferenceTarget> {
private async symbolTarget(rel: string | undefined, reference: SymbolReference): Promise<ReferenceTarget> {
if (rel === undefined)
return this.resolveSymbolLink({ symbol, block })
return this.resolveSymbolLink(reference)
const entry = await linkEntry(await linkBaseOf(this.document.uri), rel)
return entry && !entry.isDirectory ? this.locateRepoSymbol(Uri.file(entry.fsPath), symbol) : undefined
return entry && !entry.isDirectory ? this.locateRepoSymbol(Uri.file(entry.fsPath), reference.symbol) : undefined
}
/** 用文档符号按名字找(`Agent`、`Agent.owner_id`);没有这个名字就没有,不打开到文件顶上。 */
@@ -1070,13 +1077,17 @@ class Preview {
/**
* 注释里不带路径的引用(`[Name]`、`[Class.member]`)指到哪,检查坏链接、点击、悬停看定义卡片都走这里;解析过的记在 [ReferenceTargets] 里,不再问语言扩展。
*
* 从它写在的那块(在最近发出去的块树里找)的声明起一层层往外找本文件定义的,再找导入的、代码里用到的语言自带的名字,
* 按它写在的那份文件找([SymbolReference] 的 `file`):没写、写的就是这份文档的,按最近发出去的块树和这份文档的导入、用到的名字找;
* 别的文件的(定义卡片里的注释)按那份文件读成的文档找(见 [PreviewEditorProvider.resolveElsewhere])。
* 都是从它写在的那块的声明起一层层往外找那份文件定义的,再找导入的、代码里用到的语言自带的名字,
* 语言自带又问不到定义的是 `'builtin'`(见 [解析引用](vscode/src/codeReader/symbolLinks.ts#resolveReference))。出错时记一笔,当找不到。
*/
private async resolveSymbolLink(reference: SymbolReference): Promise<ReferenceTarget> {
const { document, sent, imports, uses } = this
try {
return await this.references.resolve({ version: document.version, sent, imports, uses }, reference)
if (reference.file === undefined || reference.file === document.uri.toString())
return await this.references.resolve({ version: document.version, sent, imports, uses }, reference)
return await this.owner.resolveElsewhere(await workspace.openTextDocument(Uri.parse(reference.file)), reference)
}
catch (err) {
logger.add({ level: 'warn', source: 'codeReader', message: `没能找注释里的引用 [${reference.symbol}]:${err instanceof Error ? err.message : String(err)}` })
@@ -1099,12 +1110,12 @@ class Preview {
await this.openElsewhere(uri, spot)
}
private markRepoLinkBroken(path: string | undefined, symbol?: string, block?: string): void {
this.post({ type: 'repoLinkBroken', ...(path !== undefined && { path }), ...(symbol !== undefined && { symbol }), ...(block !== undefined && { block }) })
private markRepoLinkBroken(path: string | undefined, reference?: SymbolReference): void {
this.post({ type: 'repoLinkBroken', ...(path !== undefined && { path }), ...reference })
}
private markRepoLinkOpened(path: string | undefined, symbol?: string, block?: string): void {
this.post({ type: 'repoLinkOpened', ...(path !== undefined && { path }), ...(symbol !== undefined && { symbol }), ...(block !== undefined && { block }) })
private markRepoLinkOpened(path: string | undefined, reference?: SymbolReference): void {
this.post({ type: 'repoLinkOpened', ...(path !== undefined && { path }), ...reference })
}
/**
@@ -1165,6 +1176,32 @@ async function linkEntry(base: LinkBase, rel: string): Promise<RepoEntry | undef
return place && resolveRepoEntry(place.root, place.rel)
}
/**
* `document` 这份文件的注释里不带路径的引用指到哪,解析过的记着(见 [ReferenceTargets])。文档符号问这种语言的扩展(同仓库链接找符号),
* 导入的、代码里用到的名字问语言扩展它在这份文件里的定义(同跳到定义,几处取第一处),
* 哪些名字是语言自带的问这份文件的语言插件([LanguageAdapter.builtins](vscode/src/codeReader/contract.ts#LanguageAdapter.builtins))。
*/
function referenceTargets(document: TextDocument, registry: Pick<CodeReaderRegistry, 'languageFor'>): ReferenceTargets {
return new ReferenceTargets({
uri: document.uri.toString(),
symbols: documentSymbolNodes,
definition: async (offset) => {
const [found] = await languageDefinitions(document.uri, document.positionAt(offset))
if (!found)
return undefined
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) } }
},
builtin: name => registry.languageFor(document.languageId)?.builtins?.has(name) ?? false,
versionOf: uri => workspace.textDocuments.find(doc => doc.uri.toString() === uri)?.version,
})
}
/** 消息里带的那条引用:只取 `symbol`、`block`、`file` 里写了的,报回页面时和页面问的那条一样。 */
function referenceOf({ symbol, block, file }: SymbolReference): SymbolReference {
return { symbol, ...(block !== undefined && { block }), ...(file !== undefined && { file }) }
}
/** 写回重写好的注释:换掉原来那几行(同块编辑),或在块上面插进新写的几行。 */
async function writeCommentEdit(document: TextDocument, edit: CommentEdit): Promise<WriteResult> {
if (edit.kind === 'replace')