✨ feat(vscode): 代码阅读鼠标停在名字和注释链接上弹出定义所在那块的卡片,别的文件的也行

This commit is contained in:
cruldra committed 2026-10-08 07:17:26 +08:00
1 parent bc7380c716
commit a48b016d73
20 files changed
+1145 -89

No files matched your search

+101 -22
View File
@@ -13,7 +13,7 @@ import type { ImportedName, LinkTarget, SymbolNode } from '../symbolLinks'
import type { IndexedEntry } from '../workspaceIndex'
import type { BlockFilter } from './blockFilter'
import type { FoundLocation } from './definitions'
import type { ExtensionToWebview, Occurrence, Reveal, WebviewToExtension } from './messages'
import type { DefinitionPeek, ExtensionToWebview, Occurrence, Reveal, WebviewToExtension } from './messages'
import type { TargetSpot } from './openTarget'
import { realpath } from 'node:fs/promises'
import { dirname } from 'node:path'
@@ -33,6 +33,7 @@ import { askPrompt, followUpPrompt } from '../qa/prompt'
import { DocumentLookups } from '../ruleLookups'
import { resolveReference } from '../symbolLinks'
import { definitionAction } from './definitionAction'
import { definitionSiteNames, namesOnCard, peekBlock } from './definitionPeek'
import { definitionKind, findDefinitions, languageDefinitions, pickDefinition, ruleDefinitions } from './definitions'
import { DefinitionSources } from './definitionSources'
import { documentMessage } from './documentMessage'
@@ -259,6 +260,10 @@ class Preview {
this.names.edited(document.version, e.contentChanges.map(change => ({ offset: change.rangeOffset, length: change.rangeLength, text: change.text })))
this.schedulePush()
}),
workspace.onDidChangeConfiguration((e) => {
if (this.ready && e.affectsConfiguration('editor.hover', document))
this.postHoverDelay()
}),
// 页面没 ready 时收不到,等它 ready 再随第一份文档之前补发
filter.subscribe((state) => {
if (this.ready)
@@ -533,6 +538,7 @@ class Preview {
this.ready = true
// 先于文档发:第一份文档按筛选画,不会先整份露出来再藏
this.post({ type: 'filter', ...this.filter.current })
this.postHoverDelay()
this.postQa()
void this.postProfiles()
await this.push()
@@ -579,6 +585,12 @@ class Preview {
case 'goToDefinition':
await this.goToDefinition(msg.version, msg.offset)
return
case 'peekDefinition':
await this.peekDefinition(msg.id, msg.version, msg.offset)
return
case 'peekLink':
await this.peekLink(msg.id, msg.path, msg.symbol)
return
case 'findOccurrences':
await this.findOccurrences(msg.id, msg.version, msg.offset)
return
@@ -644,6 +656,73 @@ class Preview {
}
}
/** VS Code 的悬停延迟(按这份文件取,语言自己的设置优先):页面按它弹、收定义卡片。 */
private postHoverDelay(): void {
const hover = workspace.getConfiguration('editor.hover', this.document)
this.post({ type: 'hoverDelay', delay: hover.get<number>('delay', 300), hidingDelay: hover.get<number>('hidingDelay', 300) })
}
/**
* 回答 webview 第 id 次要定义卡片:offset 处名字的第一处定义所在那块(见 peekAt);指着的名字就是定义本身时没有。
* 下标属于别的文档版本时不答,webview 换了文档版本会收起浮窗
*/
private async peekDefinition(id: number, version: number, offset: number): Promise<void> {
const { document } = this
if (version !== document.version)
return
const [definition] = await findDefinitions(await this.definitions.locate(version, offset))
const own = definition?.uri === document.uri.toString() && definition.offset === offset
const peek = definition && !own ? await this.peekAt(definition.location.uri, definition.offset) : undefined
if (version === document.version && this.ready)
this.post({ type: 'definitionPeeked', id, ...(peek && { peek }) })
}
/** 回答 webview 第 id 次要注释里的链接指的符号的卡片:符号同点这个链接时找的那个(symbolTarget),找不到没有。 */
private async peekLink(id: number, rel: string | undefined, symbol: string): Promise<void> {
let peek: DefinitionPeek | undefined
try {
const target = await this.symbolTarget(rel, symbol)
peek = target && await this.peekAt(Uri.parse(target.uri), target.selection.startIndex)
}
catch (err) {
const link = rel === undefined ? `[${symbol}]` : `${rel}#${symbol}`
logger.add({ level: 'warn', source: 'codeReader', message: `没能找 ${link} 指的符号:${err instanceof Error ? err.message : String(err)}` })
}
if (this.ready)
this.post({ type: 'definitionPeeked', id, ...(peek && { peek }) })
}
/**
* uri 里 offset 处的定义的卡片:盖住它的那块(peekBlock)。同一份文件用最近发出去的那份文档的块和中文名;
* 别的文件打开它、按它的语言排视图数据(同它自己的预览:翻译插件、库规则、项目入口;规则查名字的定义、调用的种类不等),
* 中文名只认起了中文名的定义本身。语言没有代码阅读、解析失败、没有块盖住定义处时没有。
*/
private async peekAt(uri: Uri, offset: number): Promise<DefinitionPeek | undefined> {
try {
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 { sent } = this
const block = sent?.version === target.version ? peekBlock(sent.blocks, offset, line) : undefined
return block && { path, line, block, names: namesOnCard(block, this.names.known(target.version)) }
}
const { message, displayNames } = await documentMessage(
{ languageId: target.languageId, version: target.version, getText: () => target.getText(), fileName: target.fileName, entryNames: this.owner.entries.namesOf(uri) },
this.registry,
this.runtime,
// 那个文件里规则出的错由它自己的预览记
() => {},
)
const block = message.error === undefined ? peekBlock(message.blocks, offset, line) : undefined
return block && { path, line, block, names: namesOnCard(block, definitionSiteNames(displayNames)) }
}
catch (err) {
logger.add({ level: 'warn', source: 'codeReader', message: `没能读出 ${uri.toString()} 里定义的那块:${err instanceof Error ? err.message : String(err)}` })
return undefined
}
}
/**
* 在预览所在的编辑器组打开别的文件,和普通编辑器里 ⌘ + 点击一样:语言有代码阅读的用预览打开,给了 spot 就滚过去、闪这处;
* 其余用 VS Code 默认的打开方式,给了 spot 就选中它(见 openingFor)。目标是否按「预览标签」打开听 VS Code 的设置,
@@ -732,45 +811,33 @@ class Preview {
private async openRepoLink(rel: string | undefined, symbol?: string): Promise<void> {
const link = rel ?? `[${symbol}]`
try {
if (rel === undefined) {
const target = symbol === undefined ? undefined : await this.resolveSymbolLink(symbol)
if (symbol !== undefined) {
const target = await this.symbolTarget(rel, symbol)
if (!target) {
this.markRepoLinkBroken(undefined, symbol)
this.markRepoLinkBroken(rel, symbol)
return
}
await this.showLinkTarget(target)
this.markRepoLinkOpened(undefined, symbol)
this.markRepoLinkOpened(rel, symbol)
return
}
if (rel === undefined)
return
const root = await repoRootOf(this.document.uri.fsPath)
const entry = root === undefined ? undefined : await resolveRepoEntry(root, rel)
if (!entry) {
this.markRepoLinkBroken(rel, symbol)
this.markRepoLinkBroken(rel)
return
}
const uri = Uri.file(entry.fsPath)
if (entry.isDirectory) {
if (symbol !== undefined) {
this.markRepoLinkBroken(rel, symbol)
return
}
if (!await this.owner.revealInTree(uri))
void window.showInformationMessage(`代码阅读:侧栏的树上没有 ${rel}`)
this.markRepoLinkOpened(rel)
return
}
if (symbol === undefined) {
await this.openElsewhere(uri)
this.markRepoLinkOpened(rel)
return
}
const found = await this.locateRepoSymbol(uri, symbol)
if (!found) {
this.markRepoLinkBroken(rel, symbol)
return
}
await this.showLinkTarget(found)
this.markRepoLinkOpened(rel, symbol)
await this.openElsewhere(uri)
this.markRepoLinkOpened(rel)
}
catch (err) {
const message = err instanceof Error ? err.message : String(err)
@@ -779,6 +846,18 @@ class Preview {
}
}
/**
* 链接指的符号在哪:没有 path 的是不带路径的引用,按这份文件找(resolveSymbolLink);有 path 的在那个文件的文档符号里找(locateRepoSymbol)。
* 文件没有、是目录、符号没有时没有。点链接、悬停链接看定义卡片都按它找。
*/
private async symbolTarget(rel: string | undefined, symbol: string): Promise<LinkTarget | undefined> {
if (rel === undefined)
return this.resolveSymbolLink(symbol)
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
}
/** 用文档符号按名字找(`Agent`、`Agent.owner_id`);没有这个名字就没有,不打开到文件顶上。 */
private async locateRepoSymbol(uri: Uri, symbol: string): Promise<LinkTarget | undefined> {
const found = findRepoSymbol(await this.symbolNodes(uri.toString()), symbol)