🐛 fix(vscode): 代码阅读注释里的链接指到别的文件时用预览打开,和 ⌘ 点击跳到定义走同一条路

This commit is contained in:
cruldra committed 2026-10-08 07:17:26 +08:00
1 parent e04a90c877
commit bc7380c716
9 files changed
+123 -72

No files matched your search

+6 -4
View File
@@ -472,11 +472,11 @@
- 中文名的兜底(`languages/typescriptNames.ts`、`pythonNames.ts` 从 `@param` / `Args:` 取参数的中文名)是另一回事,照旧读源码。
- 渲染:webview 用 markdown-it(`html: false`、`linkify: true`),CommonMark 加 GFM 的表格、删除线:段落、强调、行内代码(含双反引号)、代码块(围栏、缩进四格)、列表、链接、标题、引用、表格。段内单个换行是软换行,空行分段。同一段文字只渲染一次(缓存最近 500 段)。
- 安全:源码里的 HTML 当文字显示;链接认 http / https / mailto,带协议的网址和邮箱自动成链接(`sample.py` 这种不带协议的不算),别的写法(`javascript:`、`command:`、`./`、`..`、绝对路径、盘符)照原文显示。http / https / mailto 由 VS Code 的 webview 自己拦下、交给宿主在外部打开,不用另发消息。http / https 图片不渲染(webview 的 CSP 不放行外部图片),`![alt](url)` 显示成 `!` 加链接。仓库内图片按原图比例显示,点了打开原图,没有就写「找不到图片」和路径。
- 仓库路径链接(`[名字](从仓库根起的路径)`,可带 `#类名` 或 `#类名.成员`)能点,点了打开文件并跳到符号(指到这份文件里的符号时在本预览里滚过去、闪一下,同下面的引用)。指到目录的在代码阅读侧栏的树上定位(展开并选中,和侧栏跟着当前文件走是同一个定位;侧栏没露着时亮出来),不切到 VS Code 资源管理器;树上没有(开着筛选时没摆它)时提示「侧栏的树上没有 …」,不退出筛选。
- 仓库路径链接(`[名字](从仓库根起的路径)`,可带 `#类名` 或 `#类名.成员`)能点,点了打开文件并跳到符号,和 ⌘ / Ctrl + 点击跳到别的文件里的定义是同一条路(`editor/openTarget.ts` 的 `openingFor`,见「跳到定义」):语言有代码阅读的在预览所在的编辑器组用预览打开、滚过去、闪符号那处(不带符号的只打开预览),没有代码阅读的用 VS Code 默认的打开方式(普通编辑器选中符号的名字,图片看图);指到这份文件里的符号时在本预览里滚过去、闪一下(同下面的引用)。指到目录的在代码阅读侧栏的树上定位(展开并选中,和侧栏跟着当前文件走是同一个定位;侧栏没露着时亮出来),不切到 VS Code 资源管理器;树上没有(开着筛选时没摆它)时提示「侧栏的树上没有 …」,不退出筛选。
- 不带路径的引用(KDoc 的 `[Name]`、`[Class.member]`)能点:
- 认哪些:只在注释的 Markdown 里认(说明正文、参数说明、`@return`、「异常」表、单独成块的注释,连带增删的;问 AI 的回答不认)。方括号里是标识符(ASCII 字母、数字、下划线、`$`,点分开的几段);中文这类不是标识符的(`[会话进展]`)照原样是文字。反引号里的是代码;紧跟在名字、`]`、`)` 后面的方括号是下标(`items[i]`、`a[0][1]`、`f()[k]`);后面紧跟 `(…)` 的是完整链接、紧跟 `[` 的是参考式链接,都不算。webview 在 `markdown.ts` 加一条行内规则(排在 markdown-it 的 link 后面,渲染时 `env.symbolLinks` 为真才认),写成地址为 `symbol:名字` 的链接(作者写不出这种地址,`validateLink` 不放行)。
- 指到哪(扩展端,`symbolLinks.ts` 的 `resolveReference`,按 KDoc 的规矩):① 本文件定义的:文档符号(`vscode.executeDocumentSymbolProvider`,同仓库链接找符号那一套)顶层名字对上的(类、函数、常量、变量;落在导入语句里的名字不算),`Class.member` 在它底下找成员;② 本文件导入进来的:名字(`Class.member` 的第一段)是某条导入引进来的(预览已有的导入块 facts,`ImportFacts.items`:起了别名的按别名认,问原来那个名字;整模块导入按第一节认,起了别名的问最后一节),在导入语句里这个名字的位置上问语言扩展定义在哪(`vscode.executeDefinitionProvider`,同跳到定义,几处取第一处),`Class.member` 再在目标文件的文档符号里找:定义落在哪个符号里就在它底下找,落在文件开头(导入的是模块)就从顶层找;③ 都没有就找不到。本文件定义了这个名字、却没有这个成员时也算找不到,不再去导入里找。
- 问答同仓库路径链接(`useRepoLinks`、`RepoLinks`):画出来后把页面上的仓库路径和引用一起发去问(`checkRepoLinks { paths, symbols }` → `repoLinksChecked { missing, unresolved }`),问到之前是普通链接,找不到的画成坏链接(红色波浪线,悬停「找不到」);文档换了版本(收到 `document`)再问一次,引用跟着源码变。点了发 `openRepoLink`(不带 `path`,只带 `symbol`):找到了在预览所在的编辑器组打开目标文件、选中符号的名字(同完整链接的后半段),目标在这份文件里时在本预览里滚过去、闪一下(同跳到定义的闪);找不到就标坏。
- 问答同仓库路径链接(`useRepoLinks`、`RepoLinks`):画出来后把页面上的仓库路径和引用一起发去问(`checkRepoLinks { paths, symbols }` → `repoLinksChecked { missing, unresolved }`),问到之前是普通链接,找不到的画成坏链接(红色波浪线,悬停「找不到」);文档换了版本(收到 `document`)再问一次,引用跟着源码变。点了发 `openRepoLink`(不带 `path`,只带 `symbol`):找到了同完整链接的后半段打开目标(语言有代码阅读的用预览、滚过去、闪,没有的用默认的打开方式选中名字),目标在这份文件里时在本预览里滚过去、闪一下(同跳到定义的闪);找不到就标坏。
- 样子:正文用界面字体(`--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 里的名字(反引号里的代码)不能点,不参与跳到定义,能点的只有上面的链接和引用。
@@ -487,10 +487,11 @@
- ⌘ / Ctrl + 点击:
- 一处定义,在当前文件:原地滚过去;
- 一处定义,另一个文件,语言有代码阅读:在同一个编辑器组用预览打开(`vscode.openWith`),滚过去;这个文件的预览已经在这一组开着就切过去、发给它,不重新打开;
- 一处定义,语言没有代码阅读:在同一个编辑器组用普通编辑器打开,选中定义处的名字;
- 一处定义,语言没有代码阅读:在同一个编辑器组用 VS Code 默认的打开方式(`vscode.open`,一般是普通编辑器)打开,选中定义处的名字;
- 几处定义:先弹挑选框(每项「相对路径:行号」,下面一行是那行源码),挑一处再按上面跳;
- 没有定义:不动。
- 和普通编辑器里 ⌘ + 点击一样:目标是否按「预览标签」打开听 `workbench.editor.enablePreview`;`workbench.editor.enablePreviewFromCodeNavigation` 为假(默认)时先把当前预览标签固定住,免得被目标替掉。
- 打开别的文件只有一条路(预览的 `openElsewhere`,用哪种编辑器由 `editor/openTarget.ts` 的 `openingFor` 按目标文件的语言定,侧栏点文件也按它):跳到定义、注释里的仓库链接、不带路径的引用都走它。
- 闪哪一处:页面上**就是这处定义**的最具体的那一处,只按源码位置判断(定义提供者给的定义处下标,对上块和属性行带的源码范围),不看属性名文字:
1. 定义是块自己的名字(函数名、类名;只声明一个变量时的变量名,解构模式整个算)→ 整块。块的这段范围由扩展端从 facts 取(`BlockView.defines`),和翻译插件无关。
2. 定义是属性表里某一行 / 子行讲的东西(参数、导入项、一条语句声明几个变量时的某一个、类的泛型参数、对象字面量一项的键——TypeScript 对象的属性名就是定义,嵌套几层都算)→ 只闪这一行 / 子行。范围是翻译插件从 facts 取的节点(`PropertyRow.defines`、`PropertyItem.defines`;字典 / 对象的项标的是键,展开项不标),定义处落在范围里就算(`*args` 的定义在 `args` 上、解构参数里的名字都对得上)。
@@ -2146,6 +2147,7 @@ vscode/src/codeReader/
editor/definitions.ts 查定义(vscode.executeDefinitionProvider)、规则查到的位置换成 VS Code 的、几处定义的挑选框
editor/definitionSources.ts 名字的定义去哪儿查:文档里规则标的引用问规则,其余问语言扩展
editor/definitionAction.ts 查到的定义 → 怎么跳(纯函数)
editor/openTarget.ts 打开别的文件用哪种编辑器:语言有代码阅读的用预览(滚到定义处),其余用默认的打开方式(选中名字);侧栏点文件、跳到定义、注释里的链接共用
editor/occurrences.ts 查名字的各处出现(vscode.executeDocumentHighlights)
editor/occurrenceNavigation.ts 跳到下一处 / 上一处高亮:命令、上下文键
index.ts registerCodeReader(context): CodeReaderApi
@@ -2168,7 +2170,7 @@ vscode/webview-ui/code-reader.html + src/codeReader/ 预览界面(React,Vit
- 块标签:拆法(`docTags.test.ts`:多行续写、空行结束、`@returns` / `@exception` 同义、JSDoc 的 `{类型}`、`[名字=默认值]`、横杠、标签前后的正文、没有标签时原样、别的标签和围栏代码块里的行留在正文、异常名写成引用);卡片(`translators/zhTemplate.test.ts` 的「说明里的块标签」):`fixtures/conversationBrief.py`(luffy 的 `render_conversation_brief`)说明里没有 `@param` / `@return`,输入参数表「说明」列四个参数的原文,输出参数后面跟 `@return`;`fixtures/localRuntimeGate.py`(luffy 的 `gate.py`)「异常」表两行,第一行是完整链接、第二行是不带路径的引用;对不上的 `@param` 排在说明下面、提示色、悬停「没有这个参数」;类的 `@param` 照写成表、异常类的对到构造参数(`*args` 去掉星号对)。React 组件(字段没注释时用 `@param`、`props.x`、讲整个参数的跟在构造参数后面、对不上的、异常表)和事件处理函数(`rules/react.test.ts`)、FastAPI 请求表的说明列和响应后面的 `@return`(`rules/fastapi.test.ts`)、Rust 函数和结构体(`languages/rust.test.ts`)、数据类字段(`rules/dataclass.test.ts`)。
- 注释的 Markdown:注释块、函数和类「说明」的视图数据是 `{ markdown }`,原文照抄(reST 角色、双反引号、`{@link}`、HTML 都不转换,块标签拆出去),行注释连成的块留得住缩进四格的代码块;渲染时 HTML 当文字、带协议的网址和邮箱成链接、不带协议的文件名不成链接,`javascript:` / `command:` / `./` / `..` / 绝对路径的链接和 http 图片不渲染,仓库内图片按原图比例显示,点了打开原图,没有就写「找不到图片」,仓库路径链接能点、点了打开文件并跳到符号;跳到定义不认 Markdown 里的名字。不带路径的引用:行内规则(`webview-ui/.../markdown.test.ts`:`[Name]`、`[A.b]` 是 `symbol:` 链接,`` `[x]` ``、`items[i]`、`[会话进展]`、`[名字](路径)`、`[Agent](javascript:x)` 各自的结果,问 AI 的回答不认、带增删的注释照样认);问答(`repoLink.test.ts`:`symbol:` 地址只认标识符、点了发不带 `path` 的 `openRepoLink`、和仓库路径一起问、找不到的标坏、打开成功不再标、同样的链接不再问、文档换了版本再问);扩展端(`symbolLinks.test.ts`:本文件优先于导入且不问语言扩展、本文件的 `Class.member`、类有成员没有就是找不到、导入的名字问定义再在那个类底下找成员、整模块导入从顶层找、找不到;Python / TypeScript 各种导入写法引进来的名字和问定义的位置,`gate.py` 的导入)。
- 行替换:多行块替换后缩进正确。
- 跳到定义:TS / Python 译文和属性表里的名字、点分模块名每一节、模块字符串带下标且指回源码原文,关键字、运算符、说明词不带(前面有中文注释也对得上);查到的定义 → 不动 / 原地 / 预览 / 普通编辑器 / 挑选框;webview 里按下标找目标块、要展开的祖先块和属性行,滚动位置。闪哪一处(TS、Python 源码走完整的语言插件 + 翻译插件 + 视图数据):参数 → 它那个子行,导入项 → 它那一项,多变量声明里的一个 → 它的名称行,类的泛型参数 → 「类型参数」那一行,函数名 / 类名 / 单个变量名(含解构模式)→ 整块,循环变量 → 所在的块,翻译插件没填 `defines` → 整块(照样展开属性行);翻译插件给参数、导入项、声明的变量、泛型参数标的范围指回源码原文;块自己的名字只有函数、类和单变量声明有。
- 跳到定义:TS / Python 译文和属性表里的名字、点分模块名每一节、模块字符串带下标且指回源码原文,关键字、运算符、说明词不带(前面有中文注释也对得上);查到的定义 → 不动 / 原地 / 别的文件 / 挑选框,别的文件用哪种编辑器(`editor/openTarget.test.ts`:有代码阅读的用预览、带位置时滚到那一行闪定义处,没有的用默认的打开方式选中名字,打不开成文本的用默认的打开方式);webview 里按下标找目标块、要展开的祖先块和属性行,滚动位置。闪哪一处(TS、Python 源码走完整的语言插件 + 翻译插件 + 视图数据):参数 → 它那个子行,导入项 → 它那一项,多变量声明里的一个 → 它的名称行,类的泛型参数 → 「类型参数」那一行,函数名 / 类名 / 单个变量名(含解构模式)→ 整块,循环变量 → 所在的块,翻译插件没填 `defines` → 整块(照样展开属性行);翻译插件给参数、导入项、声明的变量、泛型参数标的范围指回源码原文;块自己的名字只有函数、类和单变量声明有。
- 高亮各处出现:段的源码范围和出现重叠才亮、边界相接不算、没下标的不亮,讲别处名字的段(`refers`)按那个名字算、自己照抄的位置不算(`occurrences.test.ts` 的 `sourceOf`),点分模块名每一节各按自己的范围认(真实的 Python 译文和属性表:定义处按写入、用到处按读取);写入 → 强高亮色,读取、按文字 → 普通高亮色,同时碰上按写入;单击名字问、点空白处清、点按钮 / 链接 / 筛选栏不动、名字在按钮里照样问,拖选、双击第二下、带修饰键的点击不动,⌘ / Ctrl + 点击照旧发跳到定义、不问出现处;回答连同名字显示、空回答不亮、换名字时回答到前旧的留着、迟到的回答丢掉、答过「没有」再点照样再问、清掉后才到的回答丢掉、换文档版本或翻译清掉而同版本同翻译重发不清、没有文档时不问。键盘跳(`occurrences.test.ts`、`reveal.test.ts`、`editor/occurrenceNavigation.test.ts`):回答照源码先后排、点中的那处是当前那一处、「2/5」和「N 处」;往后 / 往前、到头接着跳,找不到的跳过、总数照旧、一处都找不到不动,点中的不在回答里从点中的地方数起;只有当前那一处描框;能不能跳(高亮、焦点、输入框)第一次必报、之后变了才报;页面上找第一处显示着它的地方、要展开的块 / 属性行 / 子行、跳过筛掉 / 编辑 / 盖住的块、Markdown 不算;`package.json` 的两条命令、默认键、`when`、命令面板,扩展和页面的代码里不写键、不拦键,上下文键有一个预览能跳就真、变了才设,命令发给能跳的预览、没有就发给活动的;按键那一刻的位置跳转时回一次、隔太久 / 点过鼠标 / 没按过键不回,跳完守住跳到的位置、过了 `KEY_SCROLL_MS` 或又按键、点鼠标不守。
- 中文名:
- 语言插件认写法(`languages/pythonNames.test.ts`、`languages/typescriptNames.test.ts`,真实解析):Python 注解 `Annotated`(`typing.Annotated`、`t.Comment`,f-string、别的调用不算)、装饰器(函数、类)、行尾记号(多行参数表、`*args`、`self.x`、变量、字段、枚举成员,记号前面有别的字,记号的范围)、单独成行 / 那一行没有定义的不算、函数那一行给参数、docstring 的谷歌写法和 Sphinx 写法(带类型、星号、续行、别的段落不算)、第一小句和 12 个字的限制、明写的记号盖过说明文字、没取的记号照样去掉;TypeScript 装饰器(类、`@d export class`、字段、构造函数和方法的参数、方法隔着别的装饰器、名字最后一节)、行尾 `//` 记号(参数、变量、枚举成员、类字段、接口属性,块注释不算)、JSDoc `@param`(类型、短横、可选、属性不算、export 函数、方法、箭头函数变量、类字段)、JavaScript 默认值参数;预筛和各方言的文件。
@@ -4,8 +4,8 @@ import { definitionAction } from './definitionAction'
const CURRENT = 'file:///ws/app/routes.py'
function at(uri: string, line: number, supported = true, character = 4): DefinitionTarget {
return { uri, line, character, supported }
function at(uri: string, line: number, character = 4): DefinitionTarget {
return { uri, line, character }
}
describe('⌘ + 点击查到定义之后怎么跳', () => {
@@ -18,27 +18,22 @@ describe('⌘ + 点击查到定义之后怎么跳', () => {
expect(definitionAction([target], CURRENT)).toEqual({ kind: 'here', target })
})
it('另一个文件、语言有代码阅读:用预览打开', () => {
it('另一个文件:交给打开别的文件那一条路', () => {
const target = at('file:///ws/luffy_agent/core/db.py', 30)
expect(definitionAction([target], CURRENT)).toEqual({ kind: 'preview', target })
})
it('另一个文件、语言没有代码阅读:用普通文本编辑器打开', () => {
const target = at('file:///ws/native/ext.c', 7, false)
expect(definitionAction([target], CURRENT)).toEqual({ kind: 'text', target })
expect(definitionAction([target], CURRENT)).toEqual({ kind: 'elsewhere', target })
})
it('几处定义:让用户挑,挑好的一处再按上面的规则跳', () => {
const stub = at('file:///ws/typings/db.pyi', 3)
const source = at('file:///ws/luffy_agent/core/db.py', 30)
expect(definitionAction([stub, source], CURRENT)).toEqual({ kind: 'pick', targets: [stub, source] })
expect(definitionAction([source], CURRENT)).toEqual({ kind: 'preview', target: source })
expect(definitionAction([source], CURRENT)).toEqual({ kind: 'elsewhere', target: source })
})
it('同一处报了几遍算一处;同一行不同列算两处', () => {
const target = at('file:///ws/luffy_agent/core/db.py', 30)
expect(definitionAction([target, { ...target }], CURRENT)).toEqual({ kind: 'preview', target })
const other = at(target.uri, 30, true, 20)
expect(definitionAction([target, { ...target }], CURRENT)).toEqual({ kind: 'elsewhere', target })
const other = at(target.uri, 30, 20)
expect(definitionAction([target, other], CURRENT)).toEqual({ kind: 'pick', targets: [target, other] })
})
})
@@ -1,20 +1,19 @@
/** 一处定义。uri 是 Uri.toString(),用来和当前文件比、去重;supported:目标文件的语言有代码阅读。 */
/** 一处定义。uri 是 Uri.toString(),用来和当前文件比、去重。 */
export interface DefinitionTarget {
uri: string
line: number
character: number
supported: boolean
}
/**
* ⌘ / Ctrl + 点击查到定义之后怎么做:
* none 不动;pick 让用户从几处里挑一处,挑好的那处再交给 definitionAction 决定;
* here 定义就在当前文件,预览原地滚过去;preview 用代码阅读预览打开目标文件;text 目标语言没有代码阅读,用普通文本编辑器打开。
* here 定义就在当前文件,预览原地滚过去;elsewhere 在另一个文件,用哪种编辑器打开见 openTarget.ts 的 openingFor。
*/
export type DefinitionAction<T extends DefinitionTarget>
= | { kind: 'none' }
| { kind: 'pick', targets: T[] }
| { kind: 'here' | 'preview' | 'text', target: T }
| { kind: 'here' | 'elsewhere', target: T }
/** 定义提供者查到的几处(可能重复)→ 怎么跳。current 是当前文件的 Uri.toString()。 */
export function definitionAction<T extends DefinitionTarget>(targets: readonly T[], current: string): DefinitionAction<T> {
@@ -25,7 +24,5 @@ export function definitionAction<T extends DefinitionTarget>(targets: readonly T
if (unique.length > 1)
return { kind: 'pick', targets: unique }
const [target] = unique
if (target.uri === current)
return { kind: 'here', target }
return { kind: target.supported ? 'preview' : 'text', target }
return { kind: target.uri === current ? 'here' : 'elsewhere', target }
}
+1 -2
View File
@@ -149,7 +149,7 @@ export async function definitionKind(
}
/** 查到的各处定义打开目标文件补上行文字、下标;目标打不开成文本的(例如 Python 命名空间包指向的目录)略过。 */
export async function findDefinitions(locations: readonly FoundLocation[], supports: (languageId: string) => boolean): Promise<Definition[]> {
export async function findDefinitions(locations: readonly FoundLocation[]): Promise<Definition[]> {
const found = await Promise.all(locations.map(async (location): Promise<Definition[]> => {
const { start } = location.range
try {
@@ -158,7 +158,6 @@ export async function findDefinitions(locations: readonly FoundLocation[], suppo
uri: location.uri.toString(),
line: start.line,
character: start.character,
supported: supports(document.languageId),
location,
offset: document.offsetAt(start),
lineText: document.lineAt(start.line).text,
@@ -0,0 +1,40 @@
import type { Range, Uri } from 'vscode'
import { describe, expect, it, vi } from 'vitest'
import { workspace } from 'vscode'
import { languageOf, openingFor } from './openTarget'
vi.mock('vscode', () => ({
workspace: { openTextDocument: vi.fn() },
}))
const supports = (languageId: string): boolean => languageId === 'python'
const range = { start: { line: 41, character: 6 }, end: { line: 41, character: 28 } } as Range
const spot = { range, offset: 1200 }
describe('打开别的文件用哪种编辑器', () => {
it('语言有代码阅读:用预览打开,给了位置就滚到那一行、闪这处定义', () => {
expect(openingFor('python', supports, spot)).toEqual({ kind: 'preview', reveal: { line: 41, definition: 1200 } })
})
it('语言有代码阅读、只打开文件(不带符号的仓库链接):用预览打开,不滚', () => {
expect(openingFor('python', supports)).toEqual({ kind: 'preview' })
})
it('语言没有代码阅读:VS Code 默认的打开方式,选中定义处的名字', () => {
expect(openingFor('c', supports, spot)).toEqual({ kind: 'default', selection: range })
})
it('打不开成文本(图片):VS Code 默认的打开方式', () => {
expect(openingFor(undefined, supports)).toEqual({ kind: 'default' })
})
})
describe('文件的语言', () => {
it('按 VS Code 打开的文档认;打不开成文本时没有', async () => {
const openTextDocument = vi.mocked(workspace.openTextDocument)
openTextDocument.mockResolvedValueOnce({ languageId: 'python' } as Awaited<ReturnType<typeof workspace.openTextDocument>>)
expect(await languageOf({} as Uri)).toBe('python')
openTextDocument.mockRejectedValueOnce(new Error('binary'))
expect(await languageOf({} as Uri)).toBeUndefined()
})
})
@@ -0,0 +1,37 @@
import type { Range, Uri } from 'vscode'
import type { Reveal } from './messages'
import { workspace } from 'vscode'
/** 别的文件里要打开的一处:名字的范围(普通编辑器选中它)和起点下标(预览滚过去、闪这处定义)。 */
export interface TargetSpot {
range: Range
offset: number
}
/**
* 打开一个文件用哪种编辑器:preview 用代码阅读预览打开,带了 reveal 就滚过去、闪这处;
* default 交给 VS Code 默认的打开方式(普通编辑器,带了 selection 就选中它;图片看图)。
*/
export type Opening
= | { kind: 'preview', reveal?: Reveal }
| { kind: 'default', selection?: Range }
/**
* 语言有代码阅读的用预览,其余用 VS Code 默认的打开方式;languageId 是 undefined 表示打不开成文本(图片等二进制文件)。
* 侧栏点文件、⌘ / Ctrl + 点击跳到别的文件里的定义、点注释里的仓库链接和不带路径的引用,都按它定。
*/
export function openingFor(languageId: string | undefined, supports: (languageId: string) => boolean, spot?: TargetSpot): Opening {
if (languageId !== undefined && supports(languageId))
return { kind: 'preview', ...(spot && { reveal: { line: spot.range.start.line, definition: spot.offset } }) }
return { kind: 'default', ...(spot && { selection: spot.range }) }
}
/** 按 VS Code 的判断(含 files.associations)取文件的 languageId;图片等二进制文件打不开成文本,返回 undefined。 */
export async function languageOf(uri: Uri): Promise<string | undefined> {
try {
return (await workspace.openTextDocument(uri)).languageId
}
catch {
return undefined
}
}
+23 -28
View File
@@ -12,8 +12,9 @@ import type { TreeSitterRuntime } from '../runtime'
import type { ImportedName, LinkTarget, SymbolNode } from '../symbolLinks'
import type { IndexedEntry } from '../workspaceIndex'
import type { BlockFilter } from './blockFilter'
import type { Definition, FoundLocation } from './definitions'
import type { FoundLocation } from './definitions'
import type { ExtensionToWebview, Occurrence, Reveal, WebviewToExtension } from './messages'
import type { TargetSpot } from './openTarget'
import { realpath } from 'node:fs/promises'
import { dirname } from 'node:path'
import { commands, Range, Uri, window, workspace, WorkspaceEdit } from 'vscode'
@@ -37,8 +38,9 @@ import { DefinitionSources } from './definitionSources'
import { documentMessage } from './documentMessage'
import { NavigationContext, OCCURRENCES_CONTEXT } from './occurrenceNavigation'
import { occurrencesOf } from './occurrences'
import { languageOf, openingFor } from './openTarget'
import { pathPreviewOf } from './pathPreview'
import { findRepoSymbol, isImageFile, isRepoImagePath, repoRootOf, resolveRepoEntry } from './repoPath'
import { findRepoSymbol, isRepoImagePath, repoRootOf, resolveRepoEntry } from './repoPath'
export const PREVIEW_VIEW_TYPE = 'superpowers.codeReader.preview'
@@ -620,8 +622,7 @@ class Preview {
if (version !== document.version)
return
const current = document.uri.toString()
const supports = (languageId: string): boolean => this.registry.languageFor(languageId) !== undefined
let action = definitionAction(await findDefinitions(await this.definitions.locate(version, offset), supports), current)
let action = definitionAction(await findDefinitions(await this.definitions.locate(version, offset)), current)
if (action.kind === 'pick') {
const picked = await pickDefinition(action.targets)
action = definitionAction(picked ? [picked] : [], current)
@@ -629,13 +630,12 @@ class Preview {
if (action.kind === 'none' || action.kind === 'pick')
return
const { target } = action
const reveal: Reveal = { line: target.line, definition: target.offset }
if (action.kind === 'here') {
this.reveal(reveal)
this.reveal({ line: target.line, definition: target.offset })
return
}
try {
await this.openDefinition(action.kind, target, reveal)
await this.openElsewhere(target.location.uri, { range: target.location.range, offset: target.offset })
}
catch (err) {
const message = err instanceof Error ? err.message : String(err)
@@ -645,18 +645,24 @@ class Preview {
}
/**
* 和普通编辑器里 ⌘ + 点击一样:在同一个编辑器组打开目标(是否按「预览标签」打开听 VS Code 的设置),
* 并按 workbench.editor.enablePreviewFromCodeNavigation 把当前这个预览标签固定住,免得被目标替掉。
* 在预览所在的编辑器组打开别的文件,和普通编辑器里 ⌘ + 点击一样:语言有代码阅读的用预览打开,给了 spot 就滚过去、闪这处;
* 其余用 VS Code 默认的打开方式,给了 spot 就选中它(见 openingFor)。目标是否按「预览标签」打开听 VS Code 的设置,
* workbench.editor.enablePreviewFromCodeNavigation 为假时先把当前这个预览标签固定住,免得被目标替掉。
*/
private async openDefinition(kind: 'preview' | 'text', target: Definition, reveal: Reveal): Promise<void> {
private async openElsewhere(uri: Uri, spot?: TargetSpot): Promise<void> {
if (this.panel.active && !workspace.getConfiguration('workbench.editor').get<boolean>('enablePreviewFromCodeNavigation', false))
await commands.executeCommand('workbench.action.keepEditor')
const options: TextDocumentShowOptions = { viewColumn: this.panel.viewColumn }
const { uri, range } = target.location
if (kind === 'text')
await window.showTextDocument(uri, { ...options, selection: range })
const opening = openingFor(await languageOf(uri), languageId => this.registry.languageFor(languageId) !== undefined, spot)
if (opening.kind === 'default') {
await commands.executeCommand('vscode.open', uri, { ...options, ...(opening.selection && { selection: opening.selection }) })
return
}
const open = (): Thenable<unknown> => commands.executeCommand('vscode.openWith', uri, PREVIEW_VIEW_TYPE, options)
if (opening.reveal)
await this.owner.openAt(uri, opening.reveal, open)
else
await this.owner.openAt(uri, reveal, () => commands.executeCommand('vscode.openWith', uri, PREVIEW_VIEW_TYPE, options))
await open()
}
/** 预览里这批仓库路径,哪些文件或目录不在;这批不带路径的引用,哪些找不到。 */
@@ -754,7 +760,7 @@ class Preview {
return
}
if (symbol === undefined) {
await this.showRepoFile(uri)
await this.openElsewhere(uri)
this.markRepoLinkOpened(rel)
return
}
@@ -818,25 +824,14 @@ class Preview {
}
}
/** 跳到链接指的符号:在这份文件里就在本预览里滚过去、闪一下;别的文件在预览所在的编辑器组打开、选中符号的名字。 */
/** 跳到链接指的符号:在这份文件里就在本预览里滚过去、闪一下;别的文件同 ⌘ + 点击跳到定义(见 openElsewhere)。 */
private async showLinkTarget({ uri, selection }: LinkTarget): Promise<void> {
if (uri === this.document.uri.toString()) {
this.reveal({ line: this.document.positionAt(selection.startIndex).line, definition: selection.startIndex })
return
}
const doc = await workspace.openTextDocument(Uri.parse(uri))
await this.showRepoFile(doc.uri, new Range(doc.positionAt(selection.startIndex), doc.positionAt(selection.endIndex)))
}
/** 在预览所在的编辑器组打开文件,并把预览标签固定住,免得被它替掉。图片用默认打开方式,才看得到图。 */
private async showRepoFile(uri: Uri, selection?: Range): Promise<void> {
if (this.panel.active && !workspace.getConfiguration('workbench.editor').get<boolean>('enablePreviewFromCodeNavigation', false))
await commands.executeCommand('workbench.action.keepEditor')
if (selection === undefined && isImageFile(uri.fsPath)) {
await commands.executeCommand('vscode.open', uri, { viewColumn: this.panel.viewColumn })
return
}
await window.showTextDocument(uri, { viewColumn: this.panel.viewColumn, ...(selection && { selection }) })
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 {
+1 -6
View File
@@ -33,14 +33,9 @@ export function isSafeRepoPath(rel: string): boolean {
/** 注释里当图片显示的文件后缀。webview 的 repoImage.ts 用同一份。 */
const IMAGE_FILE = /\.(?:avif|bmp|gif|ico|jpe?g|png|svg|webp)$/i
/** 文件名是图片。绝对路径也能用来判断,不检查有没有跑出仓库。 */
export function isImageFile(filePath: string): boolean {
return IMAGE_FILE.test(filePath)
}
/** 从仓库根起、能在注释里当图片显示的路径。别的仓库文件不给预览地址。 */
export function isRepoImagePath(rel: string): boolean {
return isSafeRepoPath(rel) && isImageFile(rel)
return isSafeRepoPath(rel) && IMAGE_FILE.test(rel)
}
/** target 解析之后还在 root 里面(root 自己不算)。 */
+4 -13
View File
@@ -9,6 +9,7 @@ import { shellCommandRules } from './commands/shellCommands'
import { systemCommandRules } from './commands/systemCommands'
import { BlockFilter } from './editor/blockFilter'
import { NEXT_OCCURRENCE, PREVIOUS_OCCURRENCE } from './editor/occurrenceNavigation'
import { languageOf, openingFor } from './editor/openTarget'
import { PATH_PREVIEW_SCHEME } from './editor/pathPreview'
import { PREVIEW_VIEW_TYPE, PreviewEditorProvider } from './editor/previewEditor'
import { showPreview, showSource } from './editor/switchEditor'
@@ -59,20 +60,10 @@ const LANGUAGE_IDS_CONTEXT = 'superpowers.codeReader.languageIds'
const FILES_VIEW = 'superpowers.codeReader.files'
const OPEN_COMMAND = 'superpowers.codeReader.open'
/** 按 VS Code 的判断(含 files.associations)取文件的 languageId;图片等二进制文件打不开成文本,返回 undefined。 */
async function languageOf(uri: Uri): Promise<string | undefined> {
try {
return (await workspace.openTextDocument(uri)).languageId
}
catch {
return undefined
}
}
/** 侧栏点文件:语言受支持就用预览打开,否则用普通编辑器。 */
/** 侧栏点文件:语言受支持就用预览打开,否则用 VS Code 默认的打开方式(见 openingFor)。 */
async function openFile(uri: Uri, registry: CodeReaderRegistry): Promise<void> {
const languageId = await languageOf(uri)
if (languageId !== undefined && registry.languageFor(languageId))
const opening = openingFor(await languageOf(uri), languageId => registry.languageFor(languageId) !== undefined)
if (opening.kind === 'preview')
// 和 vscode.open 一样按「预览标签」打开(用户关了 workbench.editor.enablePreview 时 VS Code 自会固定)
await commands.executeCommand('vscode.openWith', uri, PREVIEW_VIEW_TYPE, { preview: true })
else