From bea1f2e679d94fcc6067488defc4e836a5d48bb0 Mon Sep 17 00:00:00 2001 From: cruldra Date: Thu, 15 Oct 2026 17:36:50 +1100 Subject: [PATCH] =?UTF-8?q?=F0=9F=90=9B=20fix(vscode):=20=E5=AE=9A?= =?UTF-8?q?=E4=B9=89=E5=8D=A1=E7=89=87=E9=87=8C=E6=B3=A8=E9=87=8A=E7=9A=84?= =?UTF-8?q?=20[Name]=20=E6=B8=B2=E6=9F=93=E6=88=90=E9=93=BE=E6=8E=A5?= =?UTF-8?q?=EF=BC=8C=E6=8C=89=E5=8D=A1=E7=89=87=E9=82=A3=E4=BB=BD=E6=96=87?= =?UTF-8?q?=E4=BB=B6=E8=A7=A3=E6=9E=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/superpowers/specs/code-reader/spec.md | 6 +- .../codeReader/editor/documentReads.test.ts | 12 ++ vscode/src/codeReader/editor/documentReads.ts | 14 +- vscode/src/codeReader/editor/messages.ts | 28 ++-- .../codeReader/editor/previewEditor.test.ts | 64 +++++++++ vscode/src/codeReader/editor/previewEditor.ts | 133 +++++++++++------- .../src/codeReader/editor/referenceTargets.ts | 11 +- .../codeReader/DefinitionPeekPopup.test.tsx | 33 ++++- .../src/codeReader/DefinitionPeekPopup.tsx | 10 +- .../src/codeReader/TextPreview.test.tsx | 19 +-- .../webview-ui/src/codeReader/TextPreview.tsx | 9 +- .../src/codeReader/definitionPeek.ts | 7 +- .../src/codeReader/markdown.test.ts | 6 - vscode/webview-ui/src/codeReader/markdown.ts | 6 +- .../src/codeReader/positions.test.tsx | 2 +- .../src/codeReader/repoLink.test.ts | 34 +++++ vscode/webview-ui/src/codeReader/repoLink.ts | 46 +++--- vscode/webview-ui/src/codeReader/types.ts | 28 ++-- .../webview-ui/src/codeReader/useRepoLinks.ts | 2 +- 19 files changed, 319 insertions(+), 151 deletions(-) diff --git a/docs/superpowers/specs/code-reader/spec.md b/docs/superpowers/specs/code-reader/spec.md index effb908..bdd1ec1 100644 --- a/docs/superpowers/specs/code-reader/spec.md +++ b/docs/superpowers/specs/code-reader/spec.md @@ -517,7 +517,7 @@ - 问答同仓库路径链接(`useRepoLinks`、`RepoLinks`):画出来后把页面上的仓库路径和引用一起发去问(`checkRepoLinks { paths, symbols }` → `repoLinksChecked { missing, unresolved, builtin }`),问到之前是普通链接,找不到的画成坏链接(红色波浪线,悬停「找不到」),`builtin` 的(语言自带、又问不到定义的名字)画成普通文字(`cr-link-plain`:颜色同旁边的字、不带下划线,点了不发消息);文档换了版本(收到 `document`)再问一次,引用跟着源码变。点了发 `openRepoLink`(不带 `path`,只带 `symbol`):找到了同完整链接的后半段打开目标(语言有代码阅读的用预览、滚过去、闪,没有的用默认的打开方式选中名字),目标在这份文件里时在本预览里滚过去、闪一下(同跳到定义的闪);找不到就标坏;是语言自带的名字就不打开,回 `repoLinkBuiltin`,页面改当普通文字。 - 样子:正文用界面字体(`--vscode-font-family`、`--vscode-font-size`)、comment 色、行高 20px,一行注释和一行译文一样高;行内代码用编辑器字体、`--vscode-textCodeBlock-background` 底;围栏代码块用编辑器字体和字号、同样的底,长行折行;段落、列表、代码块、表格之间 6px;标题只比正文稍大(h1 1.15em、h2 1.08em,其余同正文,加粗);链接 `--vscode-textLink-foreground`;引用左边 3px `--vscode-textBlockQuote-border` 竖线、`--vscode-textBlockQuote-background` 底;表格 1px 细线。 - 「编辑」状态照旧是源码,编辑写回不变;Markdown 里的名字(反引号里的代码)不能点,不参与跳到定义,能点的只有上面的链接和引用。 -- 定义卡片浮窗里的注释(别的文件的)按 `peek` 渲染(`TextPreview` 读 `MarkdownKindContext`):不认不带路径的引用,别的同注释。 +- 定义卡片浮窗里的注释同注释一样渲染、认不带路径的引用,引用按它写在的那份文件找:浮窗根上的 `data-peek` 记着卡片那份文件(`DefinitionPeek.uri`),`commentLinkOf` 给浮窗里的引用带上 `file`;扩展端(`resolveSymbolLink`)见 `file` 是别的文件,就按那份文件读成的文档(`DocumentReads`,同卡片画的那份)的块树、导入、用到的名字找(同一套 `ReferenceTargets`,各预览共用、按文件记),没有 `file` 或就是这份文件的按本预览找。问坏没坏、点了打开、标坏、语言自带的当普通文字都同页面上的引用(`repoLinkBroken` / `repoLinkOpened` / `repoLinkBuiltin` 带回 `file`),同一个名字和块记号写在不同文件里分开记;浮窗里的链接悬停不弹卡片(同浮窗里的仓库路径链接)。 ### 跳到定义 @@ -545,10 +545,10 @@ - 能弹的:带源码下标的名字(同「跳到定义」能点的那些),和注释里指到符号的链接(带 `#类名` / `#类名.成员` 的仓库路径链接、不带路径的引用 `[Name]`、`[Class.member]`);只指到文件、目录的链接不弹。不用按修饰键。 - 什么时候弹(同 VS Code 编辑器里的悬停):鼠标停在上面 `editor.hover.delay` 毫秒后问扩展端要卡片(扩展端按这份文件读 VS Code 的设置,语言自己的设置优先;`ready` 后发给页面,设置改了再发,页面不写死),没到时间就移开的作罢;回答带卡片才弹,没有就什么都不显示。同一时刻只有一个浮窗:指针在浮窗里时不弹别的,移到另一个名字、它有卡片时换成它的。只认最近一次询问的回答,迟到的丢掉。 - 什么时候收:移开名字和浮窗两者 `editor.hover.hidingDelay` 毫秒后收(期间移进浮窗、移回名字就不收;从名字移向浮窗时扫过别的名字不把时间往后推);Esc、滚动(页面或页面里能滚的框,浮窗自己里面滚不算)、在浮窗外按下鼠标、换了文档版本、跳到某处(`reveal`)立刻收;收了之后指针不离开这一处不再弹。 -- 卡片是哪块(扩展端,`editor/definitionPeek.ts` 的 `peekBlock`):名字的定义同 ⌘ / Ctrl + 点击查的(规则标了引用的问规则,其余问语言扩展),几处定义取第一处;链接的符号同点这个链接时找的(`symbolTarget`:不带路径的引用按这份文件找,仓库链接在那个文件的文档符号里找)。定义处是哪块自己的名字(`defines`)就取那块,否则取编辑范围盖住定义那一行的最里层那块(参数、导入项、字段落在它们所在的块里),连同子块;挂在父卡片子行上的子块去掉 `slot`,在浮窗里是最外层那张。定义在这份文件里用最近发出去的那份文档的块(版本对不上就没有);在别的文件就打开它,按它的语言、选中的翻译插件、库规则、项目入口排视图数据(同 `documentMessage`;规则查名字的定义、调用的种类不等,规则出的错不记,由那个文件自己的预览记)。没有卡片:查不到定义、目标语言没有代码阅读、解析失败、没有块盖住定义处、指着的名字就是定义本身(同一文件、定义处下标就是这个名字的)。 +- 卡片是哪块(扩展端,`editor/definitionPeek.ts` 的 `peekBlock`):名字的定义同 ⌘ / Ctrl + 点击查的(规则标了引用的问规则,其余问语言扩展),几处定义取第一处;链接的符号同点这个链接时找的(`symbolTarget`:不带路径的引用按它写在的那份文件找,仓库链接在那个文件的文档符号里找)。定义处是哪块自己的名字(`defines`)就取那块,否则取编辑范围盖住定义那一行的最里层那块(参数、导入项、字段落在它们所在的块里),连同子块;挂在父卡片子行上的子块去掉 `slot`,在浮窗里是最外层那张。定义在这份文件里用最近发出去的那份文档的块(版本对不上就没有);在别的文件就打开它,按它的语言、选中的翻译插件、库规则、项目入口排视图数据(同 `documentMessage`;规则查名字的定义、调用的种类不等,规则出的错不记,由那个文件自己的预览记)。没有卡片:查不到定义、目标语言没有代码阅读、解析失败、没有块盖住定义处、指着的名字就是定义本身(同一文件、定义处下标就是这个名字的)。 - 中文名:同一文件的带上这个版本眼下已知的;别的文件只认起了中文名的定义本身(定义处标 `definition`,后面淡淡写原名),用到它们的各处要问语言扩展,浮窗不等。都只带卡片上画出来的名字(`namesOnCard`)。 - 样子(`DefinitionPeekPopup.tsx`):portal 到页面根上的浮窗,外框用 VS Code 悬停框的边框、底色、阴影(`--vscode-editorHoverWidget-*`);标题一行「相对路径:行号」(路径从工作区文件夹算起,同挑选框),垫悬停框状态栏的底色、链接色,点了跳到定义(名字的同 ⌘ / Ctrl + 点击,发 `goToDefinition`;链接的同点链接,发 `openRepoLink`),浮窗跟着收。下面是卡片,卡片区最宽 640px、最高 420px,再大在里面滚。默认放在名字下方、左边对齐名字;下方放不下、上方空间更大就放上方,底边贴着名字往上长(卡片展开时不盖住名字);宽高再限到那边剩下的空间,钳在视口内。层级低于悬停提示,浮窗里名字的提示照样盖在它上面。 -- 卡片只读:同页面上那张卡片(类、函数默认收起子块,能在浮窗里折叠展开,只记在浮窗里),块树不带编辑那部分(`BlockTreeState.edit`),没有编辑、问 AI、重写注释的按钮,不标差异、不筛选。浮窗里的名字下标是那个文件的,页面上管名字的几处都不认它们(浮窗根上标 `data-peek`):不按 ⌘ / Ctrl 变链接、不能跳,单击不高亮也不清页面上的高亮,不画页面上的高亮,不再弹卡片;注释里不带路径的引用不成链接(引用按注释所在的文件找,页面只替自己这份文件问),仓库路径链接照样能点。 +- 卡片只读:同页面上那张卡片(类、函数默认收起子块,能在浮窗里折叠展开,只记在浮窗里),块树不带编辑那部分(`BlockTreeState.edit`),没有编辑、问 AI、重写注释的按钮,不标差异、不筛选。浮窗里的名字下标是那个文件的,页面上管名字的几处都不认它们(浮窗根上标 `data-peek`):不按 ⌘ / Ctrl 变链接、不能跳,单击不高亮也不清页面上的高亮,不画页面上的高亮,不再弹卡片;注释里的仓库路径链接、不带路径的引用照样能点(引用按卡片那份文件找,见「注释的 Markdown 排版」),悬停不弹卡片。 - 按住 ⌘ / Ctrl 变链接、⌘ / Ctrl + 点击跳转、单击高亮各处出现都照旧;在浮窗外按下鼠标(单击名字、⌘ + 点击)先收起浮窗。 ### 钉住卡片(Sticky Scroll) diff --git a/vscode/src/codeReader/editor/documentReads.test.ts b/vscode/src/codeReader/editor/documentReads.test.ts index 965e443..76f77ad 100644 --- a/vscode/src/codeReader/editor/documentReads.test.ts +++ b/vscode/src/codeReader/editor/documentReads.test.ts @@ -97,4 +97,16 @@ describe('悬停看别的文件里的定义时读成的文档(DocumentReads) await reads.read('file:///b.py', file(1)) expect(parses()).toBe(once * 4) }) + + it('带着卡片里注释中的引用按它找的样子:块树就是读成的块,导入引进来的名字、代码里用到又没定义的名字;同一份读成的文档给同一个对象', async () => { + const reads = new DocumentReads(registryWithPython(), runtime) + const source = 'from app.bridge import tauri_bridge\n\nclass UserService:\n def update_user(self):\n raise ValueError(tauri_bridge)\n' + const snapshot = { ...file(1), getText: () => source } + const { message, references } = await reads.read('file:///app/service.py', snapshot) + expect(references.version).toBe(1) + expect(references.sent?.blocks).toBe(message.blocks) + expect(references.imports.map(item => item.name)).toEqual(['tauri_bridge']) + expect([...references.uses.keys()]).toEqual(expect.arrayContaining(['tauri_bridge', 'ValueError'])) + expect((await reads.read('file:///app/service.py', snapshot)).references).toBe(references) + }) }) diff --git a/vscode/src/codeReader/editor/documentReads.ts b/vscode/src/codeReader/editor/documentReads.ts index 221e6a4..4231aa1 100644 --- a/vscode/src/codeReader/editor/documentReads.ts +++ b/vscode/src/codeReader/editor/documentReads.ts @@ -1,14 +1,19 @@ import type { CodeReaderRegistry } from '../registry' import type { TreeSitterRuntime } from '../runtime' import type { DocumentRead, DocumentSnapshot } from './documentMessage' +import type { ReferenceInputs } from './referenceTargets' import { RecentPromises } from '../recentPromises' +import { outsideUses } from '../symbolLinks' import { documentMessage } from './documentMessage' /** 默认记几份文件 */ const LIMIT = 8 -/** 悬停看定义用到的那部分:读成的文档,和里面起了中文名的定义。 */ -export type PeekRead = Pick +/** + * 悬停看定义用到的那部分:读成的文档,里面起了中文名的定义,和卡片里注释中不带路径的引用按它找的样子(`references`,见 [ReferenceInputs]; + * 同一份读成的文档给的是同一个对象,解析过的引用接着用)。 + */ +export type PeekRead = Pick & { references: ReferenceInputs } /** * 悬停看别的文件里的定义时,那份文件读成的文档(见 [documentMessage]),各预览共用,免得每次悬停都把整份文件重新解析一遍。 @@ -37,7 +42,8 @@ export class DocumentReads { private async parse(document: DocumentSnapshot): Promise { // 那个文件里规则出的错由它自己的预览记 - const { message, displayNames } = await documentMessage(document, this.registry, this.runtime, () => {}) - return { message, displayNames } + const { message, displayNames, imports, bindings } = await documentMessage(document, this.registry, this.runtime, () => {}) + const blocks = message.error === undefined ? { blocks: message.blocks } : undefined + return { message, displayNames, references: { version: message.version, sent: blocks, imports, uses: outsideUses(bindings, message.source) } } } } diff --git a/vscode/src/codeReader/editor/messages.ts b/vscode/src/codeReader/editor/messages.ts index 5bb56cd..580cd64 100644 --- a/vscode/src/codeReader/editor/messages.ts +++ b/vscode/src/codeReader/editor/messages.ts @@ -23,11 +23,13 @@ export interface TranslatorOption { id: string, label: string } /** * 注释里一个不带路径的引用(`[Name]`、`[Class.member]`):symbol 是写的名字;block 是这段注释属于哪一块(块的 anchor), * 从那块代码的声明起一层层往外找(见 symbolLinks.ts)。不在哪块的注释里的没有 block,只从文件顶层找。 - * 同一个名字写在不同块的注释里,指到的可以不一样。 + * file 是这段注释写在哪份文件里(uri,见 [DefinitionPeek] 的 `uri`):定义卡片里的注释带着卡片那份文件,按那份文件的块、导入找; + * 页面上这份文档自己的注释没有 file。同一个名字写在不同块、不同文件的注释里,指到的可以不一样。 */ export interface SymbolReference { symbol: string block?: string + file?: string } /** @@ -99,12 +101,14 @@ export type PageBlock = PageCard & { } /** - * 悬停名字、注释里的链接时浮窗里的定义卡片:定义所在的文件(从工作区文件夹算起的相对路径,同挑选框)、定义处那一行(0 起), + * 悬停名字、注释里的链接时浮窗里的定义卡片:定义所在的文件(path 是给人看的、从工作区文件夹算起的相对路径,同挑选框; + * uri 是这份文件本身,卡片里注释中不带路径的引用带着它,按这份文件找,见 [SymbolReference])、定义处那一行(0 起), * 盖住定义处的那块(连同子块,和那个文件的预览画的是同一张卡片;它自己当那个文件的顶层第一块,span 从文件开头算), * 显示成中文名的名字(下标是那个文件里的)。 */ export interface DefinitionPeek { path: string + uri: string line: number block: PageBlock names: ShownName[] @@ -214,9 +218,9 @@ export interface StickyScrollSettings { * repoLinksChecked:id 那次 checkRepoLinks 的回答,missing 是仓库里没有的路径,unresolved 是找不到的不带路径的引用, * builtin 是语言自带、又问不到定义的不带路径的引用(Python 的 `ValueError`、`str`,页面当普通文字);两样都原样带回问的那几条 * repoLinkBroken:这个仓库链接打不开。没有 symbol 是文件或目录没有;有 symbol 是文件在、这个符号没有;没有 path 是不带路径的引用找不到, - * block 同 openRepoLink - * repoLinkBuiltin:点的不带路径的引用是语言自带、又问不到定义的名字,没打开,页面当普通文字;symbol、block 同 openRepoLink - * repoLinkOpened:这个仓库链接打开了。路径在;带了 symbol 时这个符号也在;没有 path 是不带路径的引用找到了,block 同 openRepoLink + * block、file 同 openRepoLink + * repoLinkBuiltin:点的不带路径的引用是语言自带、又问不到定义的名字,没打开,页面当普通文字;symbol、block、file 同 openRepoLink + * repoLinkOpened:这个仓库链接打开了。路径在;带了 symbol 时这个符号也在;没有 path 是不带路径的引用找到了,block、file 同 openRepoLink * repoImagesChecked:id 那次 checkRepoImages 的回答。src 是预览里能直接用的图片地址;没有 src 是文件不在 * callTreeCounts:version 这份文档里,每个能看调用树的块(BlockView.callTree)落在几棵调用树里(只包括大于 0 的)。不是页面上这个版本的不理 * callTree:anchor 那块的调用树。trees 空着、又没有 contractMiss 是没有调用它的地方;接口卡片在契约文件里没对上时带 contractMiss。 @@ -245,9 +249,9 @@ export type ExtensionToWebview | { type: 'diff', diff: DocumentDiff | null } | { type: 'openedFrom', from: OpenedFrom } | { type: 'repoLinksChecked', id: number, missing: string[], unresolved: SymbolReference[], builtin: SymbolReference[] } - | { type: 'repoLinkBroken', path?: string, symbol?: string, block?: string } + | { type: 'repoLinkBroken', path?: string, symbol?: string, block?: string, file?: string } | ({ type: 'repoLinkBuiltin' } & SymbolReference) - | { type: 'repoLinkOpened', path?: string, symbol?: string, block?: string } + | { type: 'repoLinkOpened', path?: string, symbol?: string, block?: string, file?: string } | { type: 'repoImagesChecked', id: number, images: { path: string, src?: string }[] } | { type: 'focusPathAsk' } | { type: 'callTreeCounts', version: number, counts: Record } @@ -261,7 +265,7 @@ export type ExtensionToWebview * version 是这份下标所属的文档版本 * goToDefinition:⌘ / Ctrl + 点击,跳到 offset 处名字的定义 * peekDefinition:鼠标停在带下标的名字上,要 offset 处名字的定义卡片(几处定义取第一处);id、version 同 probeDefinition - * peekLink:鼠标停在注释里指到符号的链接上(带 #符号 的仓库链接、不带路径的引用),要那个符号的定义卡片;path、symbol、block 同 openRepoLink + * peekLink:鼠标停在注释里指到符号的链接上(带 #符号 的仓库链接、不带路径的引用),要那个符号的定义卡片;path、symbol、block、file 同 openRepoLink * findOccurrences:单击带下标的名字,问它在这份文档里出现在哪几处;id、version 同 probeDefinition * setFilter:改块筛选(整份替换),扩展端记下后发 filter 给所有预览 * setOnlyChanges:打开 / 关掉「只看改动」,扩展端记下后发 filter 给所有预览 @@ -280,8 +284,8 @@ export type ExtensionToWebview * findNavigation:眼下查找栏的快捷键归不归这个页面(查找栏开着、页面有焦点),变了才发;扩展端按它设 when 子句用的上下文键 * openRepoLink:点了注释里的仓库路径链接,打开 path(从仓库根算起)。symbol 是类名、函数名或「类名.成员名」,lines 是文件里的几行(和 symbol 二选一), * 都没有就只打开文件或在代码阅读侧栏的树上定位目录; - * 没有 path 是不带路径的引用(`[Name]`、`[Class.member]`),按这份文件找 symbol:block 是它写在哪块的注释里(见 SymbolReference), - * 从那块的声明起一层层往外找本文件定义的,再是导入的(见 symbolLinks.ts) + * 没有 path 是不带路径的引用(`[Name]`、`[Class.member]`),按它写在的那份文件找 symbol:file 是那份文件(定义卡片里的注释才有,没有是这份文件), + * block 是它写在哪块的注释里(见 SymbolReference),从那块的声明起一层层往外找那份文件定义的,再是导入的(见 symbolLinks.ts) * checkRepoLinks:预览里这批仓库路径问哪些文件或目录没有,这批不带路径的引用(references)问哪些找不到。id 标出是哪一次问 * checkRepoImages:预览里这批仓库图片,问哪些文件在,在的要预览里能直接用的地址。id 标出是哪一次问 * openCallTree:打开 anchor 那块的调用树。version 是这块所在的文档版本,对不上就不查 @@ -301,7 +305,7 @@ 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, block?: string } + | { type: 'peekLink', id: number, path?: string, symbol: string, block?: string, file?: string } | { type: 'findOccurrences', id: number, version: number, offset: number } | { type: 'ask', askId: string, anchor: string, question: string, images: ClaudeImage[] } | { type: 'stopAsk', askId: string } @@ -309,7 +313,7 @@ export type WebviewToExtension | { type: 'selectProfile', kind: ChatKind, id: string } | { type: 'occurrenceNavigation', available: boolean } | { type: 'findNavigation', available: boolean } - | { type: 'openRepoLink', path?: string, symbol?: string, block?: string, lines?: LineRange } + | { type: 'openRepoLink', path?: string, symbol?: string, block?: string, file?: string, lines?: LineRange } | { type: 'checkRepoLinks', id: number, paths: string[], references: SymbolReference[] } | { type: 'checkRepoImages', id: number, paths: string[] } | { type: 'openCallTree', version: number, anchor: string } diff --git a/vscode/src/codeReader/editor/previewEditor.test.ts b/vscode/src/codeReader/editor/previewEditor.test.ts index 5473893..6349eef 100644 --- a/vscode/src/codeReader/editor/previewEditor.test.ts +++ b/vscode/src/codeReader/editor/previewEditor.test.ts @@ -673,6 +673,70 @@ describe('注释里不带路径的引用按它写在哪块的注释里找', () = vi.mocked(workspace.openTextDocument).mockReset() } }) + + it('定义卡片里别的文件的注释:卡片带着那份文件,引用按那份文件的导入、定义找,标坏链接、点了打开都按它;写着本文件的照本文件找', async () => { + const OTHER = '/workspace/web/b.ts' + const BRIDGE = '/workspace/web/tauri-bridge.ts' + const EVENTS = 'function useShellEvents' + const textDocument = (fsPath: string) => ({ uri: Uri.file(fsPath), version: 1, languageId: 'typescript', fileName: fsPath, getText: () => '', positionAt: (offset: number) => ({ line: 0, character: offset }), offsetAt: (position: { character: number }) => position.character }) + const other = textDocument(OTHER) + const bridge = textDocument(BRIDGE) + const read = (blocks: BlockView[], imports: unknown[]) => ({ + message: { type: 'document', version: 1, languageId: 'typescript', blocks, source: '', colors: { roles: [], runs: [] }, attributes: [], translators: [], translatorId: '', names: [] }, + displayNames: [], + symbols: [], + imports, + kinds: [], + bindings: [], + }) + const original = vi.mocked(documentMessage).getMockImplementation()! + vi.mocked(documentMessage).mockImplementation(async input => (input.fileName === OTHER + ? read([declared(EVENTS, 300)], [{ name: 'tauriBridge', offset: 20, range: { startIndex: 0, endIndex: 40 } }]) + : read([declared(HELPER, 950)], [])) as never) + vi.mocked(documentSymbolNodes).mockReset().mockImplementation(async uri => uri === other.uri.toString() + ? [symbol('useShellEvents', 300, 600)] + : [symbol('helper', 950, 990)]) + vi.mocked(languageDefinitions).mockReset().mockImplementation((async (uri: { toString: () => string }, position: { character: number }) => + uri.toString() === other.uri.toString() && position.character === 20 ? [{ uri: bridge.uri, range: new Range(0, 13, 0, 24) }] : []) as never) + const { provider, document } = opening() + Object.assign(workspace, { textDocuments: [document, other, bridge] }) + vi.mocked(workspace.openTextDocument).mockImplementation((async (uri: { toString: () => string }) => + [other, bridge].find(doc => doc.uri.toString() === uri.toString()) ?? document) as never) + try { + const page = tab() + provider.resolveCustomTextEditor(document as never, page.panel) + await page.send({ type: 'ready' }) + await vi.waitFor(() => expect(page.posted()).toContainEqual(expect.objectContaining({ type: 'document' }))) + vi.mocked(documentMessage).mockClear() + vi.mocked(commands.executeCommand).mockClear() + + await page.send({ type: 'peekLink', id: 1, path: 'web/b.ts', symbol: 'useShellEvents' }) + expect(page.posted()).toContainEqual({ type: 'definitionPeeked', id: 1, peek: expect.objectContaining({ uri: other.uri.toString() }) }) + + const imported = { symbol: 'tauriBridge', block: EVENTS, file: other.uri.toString() } + const defined = { symbol: 'useShellEvents', file: other.uri.toString() } + const ownName = { symbol: 'helper', file: other.uri.toString() } + const writtenHere = { symbol: 'helper', file: document.uri.toString() } + const onPage = { symbol: 'tauriBridge', block: HELPER } + await page.send({ type: 'checkRepoLinks', id: 2, paths: [], references: [imported, defined, ownName, writtenHere, onPage] }) + expect(page.posted()).toContainEqual({ type: 'repoLinksChecked', id: 2, missing: [], unresolved: [ownName, onPage], builtin: [] }) + + await page.send({ type: 'openRepoLink', ...imported }) + expect(page.posted()).toContainEqual({ type: 'repoLinkOpened', ...imported }) + expect(commands.executeCommand).toHaveBeenCalledWith('vscode.open', expect.objectContaining({ fsPath: BRIDGE }), expect.anything()) + expect(languageDefinitions).toHaveBeenCalledWith(expect.objectContaining({ fsPath: OTHER }), expect.objectContaining({ character: 20 })) + await page.send({ type: 'openRepoLink', ...ownName }) + expect(page.posted()).toContainEqual({ type: 'repoLinkBroken', ...ownName }) + expect(documentMessage).toHaveBeenCalledTimes(1) + } + finally { + vi.mocked(documentMessage).mockImplementation(original) + vi.mocked(documentSymbolNodes).mockReset().mockResolvedValue([]) + vi.mocked(languageDefinitions).mockReset().mockResolvedValue([]) + Object.assign(workspace, { textDocuments: [] }) + vi.mocked(workspace.openTextDocument).mockReset() + } + }) }) describe('点了带几行的仓库链接', () => { diff --git a/vscode/src/codeReader/editor/previewEditor.ts b/vscode/src/codeReader/editor/previewEditor.ts index 00c02cb..c8a1194 100644 --- a/vscode/src/codeReader/editor/previewEditor.ts +++ b/vscode/src/codeReader/editor/previewEditor.ts @@ -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() 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 { + 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 { + 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 { + private async peekLink(id: number, rel: string | undefined, reference: SymbolReference): Promise { 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 { 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 { + private async openRepoLink({ path: rel, symbol, block, file, lines }: Extract): Promise { 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 { + private async symbolTarget(rel: string | undefined, reference: SymbolReference): Promise { 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 { 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): 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 { if (edit.kind === 'replace') diff --git a/vscode/src/codeReader/editor/referenceTargets.ts b/vscode/src/codeReader/editor/referenceTargets.ts index 794355e..8cbf119 100644 --- a/vscode/src/codeReader/editor/referenceTargets.ts +++ b/vscode/src/codeReader/editor/referenceTargets.ts @@ -5,8 +5,8 @@ import type { SymbolReference } from './messages' import { referenceScope, resolveReference } from '../symbolLinks' /** - * 解析引用时本文件的样子:文档版本、最近一次发出去的块树(没有时只从文件顶层找)、各条导入引进来的名字、代码里用到又没定义的名字各在哪几处。 - * 哪样换了,引用从哪个声明起找、导入和用到的名字在哪都可能跟着变。 + * 解析引用时这份文件的样子:文档版本、块树(本预览最近一次发出去的,或别的文件读成的;没有时只从文件顶层找)、各条导入引进来的名字、 + * 代码里用到又没定义的名字各在哪几处。哪样换了,引用从哪个声明起找、导入和用到的名字在哪都可能跟着变。 */ export interface ReferenceInputs { version: number @@ -33,7 +33,10 @@ interface Round { symbols: Map> } -/** 引用的键:同一个名字写在不同块的注释里分开记,指到的可以不一样(拼法同[页面那边的键](vscode/webview-ui/src/codeReader/repoLink.ts#referenceKey))。 */ +/** + * 引用的键:同一个名字写在不同块的注释里分开记,指到的可以不一样。记着的都是同一份文件里的引用,不分 `file` + * ([页面那边的键](vscode/webview-ui/src/codeReader/repoLink.ts#referenceKey)还要分)。 + */ function referenceKey({ symbol, block }: SymbolReference): string { return `${symbol}\u0000${block ?? ''}` } @@ -43,7 +46,7 @@ function sameInputs(a: ReferenceInputs, b: ReferenceInputs): boolean { } /** - * 一个预览里注释中不带路径的引用指到哪(见 [resolveReference]),解析过的记着:标链接、悬停、点击同一个引用只问一次语言扩展,同时问的等同一个 Promise。 + * 一份文件的注释里不带路径的引用指到哪(见 [resolveReference]),解析过的记着:标链接、悬停、点击同一个引用只问一次语言扩展,同时问的等同一个 Promise。 * * 本文件的样子([ReferenceInputs])换了,记着的全部作废。指到的文件改过、关掉了的再问一遍,免得范围指错地方。 * 找不到的、语言自带的、出错的不留,因为语言扩展还没准备好时会暂时答空,下次要再问。各文件的文档符号也这样记着,一批引用共用一份。 diff --git a/vscode/webview-ui/src/codeReader/DefinitionPeekPopup.test.tsx b/vscode/webview-ui/src/codeReader/DefinitionPeekPopup.test.tsx index ee4c83de..d3031f3 100644 --- a/vscode/webview-ui/src/codeReader/DefinitionPeekPopup.test.tsx +++ b/vscode/webview-ui/src/codeReader/DefinitionPeekPopup.test.tsx @@ -1,7 +1,12 @@ +// @vitest-environment happy-dom + import type { BlockView, DefinitionPeek } from './types' +import { act } from 'react' +import { createRoot } from 'react-dom/client' import { renderToStaticMarkup } from 'react-dom/server' import { describe, expect, it, vi } from 'vitest' -import { PeekTree } from './DefinitionPeekPopup' +import { DefinitionPeekPopup, PeekTree } from './DefinitionPeekPopup' +import { commentLinkOf } from './repoLink' import { NO_SPAN } from './testBlocks' vi.mock('./vscode', () => ({ postMessage: () => {}, onMessage: () => () => {}, viewState: { get: () => ({ editing: [], folds: {} }), update: () => {} } })) @@ -24,6 +29,7 @@ const NAME = 'PairedDeviceRepository' const PEEK: DefinitionPeek = { path: 'app/devices.py', + uri: 'file:///w/app/devices.py', line: 41, block: block('3', 'class', { commentStartLine: 42, @@ -57,8 +63,27 @@ describe('定义卡片浮窗里的卡片', () => { expect(html).toContain(`${NAME}`) }) - it('注释里不带路径的引用不成链接:它按注释所在的文件找,页面只替自己这份文件问', () => { - expect(html).not.toContain('symbol:DesktopOnly') - expect(html).toContain('[DesktopOnly]') + it('注释里不带路径的引用同页面上一样是链接', () => { + expect(html).toContain('href="symbol:DesktopOnly"') + expect(html).not.toContain('[DesktopOnly]') + }) +}) + +describe('定义卡片浮窗', () => { + it('浮窗根上记着卡片那份文件:卡片注释里不带路径的引用带着它和写在的那块,扩展端按那份文件找', async () => { + vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true) + const anchor = document.createElement('span') + document.body.append(anchor) + const root = createRoot(document.createElement('div')) + try { + await act(async () => root.render( {}} />)) + const link = document.querySelector('.cr-peek a[href="symbol:DesktopOnly"]')! + expect(commentLinkOf(link)).toEqual({ symbol: 'DesktopOnly', block: '3', file: 'file:///w/app/devices.py' }) + } + finally { + await act(async () => root.unmount()) + anchor.remove() + vi.unstubAllGlobals() + } }) }) diff --git a/vscode/webview-ui/src/codeReader/DefinitionPeekPopup.tsx b/vscode/webview-ui/src/codeReader/DefinitionPeekPopup.tsx index 1aa0384..481dedb 100644 --- a/vscode/webview-ui/src/codeReader/DefinitionPeekPopup.tsx +++ b/vscode/webview-ui/src/codeReader/DefinitionPeekPopup.tsx @@ -10,7 +10,6 @@ import { nameMap } from './displayNames' import { placementOf } from './placement' import { TextMarksContext } from './SegmentText' import { createStore, staticStore } from './store' -import { MarkdownKindContext } from './TextPreview' import { collapsedIds } from './viewState' const NONE: ReadonlySet = new Set() @@ -18,7 +17,7 @@ const NOT_COVERED: ReadonlyMap = new Map() /** * 浮窗里的那张卡片:只读的块树(不编辑、不问 AI、不重写注释、不标差异、不筛选),折叠展开只记在浮窗里,默认同页面(类、函数收起子块)。 - * 名字的下标是那个文件的:中文名换成卡片带来的,不画页面上的高亮;Markdown 不认不带路径的引用。 + * 名字的下标是那个文件的:中文名换成卡片带来的,不画页面上的高亮。注释里不带路径的引用同页面上一样是链接,按那个文件找(见 [DefinitionPeekPopup])。 */ export function PeekTree({ peek }: { peek: DefinitionPeek }): ReactElement { const [tree] = useState((): BlockTreeHandle => { @@ -43,9 +42,7 @@ export function PeekTree({ peek }: { peek: DefinitionPeek }): ReactElement { const marks = useMemo(() => staticStore({ names: nameMap(peek.names), placement: placementOf(blocks) }), [peek, blocks]) return ( - - - + ) } @@ -53,6 +50,7 @@ export function PeekTree({ peek }: { peek: DefinitionPeek }): ReactElement { /** * 定义卡片浮窗:标题一行「相对路径:行号」,点了跳到定义(同 ⌘ / Ctrl + 点击);下面是卡片,太大时在浮窗里滚。 * 外框用 VS Code 悬停框的颜色;portal 到 body,先在左上角量出大小,再落到名字下方或上方(placePeek)。 + * 根上记着卡片那份文件([PEEK_ATTRIBUTE]),卡片注释里不带路径的引用点了、标坏都按那份文件找。 */ export function DefinitionPeekPopup({ shown, onJump }: { shown: ShownPeek | undefined, onJump: () => void }): ReactElement | null { const popupRef = useRef(null) @@ -80,7 +78,7 @@ export function DefinitionPeekPopup({ shown, onJump }: { shown: ShownPeek { - const preview = (kind: 'comment' | 'peek'): ReactElement => ( - - - - ) - await render(preview('comment')) - const original = anchor() - await render(preview('peek')) - expect(container.querySelector('a')).toBeNull() - expect(container.textContent).toBe('[Client]\n') - expect(original.isConnected).toBe(false) - await render(preview('comment')) - expect(anchor().getAttribute('href')).toBe('symbol:Client') -}) - it('组件 DiffText 更新正文时保留 Markdown 排版和增删标记', async () => { await render() expect(anchor().textContent).toBe('Client') diff --git a/vscode/webview-ui/src/codeReader/TextPreview.tsx b/vscode/webview-ui/src/codeReader/TextPreview.tsx index 0399dd7..9293947 100644 --- a/vscode/webview-ui/src/codeReader/TextPreview.tsx +++ b/vscode/webview-ui/src/codeReader/TextPreview.tsx @@ -1,22 +1,17 @@ import type { ReactElement } from 'react' -import type { MarkdownKind } from './markdown' import type { PreviewText } from './types' -import { createContext, memo, useContext, useMemo } from 'react' +import { memo, useMemo } from 'react' import { cn } from '@/lib/utils' import { renderMarkdown } from './markdown' import { SegmentText } from './SegmentText' -/** 卡片里的 Markdown 说明按哪种渲染:页面上是注释;定义卡片浮窗里是 peek(别的文件的注释,不认不带路径的引用)。 */ -export const MarkdownKindContext = createContext>('comment') - /** * 预览里的一段文字:着色的词按源码排版;Markdown 说明渲染成段落、列表、代码块。 * `find` 是卡片上这段字的记号(见 [textKey](vscode/webview-ui/src/codeReader/viewState.ts#textKey)),查找栏按它找到这段字画高亮。 */ export const TextPreview = memo(({ text, className, find }: { text: PreviewText, className?: string, find?: string }): ReactElement => { - const kind = useContext(MarkdownKindContext) if ('markdown' in text) - return + return return (

diff --git a/vscode/webview-ui/src/codeReader/definitionPeek.ts b/vscode/webview-ui/src/codeReader/definitionPeek.ts index 97fac03..96451cc 100644 --- a/vscode/webview-ui/src/codeReader/definitionPeek.ts +++ b/vscode/webview-ui/src/codeReader/definitionPeek.ts @@ -1,10 +1,13 @@ import type { CommentLink } from './repoLink' import type { DefinitionPeek, WebviewToExtension } from './types' -/** 定义卡片浮窗的根上的属性:浮窗里的名字、链接是别的文件的,页面上管名字的那几处(跳到定义、高亮各处出现、再弹卡片)都不认。 */ +/** + * 定义卡片浮窗的根上的属性,值是卡片那份文件([DefinitionPeek] 的 `uri`):浮窗里的名字、链接是那份文件的,页面上管名字的那几处 + * (跳到定义、高亮各处出现、再弹卡片)都不认;注释里不带路径的引用带上它,扩展端按那份文件找(见 commentLinkOf)。 + */ export const PEEK_ATTRIBUTE = 'data-peek' -/** 能弹定义卡片的东西:带源码下标的名字,或注释里指到符号的链接(带 #符号 的仓库链接、不带路径的引用,后者带着写在哪块)。 */ +/** 能弹定义卡片的东西:带源码下标的名字,或注释里指到符号的链接(带 #符号 的仓库链接、不带路径的引用,后者带着写在哪块、哪份文件)。 */ export type PeekSource = { offset: number } | (CommentLink & { symbol: string }) /** 指针下能弹定义卡片的一处:页面上的哪个元素(浮窗挂在它旁边),它是什么。 */ diff --git a/vscode/webview-ui/src/codeReader/markdown.test.ts b/vscode/webview-ui/src/codeReader/markdown.test.ts index dbef196..cba63ff 100644 --- a/vscode/webview-ui/src/codeReader/markdown.test.ts +++ b/vscode/webview-ui/src/codeReader/markdown.test.ts @@ -110,11 +110,6 @@ describe('不带路径的引用', () => { expect(renderMarkdown('见 [Agent]', 'answer')).toBe('

见 [Agent]

\n') expect(renderMarkdownDiff([{ text: '见 ' }, { text: '[Agent]', change: 'add' }])).toContain('href="symbol:Agent"') }) - - it('定义卡片浮窗里别的文件的注释不认(引用按注释所在的文件找);仓库路径链接照样是链接', () => { - expect(renderMarkdown('见 [Agent]', 'peek')).toBe('

见 [Agent]

\n') - expect(renderMarkdown('见 [Agent](app/models.py#Agent)', 'peek')).toContain('') - }) }) describe('renderMarkdownDiff', () => { @@ -141,7 +136,6 @@ describe('mermaid 代码块', () => { it('出占位:转义后的源码记在 data-source,里面先放原样的代码块', () => { expect(renderMarkdown(`说明\n\n\`\`\`mermaid\n${source}\`\`\``)).toBe(`

说明

\n${placeholder}`) - expect(renderMarkdown(`\`\`\`mermaid\n${source}\`\`\``, 'peek')).toBe(placeholder) expect(renderMarkdown(`\`\`\`mermaid\n${source}\`\`\``, 'answer')).toBe(placeholder) }) diff --git a/vscode/webview-ui/src/codeReader/markdown.ts b/vscode/webview-ui/src/codeReader/markdown.ts index 9cea3c1..2a6e4c1 100644 --- a/vscode/webview-ui/src/codeReader/markdown.ts +++ b/vscode/webview-ui/src/codeReader/markdown.ts @@ -126,10 +126,10 @@ const CACHE_SIZE = 500 const cache = new Map() /** - * 渲染的是什么:注释(说明、注释块,认不带路径的引用),定义卡片浮窗里的注释(不认不带路径的引用:引用按注释所在的文件找, - * 页面只替自己这份文件问),还是问 AI 的回答。 + * 渲染的是什么:注释(说明、注释块,认不带路径的引用;定义卡片浮窗里别的文件的注释也是,引用按它写在的那份文件找,见 commentLinkOf), + * 还是问 AI 的回答。 */ -export type MarkdownKind = 'comment' | 'peek' | 'answer' +export type MarkdownKind = 'comment' | 'answer' function envOf(kind: MarkdownKind, diagrams: boolean): RenderEnv { return { symbolLinks: kind === 'comment', codeLinks: kind === 'answer', diagrams } diff --git a/vscode/webview-ui/src/codeReader/positions.test.tsx b/vscode/webview-ui/src/codeReader/positions.test.tsx index 6423da8..014688d 100644 --- a/vscode/webview-ui/src/codeReader/positions.test.tsx +++ b/vscode/webview-ui/src/codeReader/positions.test.tsx @@ -85,7 +85,7 @@ async function peekOf(source: string, needle: string, name: string): Promise { ]) }) + it('同一个名字、同一个块记号写在不同文件的注释里(定义卡片里别的文件的):分开问、各标各的,点了和扩展端报回的都带着那份文件', () => { + const { links, sent } = setup() + const onPage = { symbol: 'tauriBridge', block: 'function useShellEvents' } + const inCard = { ...onPage, file: 'file:///w/use-shell-events.ts' } + links.scan([onPage, inCard]) + expect(sent).toEqual([{ type: 'checkRepoLinks', id: 1, paths: [], references: [onPage, inCard] }]) + links.checked(1, [], [onPage], []) + expect(links.state(onPage)).toBe('broken') + expect(links.state(inCard)).toBe('link') + + expect(links.click(inCard)).toBe(true) + expect(sent.at(-1)).toEqual({ type: 'openRepoLink', ...inCard }) + links.broken(inCard) + links.opened(onPage) + expect(links.state(inCard)).toBe('broken') + expect(links.state(onPage)).toBe('link') + links.builtin(inCard) + expect(links.state(inCard)).toBe('plain') + expect(links.state(onPage)).toBe('link') + }) + it('名字一样、写在另一块里的也算变了,要再问', () => { const { links, sent } = setup() links.scan([{ symbol: 'USER', block: 'a' }]) @@ -231,6 +252,19 @@ describe('页面上的链接写在哪块的注释里', () => { expect(at('outside')).toEqual({ symbol: 'get_agent_admitted' }) expect(at('web')).toBeUndefined() }) + + it('定义卡片浮窗里的不带路径的引用再带上卡片那份文件(浮窗根上记着的);浮窗里的仓库路径链接不带', () => { + document.body.innerHTML = ` +
+ ` + const at = (id: string): CommentLink | undefined => commentLinkOf(document.getElementById(id)!) + expect(at('card')).toEqual({ symbol: 'tauriBridge', block: 'function useShellEvents', file: 'file:///w/use-shell-events.ts' }) + expect(at('path')).toEqual({ path: 'web/a.ts', symbol: 'load' }) + }) }) describe('行内代码里写的代码位置', () => { diff --git a/vscode/webview-ui/src/codeReader/repoLink.ts b/vscode/webview-ui/src/codeReader/repoLink.ts index 47591a8..9019740 100644 --- a/vscode/webview-ui/src/codeReader/repoLink.ts +++ b/vscode/webview-ui/src/codeReader/repoLink.ts @@ -1,4 +1,5 @@ import type { LineRange, SymbolReference, WebviewToExtension } from './types' +import { PEEK_ATTRIBUTE } from './definitionPeek' /** 注释、问 AI 的回答里指向仓库代码的链接:从仓库根起的路径,可选一个符号或几行。 */ export interface RepoLink { @@ -11,8 +12,8 @@ export interface RepoLink { } /** - * 注释里不带路径的引用(`[Name]`、`[Class.member]`):没有路径,扩展端按当前文件、从它写在的那块(block,见 commentLinkOf)找这个符号。 - * 页面上它是地址为 `symbol:Name` 的链接(见 symbolHref)。 + * 注释里不带路径的引用(`[Name]`、`[Class.member]`):没有路径,扩展端按它写在的那份文件(file,定义卡片里的才有,没有是当前文件)、 + * 从它写在的那块(block)找这个符号,两样都见 commentLinkOf。页面上它是地址为 `symbol:Name` 的链接(见 symbolHref)。 */ export interface SymbolLink extends SymbolReference { path?: undefined @@ -118,31 +119,34 @@ export function codeLocationHref(code: string): string | undefined { } /** - * 页面上一个链接元素是哪种能点的链接(同 parseCommentLink)。不带路径的引用再带上它写在哪块的注释里:离它最近的那张块卡片(.cr-block) - * 上记着的块记号(data-anchor);不在块卡片里的、那张卡片没记块记号的(删掉的块)不带,扩展端只从文件顶层找。 + * 页面上一个链接元素是哪种能点的链接(同 parseCommentLink)。不带路径的引用再带上它写在哪块、哪份文件的注释里: + * 块是离它最近的那张块卡片(.cr-block)上记着的块记号(data-anchor),不在块卡片里的、那张卡片没记块记号的(删掉的块)不带,扩展端只从文件顶层找; + * 文件是它所在的定义卡片浮窗根上记着的那份文件([PEEK_ATTRIBUTE]),页面上这份文档自己的注释不带。 */ export function commentLinkOf(element: Element): CommentLink | undefined { const link = parseCommentLink(element.getAttribute('href') ?? '') if (!link || link.path !== undefined) return link const block = element.closest('.cr-block')?.dataset.anchor - return block === undefined ? link : { ...link, block } + const file = element.closest(`[${PEEK_ATTRIBUTE}]`)?.getAttribute(PEEK_ATTRIBUTE) ?? undefined + return { ...link, ...(block !== undefined && { block }), ...(file !== undefined && { file }) } } function symbolKey(path: string, symbol: string): string { return `${path}#${symbol}` } -/** 扩展端报回来打没打开的一个链接(repoLinkBroken / repoLinkOpened),path、symbol、block 同 openRepoLink。 */ +/** 扩展端报回来打没打开的一个链接(repoLinkBroken / repoLinkOpened),path、symbol、block、file 同 openRepoLink。 */ interface ReportedLink { path?: string symbol?: string block?: string + file?: string } -/** 不带路径的引用的键:同一个名字写在不同块的注释里分开记(指到的可以不一样)。 */ -function referenceKey({ symbol, block }: SymbolReference): string { - return `${symbol}\u0000${block ?? ''}` +/** 不带路径的引用的键:同一个名字写在不同块、不同文件的注释里分开记(指到的可以不一样)。 */ +function referenceKey({ symbol, block, file }: SymbolReference): string { + return `${symbol}\u0000${block ?? ''}\u0000${file ?? ''}` } /** 页面上一个链接眼下怎么显示:照常的链接;坏的(文件、符号没有,引用找不到);普通文字(语言自带、又问不到定义的名字,点了不打开)。 */ @@ -150,7 +154,7 @@ export type LinkState = 'link' | 'broken' | 'plain' /** * 页面上的仓库链接和不带路径的引用:点了发给扩展端打开;预览出来后把路径、引用发去问哪些文件没有、哪些引用找不到、哪些是语言自带的名字; - * 扩展端说没有的标成坏的,说是语言自带的当普通文字。仓库链接的符号在不在要点了才知道。不带路径的引用按(写在哪块,名字)分开问、分开标。 + * 扩展端说没有的标成坏的,说是语言自带的当普通文字。仓库链接的符号在不在要点了才知道。不带路径的引用按(写在哪份文件、哪块,名字)分开问、分开标。 */ export class RepoLinks { private request = 0 @@ -174,7 +178,7 @@ export class RepoLinks { return true } - /** 页面上现有的链接。路径、引用(名字或写在哪块)变了(或文档换了版本,见 stale)才问扩展端哪些路径没有、哪些引用找不到。 */ + /** 页面上现有的链接。路径、引用(名字或写在哪份文件、哪块)变了(或文档换了版本,见 stale)才问扩展端哪些路径没有、哪些引用找不到。 */ scan(links: readonly CommentLink[]): void { const paths = [...new Set(links.flatMap(link => link.path === undefined ? [] : [link.path]))].sort() const references = new Map() @@ -208,12 +212,12 @@ export class RepoLinks { return true } - /** 扩展端打开时发现文件或符号没有;没有路径的是写在 block 那块里的不带路径的引用找不到。 */ - broken({ path, symbol, block }: ReportedLink): void { + /** 扩展端打开时发现文件或符号没有;没有路径的是写在 file 那份文件 block 那块里的不带路径的引用找不到。 */ + broken({ path, symbol, block, file }: ReportedLink): void { if (path === undefined) { if (symbol !== undefined) { - this.unresolved.add(referenceKey({ symbol, block })) - this.builtins.delete(referenceKey({ symbol, block })) + this.unresolved.add(referenceKey({ symbol, block, file })) + this.builtins.delete(referenceKey({ symbol, block, file })) } return } @@ -223,12 +227,12 @@ export class RepoLinks { this.missingPaths.add(path) } - /** 打开成功:这个路径在,这个符号也在;没有路径的是写在 block 那块里的不带路径的引用找到了。 */ - opened({ path, symbol, block }: ReportedLink): void { + /** 打开成功:这个路径在,这个符号也在;没有路径的是写在 file 那份文件 block 那块里的不带路径的引用找到了。 */ + opened({ path, symbol, block, file }: ReportedLink): void { if (path === undefined) { if (symbol !== undefined) { - this.unresolved.delete(referenceKey({ symbol, block })) - this.builtins.delete(referenceKey({ symbol, block })) + this.unresolved.delete(referenceKey({ symbol, block, file })) + this.builtins.delete(referenceKey({ symbol, block, file })) } return } @@ -237,13 +241,13 @@ export class RepoLinks { this.missingSymbols.delete(symbolKey(path, symbol)) } - /** 扩展端打开时发现写在 `block` 那块里的不带路径的引用是语言自带、又问不到定义的名字:改当普通文字,不再标坏。 */ + /** 扩展端打开时发现写在 `file` 那份文件 `block` 那块里的不带路径的引用是语言自带、又问不到定义的名字:改当普通文字,不再标坏。 */ builtin(reference: SymbolReference): void { this.unresolved.delete(referenceKey(reference)) this.builtins.add(referenceKey(reference)) } - /** 这个链接现在怎么显示(见 [LinkState]):文件没有、这个符号没有、写在这块里的引用找不到是坏的,写在这块里的引用是语言自带的名字是普通文字。还没问到的照常是链接。 */ + /** 这个链接现在怎么显示(见 [LinkState]):文件没有、这个符号没有、引用(按写在哪份文件、哪块分开算)找不到是坏的,引用是语言自带的名字是普通文字。还没问到的照常是链接。 */ state(link: CommentLink | undefined): LinkState { if (!link) return 'link' diff --git a/vscode/webview-ui/src/codeReader/types.ts b/vscode/webview-ui/src/codeReader/types.ts index 4e68076..84abc50 100644 --- a/vscode/webview-ui/src/codeReader/types.ts +++ b/vscode/webview-ui/src/codeReader/types.ts @@ -199,11 +199,13 @@ export interface TranslatorOption { id: string, label: string } /** * 注释里一个不带路径的引用(`[Name]`、`[Class.member]`):symbol 是写的名字;block 是这段注释属于哪一块(块的 anchor), * 扩展端从那块代码的声明起一层层往外找。不在哪块的注释里的没有 block,只从文件顶层找。 - * 同一个名字写在不同块的注释里,指到的可以不一样。 + * file 是这段注释写在哪份文件里(uri,见 [DefinitionPeek] 的 `uri`):定义卡片里的注释带着卡片那份文件,扩展端按那份文件的块、导入找; + * 页面上这份文档自己的注释没有 file。同一个名字写在不同块、不同文件的注释里,指到的可以不一样。 */ export interface SymbolReference { symbol: string block?: string + file?: string } /** 这份文件里出现过的一个属性值:显示名和有几块是这个值 */ @@ -251,12 +253,14 @@ export interface Occurrence { } /** - * 悬停名字、注释里的链接时浮窗里的定义卡片:定义所在的文件(从工作区文件夹算起的相对路径)、定义处那一行(0 起), + * 悬停名字、注释里的链接时浮窗里的定义卡片:定义所在的文件(path 是给人看的、从工作区文件夹算起的相对路径; + * uri 是这份文件本身,卡片里注释中不带路径的引用带着它,扩展端按这份文件找,见 [SymbolReference])、定义处那一行(0 起), * 盖住定义处的那块(连同子块,和那个文件的预览画的是同一张卡片;它自己当那个文件的顶层第一块,span 从文件开头算), * 显示成中文名的名字(下标是那个文件里的,不是页面上这份文档的) */ export interface DefinitionPeek { path: string + uri: string line: number block: BlockView names: ShownName[] @@ -539,9 +543,9 @@ export interface CodeColors { * repoLinksChecked:id 那次 checkRepoLinks 的回答,missing 是仓库里没有的路径,unresolved 是找不到的不带路径的引用, * builtin 是语言自带、又问不到定义的不带路径的引用(Python 的 `ValueError`、`str`),当普通文字显示;两样都原样带回问的那几条 * repoLinkBroken:这个仓库链接打不开。没有 symbol 是文件或目录没有;有 symbol 是文件在、这个符号没有;没有 path 是不带路径的引用找不到, - * block 同 openRepoLink - * repoLinkBuiltin:点的不带路径的引用是语言自带、又问不到定义的名字,没打开,改当普通文字显示;symbol、block 同 openRepoLink - * repoLinkOpened:这个仓库链接打开了。路径在;带了 symbol 时这个符号也在;没有 path 是不带路径的引用找到了,block 同 openRepoLink + * block、file 同 openRepoLink + * repoLinkBuiltin:点的不带路径的引用是语言自带、又问不到定义的名字,没打开,改当普通文字显示;symbol、block、file 同 openRepoLink + * repoLinkOpened:这个仓库链接打开了。路径在;带了 symbol 时这个符号也在;没有 path 是不带路径的引用找到了,block、file 同 openRepoLink * repoImagesChecked:id 那次 checkRepoImages 的回答。src 是预览里能直接用的图片地址;没有 src 是文件不在 * callTreeCounts:version 这份文档里,每个能看调用树的块落在几棵调用树里(只包括大于 0 的)。不是页面上这个版本的不理 * callTree:anchor 那块的调用树。trees 空着、又没有 contractMiss 是没有调用它的地方;接口卡片在契约文件里没对上时带 contractMiss。 @@ -609,9 +613,9 @@ export type ExtensionToWebview | { type: 'diff', diff: DocumentDiff | null } | { type: 'openedFrom', from: OpenedFrom } | { type: 'repoLinksChecked', id: number, missing: string[], unresolved: SymbolReference[], builtin: SymbolReference[] } - | { type: 'repoLinkBroken', path?: string, symbol?: string, block?: string } + | { type: 'repoLinkBroken', path?: string, symbol?: string, block?: string, file?: string } | ({ type: 'repoLinkBuiltin' } & SymbolReference) - | { type: 'repoLinkOpened', path?: string, symbol?: string, block?: string } + | { type: 'repoLinkOpened', path?: string, symbol?: string, block?: string, file?: string } | { type: 'repoImagesChecked', id: number, images: { path: string, src?: string }[] } | { type: 'focusPathAsk' } | { type: 'callTreeCounts', version: number, counts: Record } @@ -625,7 +629,7 @@ export type ExtensionToWebview * version 是这份下标所属的文档版本 * goToDefinition:⌘ / Ctrl + 点击,跳到 offset 处名字的定义 * peekDefinition:鼠标停在带下标的名字上,要 offset 处名字的定义卡片(几处定义取第一处);id、version 同 probeDefinition - * peekLink:鼠标停在注释里指到符号的链接上(带 #符号 的仓库链接、不带路径的引用),要那个符号的定义卡片;path、symbol、block 同 openRepoLink + * peekLink:鼠标停在注释里指到符号的链接上(带 #符号 的仓库链接、不带路径的引用),要那个符号的定义卡片;path、symbol、block、file 同 openRepoLink * findOccurrences:单击带下标的名字,问它在这份文档里出现在哪几处;id、version 同 probeDefinition * setFilter:改块筛选(整份替换);不在本地先改,等扩展端发回 filter 再按它显示 * setOnlyChanges:打开 / 关掉「只看改动」;同样等扩展端发回 filter 再按它显示 @@ -643,8 +647,8 @@ export type ExtensionToWebview * findNavigation:眼下查找栏的快捷键(下一处 / 上一处)归不归这个页面(查找栏开着、页面有焦点),变了才发 * openRepoLink:点了注释里的仓库路径链接,打开 path(从仓库根算起)。symbol 是类名、函数名或「类名.成员名」,lines 是文件里的几行(和 symbol 二选一), * 都没有就只打开文件或在代码阅读侧栏的树上定位目录; - * 没有 path 是不带路径的引用(`[Name]`、`[Class.member]`),扩展端按这份文件找 symbol:block 是它写在哪块的注释里(见 SymbolReference), - * 从那块的声明起一层层往外找 + * 没有 path 是不带路径的引用(`[Name]`、`[Class.member]`),扩展端按它写在的那份文件找 symbol:file 是那份文件(定义卡片里的注释才有,没有是这份文件), + * block 是它写在哪块的注释里(见 SymbolReference),从那块的声明起一层层往外找 * checkRepoLinks:预览里这批仓库路径问哪些文件或目录没有,这批不带路径的引用(references)问哪些找不到。id 标出是哪一次问 * checkRepoImages:预览里这批仓库图片,问哪些文件在,在的要预览里能直接用的地址。id 标出是哪一次问 * openCallTree:打开 anchor 那块的调用树。version 是这块所在的文档版本 @@ -664,7 +668,7 @@ 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, block?: string } + | { type: 'peekLink', id: number, path?: string, symbol: string, block?: string, file?: string } | { type: 'findOccurrences', id: number, version: number, offset: number } | { type: 'ask', askId: string, anchor: string, question: string, images: AskImage[] } | { type: 'stopAsk', askId: string } @@ -672,7 +676,7 @@ export type WebviewToExtension | { type: 'selectProfile', kind: ChatKind, id: string } | { type: 'occurrenceNavigation', available: boolean } | { type: 'findNavigation', available: boolean } - | { type: 'openRepoLink', path?: string, symbol?: string, block?: string, lines?: LineRange } + | { type: 'openRepoLink', path?: string, symbol?: string, block?: string, file?: string, lines?: LineRange } | { type: 'checkRepoLinks', id: number, paths: string[], references: SymbolReference[] } | { type: 'checkRepoImages', id: number, paths: string[] } | { type: 'openCallTree', version: number, anchor: string } diff --git a/vscode/webview-ui/src/codeReader/useRepoLinks.ts b/vscode/webview-ui/src/codeReader/useRepoLinks.ts index 5cfa5b7..2e168cf 100644 --- a/vscode/webview-ui/src/codeReader/useRepoLinks.ts +++ b/vscode/webview-ui/src/codeReader/useRepoLinks.ts @@ -43,7 +43,7 @@ function paint(links: RepoLinks): void { /** * 注释、说明、问 AI 的回答里的仓库路径链接,和注释里不带路径的引用:点了发给扩展端打开(不走 webview 把 http 链接交给外部打开的那条), * 画出来之后把路径、引用发去问哪些文件没有、哪些引用找不到、哪些是语言自带的名字;文档换了版本再问一次(引用跟着源码变)。 - * 不带路径的引用都带着它写在哪块(见 commentLinkOf),扩展端从那块的声明起找。 + * 不带路径的引用都带着它写在哪块(定义卡片里的还带着卡片那份文件,见 commentLinkOf),扩展端按那份文件、从那块的声明起找。 */ export function useRepoLinks(): void { const [links] = useState(() => new RepoLinks(postMessage))