语言插件按和语言无关的 id 报块属性(通用的由宿主从 facts 推),翻译插件起名,webview 只按 id 筛; 筛选整个工作区共用、记在 workspaceState;JavaScript 不再显示「抽象:否」「仅类型:否」
70 KiB
代码阅读(Code Reader)设计
目标
代码几乎全由 agent 写,逐行读太慢。给插件加一个独立的「代码阅读」入口:打开一个源码文件时,可以切到「预览」,把代码拆成一棵代码块树,每块用模板直译成中文,扫一眼就知道这个文件在干什么;要看细节时单块切回源码,并且可以直接改。
它和现有的 Superpowers 看板(活动栏小火箭)是两套东西,只是放在同一个插件(vscode/)里。
视觉和交互以 Figma 原型为准:docs/prototype/code-reader/figma-plugin/(code.js 里的数据和 test.js 里的期望值就是本文的样例)。
已确认的决定
| 事项 | 决定 |
|---|---|
| 流程 | 源码 →(tree-sitter)语法树 → 通用代码块树 → 译文 |
| 第一版翻译 | 只做中文模板直译;AI 解读以后作为另一个翻译插件加 |
| 第一版语言 | TypeScript / TSX(含 JavaScript / JSX)、Python |
| 扩展方式 | 写代码注册:语言插件、翻译插件都实现约定接口,通过本插件 activate() 导出的 API 注册;内置实现也走同一个 API |
| 多种翻译怎么选 | 预览顶部筛选栏最右边的下拉框,整个文件一起换,按语言记住选择;只有一种时不显示 |
| 块筛选 | 预览顶部钉住一条筛选栏,按块的属性(种类、访问级别、异步……)筛;属性由语言插件按和语言无关的 id 报(通用的由宿主从 facts 推),名字由翻译插件起,webview 不认识任何语言;筛选整个工作区共用、记住 |
| 侧栏点文件 | 支持的语言默认打开成「预览」;不支持的语言用普通编辑器打开 |
| 块内修改 | 改的就是文件本身:未保存标记、⌘S 保存、⌘Z 撤销都照常,切回「预览」按新代码重新翻译 |
| 折叠 | 有子块的块、属性表里带子行的行、带下一层的子行(字典 / 对象里嵌套的字典 / 对象)能折叠;函数块(含方法、嵌套函数)和类块默认收起主体、属性表照常显示,其余块、属性行和子行默认展开;用户点过的按他点的记住 |
| 函数、类的说明 | TypeScript 紧贴在函数、类前面的 JSDoc,Python 函数、类主体开头的 docstring 是它自己的说明:进属性表「说明」一行,不再单独成一块注释;它的源码行算在函数、类块的头部里,「编辑」时和签名一起改 |
| 注释按 Markdown 显示 | 注释块和函数、类的「说明」按 Markdown 渲染,像 Kotlin 的 KDoc:去掉注释符号、引号和共同缩进后的原文原样交给 Markdown,不按语言转换(reStructuredText 的 :func:、JSDoc 的 @param 都照抄,注释以后慢慢改成 Markdown 写法);段内单个换行是软换行,空行分段 |
| 跳到定义 | 和普通编辑器一样 ⌘ / Ctrl + 点击名字跳到定义,定义由 VS Code 装的语言扩展(Pylance、TypeScript 语言服务……)查;目标语言有代码阅读就在预览里打开 |
| 侧栏文件树 | 只看和打开:展开目录、打开文件、刷新、全部折叠,外加给目录 / 文件记注释;不做新建 / 重命名 / 删除 |
| 文件注释 | 读代码时给目录 / 文件随手记一句:存在各工作区文件夹的 .spx/code-notes.json,跟着仓库走;在 VS Code 里改名、挪位置、删除时注释跟着变 |
| 译文用词 | 结构照抄原语言,只翻关键字、内置类型、运算符;用专业术语(export → 公共,catch → 捕获,=== → 全等) |
| 函数、类、导入、常量、变量块 | 预览不显示一行译文,显示树形属性表;属性值里的类型、表达式照常直译 |
| 字典 / 对象字面量 | 常量、变量的值是 Python 字典 / TypeScript 对象字面量时不挤成一长行:值写「字典 N 项」/「对象 N 项」,各项展开成「键 | 值」两列的子行,值又是字典 / 对象时一层层往下展开;参数默认值照旧一行 |
用户可见行为
活动栏与侧栏
- 活动栏新增图标(书本,
book-open-text样式),视图容器标题「代码阅读」,里面一个视图「代码阅读」。 - 视图是工作区文件树:每个工作区文件夹一个根节点(默认展开),目录在前、文件在后,名字不区分大小写排序;遵守
files.exclude。 - 树节点用
resourceUri,图标跟随用户的文件图标主题,git 改动颜色(M / U)由 VS Code 自动加上,外观与自带资源管理器一致。 - 视图标题栏:刷新、全部折叠(
showCollapseAll)。文件变化时自动刷新(防抖)。 - 点文件:语言受支持 → 用「代码阅读预览」自定义编辑器打开;否则
vscode.open。
文件注释
读代码时给目录或文件随手记一句(原型画板「文件注释 · 悬停出按钮 / 填写 / 已保存」)。
- 鼠标停在目录 / 文件行上,行尾出现按钮(
view/item/context的inline组):没注释时「添加注释」($(comment)),有注释时「编辑注释」($(edit))。右键菜单里也有同一个命令。工作区根节点没有。 - 两个命令只挂在侧栏行上,命令面板里藏起来(
when: false):面板里没有「哪一行」可言,按当前文件记还得另判断它有没有注释,没有必要。 - 点按钮弹 VS Code 顶部的单行输入框(
window.showInputBox):标题添加注释 · <名字>/编辑注释 · <名字>,编辑时预填原注释(整段选中),提示按 Enter 保存,按 Esc 取消;清空后保存即删除注释。回车保存(去掉首尾空白),Esc 或点别处不改,清空后保存即删除。VS Code 会在提示后面自己再补一句「(Press 'Enter' to confirm or 'Escape' to cancel)」,关不掉。 - 显示:有注释的行名字后面灰字是注释(
TreeItem.description,放不下 VS Code 自己截断);悬停提示是 Markdown,第一行粗体的相对路径,下一行注释全文,注释里的 Markdown 符号原样显示。没注释的行和原来一样。 - contextValue:工作区根
root,目录folder,文件file;有注释的后面加noted。菜单按viewItem =~ /^(file|folder)$/(添加)和/^(file|folder) noted$/(编辑)切换。 - 存储:每个工作区文件夹一份
<工作区文件夹>/.spx/code-notes.json,{ "相对路径": "注释" },路径用/分隔、相对这个工作区文件夹;和.spx里别的文件同一套写法:文件缺失或解析失败当作没有注释,写时按需建.spx,两格缩进、键排好序、末尾换行。存第一条注释前不生成文件;注释删光后文件留着({})。 - 在 VS Code 里改名、挪位置(资源管理器拖动、
WorkspaceEdit等,走workspace.onDidRenameFiles):注释跟着走,目录连带里面各层的注释一起走;挪到另一个工作区文件夹就搬到那边的文件里,挪出工作区就不要了。删除(onDidDeleteFiles):连带目录里各层的注释一起删。在 VS Code 外面(终端、git)改名删除不跟。 - 外部改了
code-notes.json(git pull、手改):文件监听重读,树上跟着变。 - 刷新:只重画注释变了的那几行(文件树按 Uri 记住交给 VS Code 的节点对象,VS Code 按对象认行);还没展开过的行不用管。
- 注释和代码阅读的解析、翻译无关,单独放在
notes/;文件树只留一个「给行添信息」的接口(FileTreeDecorator),由index.ts接上。
编辑器右上角切换
- 受支持语言的文件:普通编辑器里标题栏显示「预览」,预览里显示「编辑」。点一下在同一个标签页位置换成另一种:用 VS Code 内部的
reopenActiveEditorWith(「重新打开编辑器的方式」背后的机制)原位替换。不能用「另开再关原标签」:文件有未保存改动时关原标签会弹保存框,选「不保存」会丢改动。 - 用
editor/title的navigation组、不带图标的命令,让按钮显示文字。若实测 VS Code 不渲染文字,改成带图标($(open-preview)/$(go-to-file)),标题保持「预览」「编辑」。VS Code 标题栏做不出原型里的分段控件,这是和原型唯一的有意差异。 - 受支持语言 = 当前注册的语言插件覆盖的
languageId,通过setContext('superpowers.codeReader.languageIds', [...])提供给 when 子句(resourceLangId in superpowers.codeReader.languageIds)。
预览(代码块树)
- 每一句是一块:导入、函数、类、类型、字段、常量、变量、条件、否则、循环、尝试、捕获、最终、返回、语句、注释。
- 嵌套的块放在父块下面、往右缩进;每一层左边一条竖线(颜色
--vscode-editorIndentGuide-background)。 - 块的组成(从左到右):折叠箭头(只有有子块的块才显示,其余占位对齐)、种类标签(字取翻译插件给
kind值起的名字,见「筛选」)、内容、右侧切换按钮。 - 种类标签和底色按种类区分(颜色见下表);注释块虚线框。
- 内容两种状态,每块单独切:
- 预览:函数、类、导入、常量、变量块是属性表(见下节),注释块是 Markdown 说明(见「注释的 Markdown 排版」),其余块是一行译文;按词着色,等宽字体(中文部分用系统中文字体回退)。
- 编辑:这一块的源码(按语法树着色),可以直接改;蓝色边框。按钮文字「预览」;Esc 也回到预览。
- 复合语句(函数、条件、循环……)块里只放它自己的头部那几行(主体交给子块);收尾括号、空行不成块。函数、类的说明(JSDoc / docstring)也算在函数、类块的头部里。
- 折叠:函数、类块打开时默认收起子块(属性表照常显示),其余块默认展开;用户点过折叠箭头的块按他点的,记在视图状态里(见「预览数据与消息」)。
- 顶部筛选栏:见「筛选」;翻译下拉框(多于一种翻译时才显示)也在栏里。
- 解析失败:显示错误原因,并提示用「编辑」查看源码。
筛选
预览顶部钉住一条筛选栏,按块的属性藏掉一部分块(原型画板「筛选 · 默认 / 按访问级别 / 按种类」)。
- 三层分工,webview 不认识任何语言:
- 语言插件给每块报和语言无关的属性「属性 = 值」,都是稳定的英文 id,不放显示文字。通用词表由契约定(
COMMON_ATTRIBUTES):kind(块的种类)、access(public/private/protected,语言可以加自己的值)、async、abstract、typeOnly('true'/'false')。通用属性由宿主从块的 kind 和 facts 推出来(FunctionFacts.access/async、ClassFacts.access/abstract、ImportFacts.typeOnly、VariableFacts.access),只有这一条路,语言插件不用另报;facts 里没填的(语言没有这个概念,例如 JavaScript 没有抽象类、仅类型导入)块就没有这个属性,不是填「否」。语言自己的属性(Rust 的unsafe = 'true')、facts 表达不了的通用属性值(Kotlin 的access = 'internal')填在块的attributes里,和推出来的合在一起,同一属性以它为准;kind总是块的种类。 - 翻译插件给属性和值起名(
Translator.attributeNames:access→ 访问级别、public→ 公共、kind的function→ 函数、布尔值 → 是 / 否),和属性表里的属性名、值是同一套叫法(中文直译里属性表的「访问级别」「异步」「抽象」「仅类型」行和筛选共用一份表);种类标签上的字也取它。没起名的显示 id。 - webview 只按属性 id / 值 id 筛,显示翻译插件给的名字。
- 语言插件给每块报和语言无关的属性「属性 = 值」,都是稳定的英文 id,不放显示文字。通用词表由契约定(
- 下拉框:这份文件里有两种以上值的属性各一个(只有一种值的不给——筛它等于按种类筛);只有一种值、但这个值正被去掉的也给,不然藏起来的块在栏上找不到原因。排列:通用属性按契约的顺序(种类、访问级别、异步、抽象、仅类型),语言自己的属性按在文件里第一次出现的先后排在后面。下拉菜单是勾选列表,列出这份文件里出现的值(按第一次出现的先后)和各有几块(所有层的块都算,例如「私有 3 / 公共 3」)。
- 规则:同一属性里勾着的值满足一个就显示,不同属性要同时满足;块没有某个属性就不受那个属性的筛选影响(条件、返回块没有访问级别,按访问级别筛时照常显示);块被筛掉,连同它的子块一起藏,不留空位,层级竖线跟着变短;子块都被筛掉的块不给折叠箭头。
- 汇总:什么都没筛掉时右边写「共 N 块」(N 是整份文件所有层的块数);筛掉了写「已隐藏 N 块 · 清除」,N 只数直接被筛掉的块,跟着藏起来的子块不算;点「清除」整份筛选清空。页面上一块都不剩时写「所有块都被筛选隐藏了。」
- 下拉框上的字:没去掉值时是属性名;去掉了(边框换
--vscode-focusBorder)勾着一种写「访问级别:公共」,几种写「种类:已选 11 种」,一种都不勾写「种类:未选」。 - 记住:记的是每个属性去掉的值(
{ 属性 id: 值 id[] }),所以文件里第一次出现的值默认显示。整个工作区共用一份(不按文件),存在扩展端的workspaceState(superpowers.codeReader.filter),重新加载窗口后还在;一个预览改了,所有开着的预览跟着变。某个属性在这份文件里没出现就没有它的下拉框,记着的去掉的值照样对它出现的文件起作用。 - 跳到定义到被筛掉的块:临时显示目标和通向它的路径(被筛掉的祖先块下面只露出这条路径,目标自己的子块照常按筛选显示,函数照样能展开看主体),本来被筛掉的块右边带小提示「已被筛选隐藏,临时显示」,照常展开、滚过去、闪。筛选一变,或文档刷新(正在编辑的块和它的祖先除外,不能打着字就消失)就回到筛选状态。
- 折叠、跳到定义、属性行 id、滚动位置恢复照旧;被筛掉的块不在页面上,按行滚动(从文本编辑器切过来)落到它前面最近的一块。视口顶部按筛选栏下沿算:记位置、恢复、滚过去都让开筛选栏。
- 样子:筛选栏钉在预览顶部(
position: sticky,块从它下面滚过去),横贯编辑区、下边一条--vscode-panel-border、底色--vscode-editor-background,里面的内容和代码块同宽对齐:「≡ 筛选」(宽 52px)、每个属性一个下拉框(宽 150px、高 24px,--vscode-dropdown-background/--vscode-dropdown-border/--vscode-dropdown-foreground)、右边汇总(descriptionForeground,「清除」--vscode-textLink-foreground)、再右边翻译下拉框;放不下就换行。下拉菜单盖在代码块上面:宽 200px,--vscode-editorWidget-background、--vscode-editorWidget-border、--vscode-widget-shadow阴影;每项勾选框(勾着是--vscode-focusBorder底)、值、右边块数(--vscode-editorLineNumber-foreground);悬停、键盘高亮--vscode-list-hoverBackground。 - 键盘和读屏:下拉框按钮
aria-haspopup="listbox"、aria-expanded;菜单role="listbox"、aria-multiselectable,每项role="option"、aria-selected表示勾着。按钮上 ↓ / 回车 / 空格打开(↑ 打开并停在最后一项),列表里 ↑ ↓ Home End 移动,空格 / 回车勾选,Esc 关掉并回到按钮,Tab、点外面关掉。
属性表(函数、类、导入、常量、变量块)
卡片外观(折叠箭头、种类标签、底色、悬停高亮、「编辑 / 预览」切换)和其他块一样,只是内容换成一张属性表:左列属性名(界面字体、descriptionForeground 色,宽 76px),右列是值(等宽字体,按词性着色,长了换行),两列间距 12px,行高 20px。「输入参数」「导入项」和值是字典 / 对象字面量的「值」「初始值」下面展开子行,属性名前带折叠箭头(图标、大小、颜色同块的折叠箭头;点箭头或属性名切换,默认展开,折叠后值「N 个」「字典 N 项」照常显示,只收起子行);子行往右缩进 5px,左边一条 1px 竖线(editorIndentGuide-background1)。输入参数的子行分名称 / 类型 / 默认值三列对齐,前两列宽 150px / 220px,编辑区窄时按 28% / 38% 收,默认值列占剩下的;字典 / 对象的子行分键 / 值两列,键列同名称列。子行下面还能再展开一层(嵌套的字典 / 对象):带下一层的子行第一列前面有折叠箭头(点箭头或第一列切换,默认展开),同一层里其余子行在同样的位置留空、第一列对齐;下一层同样往右缩进 5px、左边一条竖线,竖线对着上一层的箭头,每层自己的两列对齐。同一层里没有带下一层的子行时不留箭头的位置(和参数、导入项的子行一样)。
| 种类 | 行 |
|---|---|
| 函数 | 名称(函数色;匿名函数写「匿名」)/ 说明(有说明才显示:JSDoc 去掉 /**、*/ 和每行开头的 *,docstring 去掉引号;再去掉共同缩进和首尾空行,@param 这类标签行照抄;得到的原文按 Markdown 渲染,见「注释的 Markdown 排版」;里面的名字不能点)/ 访问级别 / 异步(是 / 否)/ 装饰器(有才显示,源码原样照抄、按源码着色,多个换行)/ 输入参数(N 个 或「无」,每个参数一行:名称 | 类型(没标注写「未标注」)| 默认值(默认 + 译文,没有留空))/ 输出参数(返回类型的译文,没标注写「未标注」) |
| 类 | 名称(类型色;匿名类 export default class {} 写「匿名」;赋给变量、类字段的类表达式写变量名、字段名)/ 说明(有说明才显示,规则同函数)/ 访问级别 / 抽象(是 / 否,只有 TypeScript / TSX 有,看 abstract class,TypeScript 的类都有这一行;JavaScript / JSX 没有)/ 装饰器(同函数)/ 类型参数(有才显示:TypeScript <K, T extends X = Y> 每个参数的译文,逗号隔开,如 K, T 限定为 X = Y;Python 3.12 的 class G[T] 同样)/ 父类(有才显示:TypeScript extends 后面的表达式连着泛型实参的译文,如 Base<字符串>;Python 类名后面括号里的参数列表照写的顺序逐项翻,关键字参数、*bases 也在这一行,如 Base, metaclass=ABCMeta;逗号隔开)/ 实现的接口(有才显示:TypeScript implements 后面每个类型的译文,逗号隔开) |
| 导入 | 来源(TypeScript 是字符串字面量原样,如 'node:fs',字符串色;Python 是点分模块名,如 fastapi、.models,类型色)/ 仅类型(是 / 否,只有 TypeScript / TSX 有,看 import type;JavaScript / JSX 没有)/ 导入项(N 个 或「无」,每项一行:name、name 作为 alias;默认导入写「默认导出 作为 x」,* as x 写「全部 作为 x」,Python from m import * 写「全部」,Python import a.b as c 写「整个模块 作为 c」,一条 import a, b 导入多个模块时写模块名) |
| 常量 / 变量 | 名称(解构声明写整个模式,如 { p, q })/ 访问级别 / 类型(写了标注才有)/ 值(常量)或初始值(变量):初始化表达式的译文,没有写「无」;是字典 / 对象字面量时见下面「字典 / 对象字面量」。一条语句声明几个变量(let a = 1, b = 2)就重复几组 |
访问级别(公共 / 私有 / 受保护):
- TypeScript 模块里的声明(函数、类、变量):
export了是公共(含export default),顶层没export是私有;嵌套在函数体、条件等语句块里的没有这一行。 - TypeScript 类成员(方法、函数值字段、类表达式字段):写了
public/private/protected按写的,#name是私有,都没写是公共。 - Python:只有模块顶层和类主体里的声明(函数、类、变量)有;名字单下划线开头(
_x、__x)是私有,前后双下划线(__init__)是公共,其余公共;解构赋值里的名字全是私有才算私有;赋值目标是属性、下标(self.x = …)时没有这一行。
值里的表达式只翻落在块头部里的那一段:const v = useMemo(() => { … }) 这种值带语句块主体回调的块,回调主体是子块,值只翻到 useMemo(() =>,和块头部一致。
字典 / 对象字面量(常量、变量的值,含 Python 类主体里不带注解的类属性;参数默认值照旧是一行译文):
- 值写项数:Python
字典 N 项、TypeScript对象 N 项(按 hint 着色),各项展开成子行,照源码顺序,注释不算一项。 - 每项两列:键 | 值。键照源码写法:字符串键带引号、按字符串着色,名字键按名字着色,计算属性名
[k]照常翻;值照常直译(等待 …、空数组……)。 - 值又是字典 / 对象字面量时,它这一项的值也写项数,它的各项展开成下一层子行,一层层往下套。
- 展开:Python
**other、TypeScript...other是一项,键写「展开」(hint),值是展开的表达式。 - TypeScript 简写
{ a }:键a,值a。对象里的方法、getter、setter:键是方法名(函数色),值是方法头部(到主体开始之前)的译文,如异步 load(id: 字符串): Promise<无>、get size()。 - 空字面量不展开:值是一行
空字典/空对象(一行译文里也这样翻,同空数组)。 as const、satisfies包着的对象({ … } as const)不算字面量本身,照旧一行译文。
注释的 Markdown 排版
- 用在:注释块(
#、//、/* */、不挂在函数、类上的 JSDoc、Python 模块开头的 docstring)的预览,函数、类属性表的「说明」。视图数据里它们是{ markdown }(见「通用契约」的Markdown),webview 按数据决定怎么渲染,不猜。 - 原文:翻译插件去掉注释符号(
#、//、/**/和每行开头的*)和引号,再去掉行尾空白、首尾空行和共同缩进,保留换行,不做别的转换。一个注释节点(块注释、docstring)的第一行紧跟在注释符号 / 引号后面,不参与算共同缩进(同 PEP 257);几条行注释连成的一块每行地位相同,第一行也算,缩进四格的代码块才留得住。 - 渲染:webview 用 markdown-it(
html: false、linkify: true),CommonMark 加 GFM 的表格、删除线:段落、强调、行内代码(含双反引号)、代码块(围栏、缩进四格)、列表、链接、标题、引用、表格。段内单个换行是软换行,空行分段。同一段文字只渲染一次(缓存最近 500 段)。 - 安全:源码里的 HTML 当文字显示;链接只认 http / https / mailto,带协议的网址和邮箱自动成链接(
sample.py这种不带协议的不算),别的写法(javascript:、command:、相对路径)照原文显示。点链接由 VS Code 的 webview 自己拦下、交给宿主在外部打开,不用另发消息。图片不渲染(webview 的 CSP 不放行外部图片),显示成!加链接。 - 样子:正文用界面字体(
--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 里的名字不能点,不参与跳到定义。
跳到定义
- 能点的:预览里(属性表的名称、类型、来源、导入项、默认值、值、字典 / 对象各层子行的键和值、类型参数、父类、实现的接口,以及一行译文)照抄自源码的名字——标识符、属性名、类型名、点分模块名的每一节(
luffy_agent.core.db三节各自能点,和 VS Code 一样)、字符串(TypeScript 的模块路径'./x'就是字符串)。翻出来的关键字、运算符、说明词不能点。 - 链接和编辑器里一样:只有按住修饰键(macOS ⌘,其余平台 Ctrl)、鼠标停在查得到定义的名字上时,名字加下划线、换成链接色(
--vscode-editorLink-activeForeground)、光标变手形;松开修饰键或移开就恢复。有没有定义问扩展端(vscode.executeDefinitionProvider,文档 uri + 名字在源码里的位置);和编辑器一样,每回指到一个名字上(或指着名字按下修饰键、换了文档版本)都现问,回答不留到下次用——Pylance、tsserver 刚启动那几秒会答「没有」;只认最近一次询问的回答,迟到的丢掉。 - ⌘ / Ctrl + 点击:
- 一处定义,在当前文件:原地滚过去;
- 一处定义,另一个文件,语言有代码阅读:在同一个编辑器组用预览打开(
vscode.openWith),滚过去;这个文件的预览已经在这一组开着就切过去、发给它,不重新打开; - 一处定义,语言没有代码阅读:在同一个编辑器组用普通编辑器打开,选中定义处的名字;
- 几处定义:先弹挑选框(每项「相对路径:行号」,下面一行是那行源码),挑一处再按上面跳;
- 没有定义:不动。
- 和普通编辑器里 ⌘ + 点击一样:目标是否按「预览标签」打开听
workbench.editor.enablePreview;workbench.editor.enablePreviewFromCodeNavigation为假(默认)时先把当前预览标签固定住,免得被目标替掉。
- 闪哪一处:页面上就是这处定义的最具体的那一处,只按源码位置判断(定义提供者给的定义处下标,对上块和属性行带的源码范围),不看属性名文字:
- 定义是块自己的名字(函数名、类名;只声明一个变量时的变量名,解构模式整个算)→ 整块。块的这段范围由扩展端从 facts 取(
BlockView.defines),和翻译插件无关。 - 定义是属性表里某一行 / 子行讲的东西(参数、导入项、一条语句声明几个变量时的某一个、类的泛型参数、对象字面量一项的键——TypeScript 对象的属性名就是定义,嵌套几层都算)→ 只闪这一行 / 子行。范围是翻译插件从 facts 取的节点(
PropertyRow.defines、PropertyItem.defines;字典 / 对象的项标的是键,展开项不标),定义处落在范围里就算(*args的定义在args上、解构参数里的名字都对得上)。 - 其余(定义在一行译文里,例如循环变量;翻译插件没填
defines)→ 显示着定义处那个名字的块,名字在属性行的子行里时展开那一行(在嵌套的子行里时连它上面各层一起展开);页面上找不到这个名字就取包含定义那一行的块。
- 同一文件原地跳、跳进另一个文件的预览,规则一样。
- 定义是块自己的名字(函数名、类名;只声明一个变量时的变量名,解构模式整个算)→ 整块。块的这段范围由扩展端从 facts 取(
- 滚过去:目标所在块的祖先块折叠着就展开,目标在折叠的属性行子行里(参数、导入项、字典 / 对象的项)就展开那一行,在嵌套的子行里就连它上面折叠着的各层子行一起展开。目标已在视口里不滚,否则滚到视口正中(比视口高就顶部对齐)。然后目标的边框和底色往聚焦色(
--vscode-focusBorder)闪三下,共约 1.5 秒后复原;同一处再跳一次从头闪;系统开了「减少动态效果」时改成静止高亮 1.5 秒。目标是属性行 / 子行但块正在编辑(页面上没有属性表)时闪整块。 - 不按修饰键的点击照旧;⌘ / Ctrl + 点击名字不会触发块的编辑切换、折叠。
通用契约
放在 vscode/src/codeReader/contract.ts,语法树类型用 @vscode/tree-sitter-wasm 自带的 web-tree-sitter 类型。
export type BlockKind =
| 'import' | 'function' | 'class' | 'type' | 'field' | 'const' | 'variable'
| 'if' | 'else' | 'loop' | 'try' | 'catch' | 'finally' | 'return' | 'statement' | 'comment'
/** 源码里一段的下标(源码字符串下标),end 为开区间。 */
export interface SourceRange { startIndex: number, endIndex: number }
/** 源码里的一段,带起止行号(从 0 开始)。 */
export interface SourceSpan extends SourceRange { startRow: number, endRow: number }
// ── 属性表块的结构化信息(facts):语言插件从语法树读出,节点都指向源码 ──
export type Access = 'public' | 'private' | 'protected'
export interface ParameterFacts {
name: Node // 解构参数是整个模式,...rest / *args / **kwargs 带前缀
optional: boolean // TypeScript 的 x?
type?: Node
default?: Node
}
export interface FunctionFacts {
name?: Node // 匿名函数没有
doc?: Node // 函数的说明:紧贴在前面的 JSDoc 注释节点(TS)、主体开头的 docstring 字符串节点(Python)
access?: Access // 嵌套在语句块里的函数没有
async?: boolean // 没有这个概念的语言不填
decorators: Node[]
parameters: ParameterFacts[]
returnType?: Node
}
/** 父类的一项:TS `extends Base<T>` 的 Base(表达式)和它后面的泛型实参 <T>;Python 类参数列表的一项(含 metaclass=…、*bases)。 */
export interface BaseFacts { value: Node, typeArguments?: Node }
export interface ClassFacts {
name?: Node // 匿名类没有;赋给变量、类字段的类表达式取变量名、字段名
doc?: Node // 同 FunctionFacts.doc
access?: Access // 嵌套在语句块里的类没有
abstract?: boolean // 只有 TypeScript 填(JavaScript 不填)
decorators: Node[]
typeParameters: Node[] // 每个泛型参数一项
bases: BaseFacts[]
implements: Node[] // TypeScript 的 implements,每个接口一项
}
export type ImportItem =
| { kind: 'named', name: Node, alias?: Node } // import { a as b }、from m import a as b
| { kind: 'default', alias: Node } // import x from 'm'
| { kind: 'namespace', alias?: Node } // import * as x、from m import *
| { kind: 'module', module: Node, alias?: Node } // import a.b as c、import x = require('m')
export interface ImportFacts {
sources: Node[] // 来源模块的写法;Python 的 import a, b 有多个
typeOnly?: boolean // 只有 TypeScript 填(JavaScript 不填)
items: ImportItem[]
}
/** 字典 / 对象字面量里的一项,照源码顺序;注释不算。 */
export type EntryFacts =
| { kind: 'pair', key: Node, value: Node, entries?: EntryFacts[] } // 'a': 1、a: 1、[k]: v;TS 简写 { a } 键和值是同一个节点;entries 同 DeclaratorFacts.entries
| { kind: 'spread', value: Node } // **other、...other:value 是展开的表达式
| { kind: 'method', name: Node, node: Node, header: SourceSpan } // 对象里的方法、getter、setter:header 同 BlockShape.header,只到主体开始之前
export interface DeclaratorFacts {
name: Node
type?: Node
value?: Node
entries?: EntryFacts[] // 值是字典 / 对象字面量(Python dictionary、TS object)时是它的各项,空字面量是空数组;别的值没有
}
export interface VariableFacts {
access?: Access
declarators: DeclaratorFacts[]
}
/**
* 筛选属性的通用词表,筛选栏按这个顺序排:kind(BlockKind)、access(public / private / protected,语言可加值)、
* async / abstract / typeOnly('true' / 'false')。宿主从 kind 和 facts 推出,facts 里没填的块就没有这个属性。
*/
export const COMMON_ATTRIBUTES = ['kind', 'access', 'async', 'abstract', 'typeOnly'] as const
export type CommonAttribute = (typeof COMMON_ATTRIBUTES)[number]
/** 块的筛选属性:属性 id → 值 id,都是稳定的英文 id,不放显示文字。 */
export type BlockAttributes = Record<string, string>
export interface BlockShape {
/** 这一块对应的语法树节点(整条语句)。 */
node: Node
/** 块自己显示 / 编辑的那一段:复合语句只到主体开始之前,其余是整条语句;函数、类的说明(doc)也算在里面。 */
header: SourceSpan
children: CodeBlock[]
/** 语言自己报的筛选属性:通用词表以外的(Rust unsafe: 'true'),或 facts 表达不了的通用属性值(Kotlin access: 'internal',盖过推出来的);kind 填了不算。 */
attributes?: BlockAttributes
}
/** 预览成属性表的块:种类和 facts 绑在一起,语言插件漏给 facts 编译不过。 */
export interface FunctionBlock extends BlockShape { kind: 'function', facts: FunctionFacts }
export interface ClassBlock extends BlockShape { kind: 'class', facts: ClassFacts }
export interface ImportBlock extends BlockShape { kind: 'import', facts: ImportFacts }
export interface VariableBlock extends BlockShape { kind: 'const' | 'variable', facts: VariableFacts }
export type FactBlock = FunctionBlock | ClassBlock | ImportBlock | VariableBlock
/** 带说明(facts.doc)的块。 */
export type DocumentedBlock = FunctionBlock | ClassBlock
export type TextKind = Exclude<BlockKind, FactBlock['kind']>
/** 预览成一行译文的块(注释块是 Markdown 说明)。 */
export interface TextBlock extends BlockShape { kind: TextKind }
export type CodeBlock = TextBlock | FactBlock
/** 着色用的词性,源码着色和译文着色共用。 */
export type TokenRole =
| 'keyword' // 流程关键字:import from export if else for return try catch await …
| 'storage' // 声明关键字与字面量关键字:const function async class interface new true false null …
| 'operator' // 被翻成文字的运算符:全等 并且 或者 非
| 'hint' // 属性表里的说明词:未标注 无 N 个 默认导出 …
| 'variable' | 'function' | 'type' | 'string' | 'number' | 'literal' | 'comment' | 'plain'
/**
* offset:这段照抄自源码里的名字(标识符、属性名、类型名、点分模块名的一节)或字符串时,它在源码里的起始下标,
* 预览拿它向 VS Code 查定义、跳过去;译词、提示词没有。由从语法树叶子生成这段的一方填(模板直译引擎的译文和属性表);
* 第三方翻译插件可以不填,不填就不能跳。
*/
export interface Segment { text: string, role: TokenRole, offset?: number }
/**
* 一段 Markdown 说明文字:注释、JSDoc、docstring 去掉注释符号、引号和共同缩进后的原文,不做别的转换。
* 预览按 Markdown 渲染成注释色的段落、列表、代码块(见「注释的 Markdown 排版」);第三方翻译插件给注释也填它。
*/
export interface Markdown { markdown: string }
/** 预览里显示的一段文字:着色的词(一行译文、属性值),或整段 Markdown 说明(注释块、函数和类的说明)。 */
export type PreviewText = Segment[] | Markdown
/**
* 属性表里的一行或一个子行讲的是哪个定义:定义出来的那个名字的节点(取自 facts)在源码里的范围——
* 参数名(连 `*` 前缀、整个解构模式)、导入项(名称到别名)、声明的变量名、类的泛型参数(整串)、字典 / 对象一项的键。跳到的定义落在里面就只闪这一行;
* 第三方翻译插件可以不填,不填就闪整块。
*/
export interface Defines { defines?: SourceRange }
/** 属性表里展开的一个子行:几列对齐的格子;items 是展开在它下面的下一层子行(嵌套的字典 / 对象),一层层可以一直往下套。 */
export interface PropertyItem extends Defines { cells: Segment[][], items?: PropertyItem[] }
/** 属性表的一行:属性名、着色的值或 Markdown 说明(函数、类的「说明」)、展开在下面的子行(输入参数、导入项、字典 / 对象的各项)。 */
export interface PropertyRow extends Defines { label: string, value: PreviewText, items?: PropertyItem[] }
/** 语言插件:语法树 → 代码块树。 */
export interface LanguageAdapter {
id: string
/** 覆盖的 VS Code languageId,例如 ['typescript', 'typescriptreact', 'javascript', 'javascriptreact']。 */
languageIds: string[]
/** 该 languageId 用的语法包 .wasm 的绝对路径(宿主负责加载,保证和解析器运行时是同一份)。 */
grammarPath: (languageId: string) => string
/** languageId:一个插件覆盖几种方言时按它区分(JavaScript 没有 TypeScript 的抽象类、仅类型导入)。 */
toBlocks: (root: Node, source: string, languageId: string) => CodeBlock[]
/** 源码着色:给语法树叶子节点定词性。 */
tokenRole: (leaf: Node) => TokenRole
}
/** 筛选属性和值的显示名;没给名字的显示 id。 */
export interface AttributeNames {
attributes: Record<string, string> // access → 访问级别
values: Record<string, Record<string, string>> // access → public → 公共、async → true → 是、kind → function → 函数
}
/** 翻译插件:代码块 → 一行译文或属性表。 */
export interface Translator {
id: string // 'typescript.zh-template'
label: string // '中文直译'
languageIds: string[]
translate: (block: TextBlock, source: string) => PreviewText // 只翻 header 那一段;注释块给 Markdown 原文
describe: (block: FactBlock, source: string) => PropertyRow[] // 函数、类、导入、常量、变量块 → 属性表
attributeNames?: AttributeNames // 筛选属性和值叫什么,和属性表的叫法一致;筛选栏、种类标签用它
}
export interface CodeReaderApi {
registerLanguage: (adapter: LanguageAdapter) => Disposable
registerTranslator: (translator: Translator) => Disposable
}
activate()返回{ codeReader: CodeReaderApi };其他扩展声明extensionDependencies后用extensions.getExtension('clurdra.superpowers-vscode-clurdra')!.exports.codeReader注册。- 内置的 TypeScript、Python 语言插件和两个中文直译翻译插件在
activate()里通过同一个 API 注册,没有旁路。 - 注册表变化时更新
superpowers.codeReader.languageIds上下文,并让已打开的预览刷新。 - 分工:语言插件只从语法树读出和语言无关的事实(facts,引用源码节点,不做正则匹配文字);翻译插件决定属性名、枚举词(公共 / 是 / 默认导出)和值怎么译,也给筛选属性和值起名(
attributeNames)。筛选属性的通用部分由宿主从 facts 推出(attributes.ts的blockAttributes),语言插件只在BlockShape.attributes里补自己语言特有的。第三方语言插件给函数、类、导入、常量、变量块填 facts 就能用上任何翻译插件的属性表;自己语言的字典 / 映射字面量填进DeclaratorFacts.entries就能展开成子行(中文模板直译按字面量的节点类型查叫法,见ZhTable.mapLiterals)。
解析运行时
- 依赖
@vscode/tree-sitter-wasm(VS Code 团队维护,包含 web-tree-sitter 运行时和 TypeScript / TSX / JavaScript / Python 语法包,版本配套)。 - 打包:
esbuild.mjs构建时把tree-sitter.wasm和用到的语法包复制到dist/tree-sitter/(package.json的files已包含dist/**/*)。 - 运行时初始化一次
Parser.init({ locateFile })指向dist/tree-sitter/tree-sitter.wasm;语法包按路径懒加载并缓存。 - 文件扩展名到语法包:
.ts→ typescript;.tsx→ tsx;.js/.jsx/.mjs/.cjs→ javascript(tree-sitter-javascript 支持 JSX);.py→ python。以languageId为准。
代码块树规则
TypeScript / JavaScript
- 顶层与语句块内的每条语句一块;连续的行注释合成一块注释。
- JSDoc(以
/**开头的块注释,/**/不算)和下一条语句之间只有空白(空行也算,中间隔着装饰器也算)、那条语句是函数或类块时,它是这个函数、类的说明:进facts.doc,头部往上延到注释开头,不单独成块。其余情况(后面不是函数、类块,中间隔着别的注释,在上一条语句的行尾)照旧是注释块。//行注释和普通/* */块注释不变。 import_statement→ import。export_statement包着声明 → 按声明定种类,节点取整个export_statement(头部包含export)。function_declaration/generator_function_declaration/method_definition→ function,子块来自主体语句。- 变量声明的唯一声明值是带语句块主体的箭头函数 / 函数表达式 → function(React 组件、回调常见写法),子块来自主体;是类表达式(
const A = class {})→ class。 lexical_declaration/variable_declaration:模块顶层的const→ const,其余 → variable。class_declaration/abstract_class_declaration、export default class {}→ class,子块:方法 → function,字段 → field(值是带语句块主体的函数 → function,值是类表达式 → class),注释 → comment。interface_declaration→ type,子块:属性 / 方法签名 → field;type_alias_declaration→ type;enum_declaration→ type,成员 → field。if_statement→ if,子块来自 consequence;else/else if展平成紧跟其后的兄弟块 else(else if的头部是else if (...),子块来自它的 consequence)。for/for…in/for…of/while/do→ loop。try_statement→ try;catch_clause→ 兄弟块 catch;finally_clause→ 兄弟块 finally。switch_statement→ if,每个case/default→ 子块 if。return_statement→ return;其余语句(含throw)→ statement。- 表达式语句的参数里带语句块主体的函数(
useEffect(() => { … })、describe('x', () => { … }))→ statement,子块来自那个函数主体,头部到主体开始为止。
Python
import_statement/import_from_statement/future_import_statement→ import。function_definition→ function;decorated_definition按内部定义定种类,头部包含装饰器。class_definition→ class;类体里的带注解赋值 → field。- 模块开头的字符串表达式(docstring)和
comment→ comment。 - 函数、类主体的第一条语句(注释不算)是单独一个字符串表达式时,它是这个函数、类的 docstring:进
facts.doc,头部往下延到字符串结尾(它和冒号之间的注释也在头部里),子块从它后面开始,不单独成块。 - 赋值:模块顶层且变量名全大写 → const,其余 → variable。
if_statement→ if;elif_clause/else_clause→ 兄弟块 else;for/while→ loop(循环的else→ 兄弟块 else)。try_statement→ try;except_clause→ catch;finally_clause→ finally。with_statement→ statement,子块来自主体;match_statement→ if,case→ 子块 if。return_statement→ return;其余 → statement。- 复合语句头部到主体开始为止(包含行尾冒号,翻译时去掉)。
属性表信息(facts)从哪来
- TypeScript 函数:
function_declaration/generator_function_declaration/function_signature、类里的method_definition/ 方法签名、值是带语句块主体函数的变量和类字段、export default的函数值。名称取声明名 / 变量名 / 字段名;说明取紧贴在前面的 JSDoc 注释节点(见上面代码块树规则);参数来自formal_parameters(required_parameter/optional_parameter的 pattern、type、value,JavaScript 的assignment_pattern),不带括号的单参数箭头函数取parameter;返回类型取return_type里的类型;异步看有没有async;类成员的装饰器是类主体里排在它前面的decorator兄弟节点。 - TypeScript 类:
class_declaration/abstract_class_declaration、类表达式class(export default class {}、变量的值、类字段的值)。名称取类名 / 变量名 / 字段名;说明同函数;访问级别同函数(类字段按类成员);抽象看节点类型是不是abstract_class_declaration(只有 languageId 是 typescript / typescriptreact 时填,JavaScript 没有这个概念,不填);装饰器是export_statement和类声明自己的decorator子节点(@d export class、export @d class、@d class都算),类字段的值是类表达式时还有类主体里排在它前面的decorator;泛型参数是type_parameters里每个type_parameter;父类、实现的接口在class_heritage里:extends_clause的每个 value 一项,跟在它后面的type_arguments归它,implements_clause里每个类型一项;JavaScript 的class_heritage直接是extends后面的表达式。 - TypeScript 导入:
import_statement的source(import x = require()取import_require_clause里的);import_clause里的标识符是默认导入,namespace_import是* as,named_imports里每个import_specifier的 name / alias;语句本身带type是仅类型(同抽象,只有 TypeScript 填)。 - TypeScript 变量:每个
variable_declarator的 name / type / value;value 是object时 entries 是它的具名子节点(注释除外):pair的 key / value(value 又是object时连它的 entries)、shorthand_property_identifier(键和值都是它)、spread_element里的表达式、method_definition的 name 和头部(到 body 之前)。 - Python 函数:
function_definition的 name / parameters / return_type,async子节点;说明是主体开头 docstring 的字符串节点(string/concatenated_string);装饰器是decorated_definition里的decorator;参数identifier、typed_parameter(名字是第一个具名子节点)、default_parameter、typed_default_parameter、*args/**kwargs,*和/分隔符不算参数。 - Python 类:
class_definition的 name;说明是主体开头 docstring 的字符串节点;装饰器同函数;泛型参数是 Python 3.12type_parameters里的每一项;父类是superclasses(argument_list)里的每个具名子节点,照写的顺序,关键字参数(metaclass=ABCMeta)、*bases、**kw也算;没有抽象、实现的接口。 - Python 导入:
import_statement每个 name 一个来源、导入项是整个模块;import_from_statement的 module_name 和各 name(aliased_import拆出 alias),wildcard_import是全部;future_import_statement的来源是__future__。 - Python 变量:
assignment的 left / type / right;right 是dictionary时 entries 是它的具名子节点(注释除外):pair的 key / value(value 又是dictionary时连它的 entries)、dictionary_splat里的表达式。
中文模板直译
结构照抄源码(按节点模板重排的语法结构除外,见下文),只替换三类东西,其余(标识符、属性名、字面量、标点)原样保留并按词性着色:
- 关键字:只认语法树里的关键字节点(匿名节点),所以属性名
i.number、对象键不会被误翻。 - 内置类型:只在类型位置翻(TS 的
predefined_type、Python 注解里的内置类型名)。 - 运算符:
===全等、!==不全等、==等于、!=不等于、&&并且、||或者、!非(Python 的not/and/or本身是关键字)。
同一套表和节点模板也用来翻属性表里的值:引擎能翻任意一个节点(只取它落在块头部里的那一段),参数类型、默认值、返回类型、变量类型和初始值、字典 / 对象各项的键和值都走这条路。
其他规则:空数组字面量 → 空数组、空对象字面量 → 空对象(Python 空列表 → 空列表、空字典 → 空字典);复合语句头部去掉结尾的 { 或 :;多行合成一行(连续空白压成一个空格,右括号前的尾逗号去掉);注释块给去掉注释符号的 Markdown 原文(见「注释的 Markdown 排版」)。关键字与前后内容之间保留源码里的空格(! 翻成「非」后补一个空格)。
关键字按所在语法节点选择译法:同一个关键字在不同语法结构里意思不同时,表里写一个默认译法,再加若干「语境 → 译法」。语境是从关键字节点往上的节点类型链,外层在前:raise_statement 指父节点是 raise_statement,for_statement > else_clause 指父节点是 else_clause、祖父节点是 for_statement(关键字包在一层子句节点里时要看到祖父节点)。几条语境同时匹配时取最长的,都不匹配用默认译法。节点类型以 tree-sitter 语法为准。下面两张表里写成「(语境)译法 / (语境)译法」的关键字就是按这条规则区分的,排第一个的是默认译法。
节点模板:照抄源码结构读不通的语法结构,按语法节点类型给一个模板,把译文重排成中文语序。模板是一串项,每项要么是插入的译词,要么是子节点槽位:槽位按 tree-sitter 字段名引用子节点;语法没给字段名的,按具名子节点序号(0 起,注释不占序号)引用。命中模板的节点只输出模板里的项,节点自己的关键字和标点(if / else、? / :)不再输出;插入的译词一律按关键字(keyword)着色,模板各项之间空一格;槽位里的子节点照常递归翻译,里面的关键字、运算符、嵌套的同类结构照样处理。两种情况不重排、照常翻:模板引用的子节点不存在;节点没整个落在要翻的这一段里。目前的模板:
| 语言 | 节点类型 | 模板 |
|---|---|---|
| TypeScript | ternary_expression(c ? a : b) |
如果 〈condition〉 则 〈consequence〉 否则 〈alternative〉 |
| Python | conditional_expression(a if c else b,没有字段名,具名子节点依次是 a、c、b) |
如果 〈1 号〉 则 〈0 号〉 否则 〈2 号〉 |
TypeScript 关键字表(流程 = keyword,声明 = storage):
| 流程 | 声明 / 字面量 |
|---|---|
import 导入、from 来自、as(导入 / 导出别名)作为 / (类型断言 x as T、as const)断言为、export 公共、default(export default)默认 / (switch 里的 default:)默认情况、if 如果、else 否则、for 遍历、of 取自、in(二元运算 'k' in obj)在 / (for…in 头部)取自、while 当、do 执行、switch 分支、case 情况、break 跳出、continue 继续、return 返回、throw 抛出、try 尝试、catch 捕获、finally 最终、await 等待、yield 产出 |
const 常量、let 变量、var 变量、function 函数、async 异步、class 类、extends(类 / 接口继承)继承 / (泛型约束 T extends X、infer U extends X)限定为 / (条件类型 A extends B ? … : …)符合、implements 实现、interface 接口、type 类型、enum 枚举、new 新建、instanceof 属于、typeof 类型为、keyof 键名、readonly 只读、public 公共、private 私有、protected 受保护、static 静态、abstract 抽象、delete 删除、true 真、false 假、null 空、undefined 未定义 |
TypeScript 内置类型:string 字符串、number 数值、boolean 布尔、any 任意、unknown 未知、never 永不、void 无、object 对象、bigint 大整数、symbol 符号。Promise 等库类型不翻。
Python 关键字:import 导入、from(导入)从 / (raise … from)源自 / (yield from)自、as(导入别名、with … as、except … as)作为、def 函数、async 异步、await 等待、class 类、return 返回、if 如果、elif 否则如果、else(if 后)否则 / (for / while 后,循环没被 break 才执行)未中断时 / (try 后,没出错才执行)未出错时、for 遍历、in(比较里)在 / (for 头部与推导式里)取自、while 当、try 尝试、except 捕获、finally 最终、raise 抛出、with 使用、yield 产出、lambda 匿名函数、pass 跳过、break 跳出、continue 继续、global 全局、nonlocal 非局部、del 删除、assert 断言、not 非、and 并且、or 或者、is 是、not in 不在、is not 不是、None 空、True 真、False 假、match 匹配、case 情况。
Python 内置类型(仅注解里):str 字符串、int 整数、float 浮点数、bool 布尔、list 列表、dict 字典、set 集合、tuple 元组、bytes 字节串。
一行译文样例(必须逐字一致):
| 源码 | 译文 |
|---|---|
if (!folder) |
如果 (非 folder) |
return [] |
返回 空数组 |
for (const f of files) { |
遍历 (常量 f 取自 files) |
failed: { path: string, reason: string }[] |
failed: { path: 字符串, reason: 字符串 }[] |
return {⏎ locked: true,⏎ … }(issueLock.ts 20–24 行) |
返回 { locked: 真, prerequisiteNumber: prereq.number, prerequisiteColumn: prereq.column } |
err instanceof Error ? err.message : String(err) |
如果 err 属于 Error 则 err.message 否则 String(err) |
属性表样例(必须逐字一致;子行各列用 | 隔开):
| 源码 | 属性表 |
|---|---|
export async function findEnvFiles(workspaceRoot: string): Promise<string[]> {(envLock.ts 18–23 行,连同前面的 JSDoc) |
名称 findEnvFiles / 说明 Scan the workspace folder rooted at …(三行原文照留,按 Markdown 排成一段)/ 访问级别 公共 / 异步 是 / 输入参数 1 个:workspaceRoot | 字符串 | / 输出参数 Promise<字符串[]> |
async function chmodAll(workspaceRoot: string, mode: number): Promise<EnvLockResult> { |
名称 chmodAll / 访问级别 私有 / 异步 是 / 输入参数 2 个:workspaceRoot | 字符串、mode | 数值 / 输出参数 Promise<EnvLockResult> |
import { promises as fsp } from 'node:fs' |
来源 'node:fs' / 仅类型 否 / 导入项 1 个:promises 作为 fsp |
const ENV_PATTERN = '**/.env*' |
名称 ENV_PATTERN / 访问级别 私有 / 值 '**/.env*' |
const issue = issues.find(i => i.number === issueNumber) |
名称 issue / 初始值 issues.find(i => i.number 全等 issueNumber) |
def find_env_files(root: str) -> list[str]:(sample.py,连同下一行的 docstring) |
名称 find_env_files / 说明 Return every .env file under root. / 访问级别 公共 / 异步 否 / 输入参数 1 个:root | 字符串 / 输出参数 列表[字符串] |
from .schemas import (⏎ UserAvatarUploadResponse, … )(routes.py 19–23 行) |
来源 .schemas / 导入项 3 个:UserAvatarUploadResponse、UserDetailResponse、UserUpdateRequest |
payload = await file.read()(routes.py,函数体里) |
名称 payload / 初始值 等待 file.read() |
_default_port = 5680 if _role == "beat" else 5679 |
名称 _default_port / 访问级别 私有 / 初始值 如果 _role 等于 "beat" 则 5680 否则 5679 |
_CONTEST_STATUS_KEYS = {⏎ "published": "contests:publish", "ended": "contests:end", "draft": "contests:update" } |
名称 _CONTEST_STATUS_KEYS / 访问级别 私有 / 初始值 字典 3 项:"published" | "contests:publish"、"ended" | "contests:end"、"draft" | "contests:update" |
CONFIG = {"db": {"host": host, "opts": {}}, **defaults, 1: await load()} |
名称 CONFIG / 访问级别 公共 / 值 字典 3 项:"db" | 字典 2 项(下一层 "host" | host、"opts" | 空字典)、展开 | defaults、1 | 等待 load() |
const result: EnvLockResult = { total: files.length, ok: [], failed: [] }(envLock.ts 40 行) |
名称 result / 类型 EnvLockResult / 初始值 对象 3 项:total | files.length、ok | 空数组、failed | 空数组 |
export const handlers = { total: files.length, 'x-y': await load(), [key]: !ok, files, ...rest, async load(id: string): Promise<void> { … }, nested: { a: [], b: { c: 1 } } } |
名称 handlers / 访问级别 公共 / 值 对象 7 项:total | files.length、'x-y' | 等待 load()、[key] | 非 ok、files | files、展开 | rest、load | 异步 load(id: 字符串): Promise<无>、nested | 对象 2 项(下一层 a | 空数组、b | 对象 1 项,再下一层 c | 1) |
export abstract class Repository<K, T extends Entity = Entity> extends Base<string> implements Loader<T>, Disposable {(classes.ts 3–9 行,连同前面的 JSDoc 和 @Injectable()) |
名称 Repository / 说明 仓库基类。… / 访问级别 公共 / 抽象 是 / 装饰器 @Injectable() / 类型参数 K, T 限定为 Entity = Entity / 父类 Base<字符串> / 实现的接口 Loader<T>, Disposable |
export default class { |
名称 匿名 / 访问级别 公共 / 抽象 否 |
class Meta(Base, metaclass=ABCMeta):(classes.py,带 @dataclass(frozen=True)) |
名称 Meta / 访问级别 公共 / 装饰器 @dataclass(frozen=True) / 父类 Base, metaclass=ABCMeta |
class NotFound(AppException):(classes.py,连同 docstring) |
名称 NotFound / 说明 找不到资源。 / 访问级别 公共 / 父类 AppException |
预览数据与消息
扩展端把代码块树转成可序列化的视图数据发给 webview:
interface BlockViewBase {
id: string // 在树里的位置,例如 '6.1.0'
kind: BlockKind
startLine: number // header 的起止行,0 起
endLine: number
code: string // header 源码,去掉共同缩进
codeTokens: Segment[] // 源码着色(拼起来等于 code)
defines?: SourceRange // 块自己的名字(函数名、类名、只声明一个变量时的变量名)在源码里的范围,扩展端从 facts 取;跳到的定义是它时闪整块
attributes: Record<string, string> // 筛选属性(含 kind),见「筛选」
children: BlockView[]
}
/** 属性表块(有 facts)只带 properties,其余块只带 translation(注释块是 `{ markdown }`);没有翻译插件时是空数组。 */
type BlockView = BlockViewBase & ({ translation: PreviewText } | { properties: PropertyRow[] })
- 扩展 → webview:
{ type: 'document', version, languageId, blocks: BlockView[], attributes: FileAttribute[], translators: { id, label }[], translatorId, error?: string, reveal?: Reveal },文档每次变化(防抖 150ms)重发。attributes是这份文件里出现过的筛选属性,按筛选栏的先后排:{ id, name, values: { id, name, count }[] },名字取翻译插件的attributeNames,没有的用 id。reveal = { line, definition? }只随新预览的第一份文档走:从文本编辑器切过来时是它顶部那一行(滚到包含这行的块、顶到顶部,折叠的不展开);跳到定义时还带definition(定义处名字在源码里的下标),按「跳到定义」一节展开、滚动、闪。{ type: 'reveal', line, definition? }:同上,发给已经开着的预览。页面正在重新加载(标签页切回来)时收不到,扩展端改成随它ready后的第一份文档带过去。{ type: 'definitionProbed', id, found }:回答第id次probeDefinition。{ type: 'filter', excluded: Record<属性 id, 值 id[]> }:整个工作区共用的块筛选。webview 发ready后先于第一份文档发一次(第一份文档按筛选画),之后任何一个预览改了都发给所有开着的预览(包括改的那个)。
- webview → 扩展:
{ type: 'probeDefinition', id, version, offset }:按住修饰键指到带offset的名字上时问有没有定义,id每问一次加一;version不是当前文档版本就不答。{ type: 'goToDefinition', version, offset }:修饰键 + 点击,按「跳到定义」一节跳。{ type: 'ready' }{ type: 'edit', startLine, endLine, text }:用WorkspaceEdit把这几行整行替换成text(每行补回原来第一行的缩进)。{ type: 'selectTranslator', id }:按languageId记到globalState,重发文档。{ type: 'setFilter', excluded }:改块筛选(整份替换);扩展端逐项核对后记进workspaceState,再发filter给所有预览。webview 不在本地先改,只按扩展端发来的显示。
- 块的编辑状态和用户的折叠选择在 webview 里按块 id 记(
vscode.setState):{ editing: string[], folds: Record<id, boolean>, scroll? }。folds只记用户点过的(id → 折叠着没有),属性行和带下一层的子行也记在里面:属性行 id 是<块 id>/p<行序号>(例如6.1.0/p3),子行 id 是上一层(行或子行)的 id 加/<子行序号>,一层层往后接(例如6.1.0/p3/1是第 3 行的第 1 个子行,6.1.0/p3/1/0是它下面的第 0 个);块 id 只有数字和点,只有行那一节带p、其余各节是纯数字,所以块、行、各层子行的 id 互不相撞。子行 id 也是跳到定义时在页面上找要闪的那一项用的。没记的按默认:函数、类块折叠,其余块、属性行和子行展开。眼下折叠着哪些由「默认 + folds」算出来,界面、跳到定义的展开都只看这一份。跳到定义展开祖先块、属性行时也记进folds(记成展开)。读回来的状态逐项核对,旧版本存的、形状不对的项当没存过。文档刷新后保留;正在编辑且有焦点的块,刷新时不覆盖输入框内容。
颜色
词性(暗色取 VS Code Dark+,亮色取 Light+):
| 词性 | 暗色 | 亮色 |
|---|---|---|
| keyword | #c586c0 | #af00db |
| storage / operator | #569cd6 | #0000ff |
| variable | #9cdcfe | #001080 |
| function | #dcdcaa | #795e26 |
| type | #4ec9b0 | #267f99 |
| string | #ce9178 | #a31515 |
| number / literal | #b5cea8 | #098658 |
| comment | #6a9955 | #008000 |
| hint | --vscode-descriptionForeground |
同左 |
| plain | --vscode-editor-foreground |
同左 |
属性表里:说明按 comment,访问级别(公共 / 私有 / 受保护)和「作为」按 keyword,是 / 否按 storage,函数名称按 function,类名称按 type,未标注、无、N 个、字典 N 项、对象 N 项、展开、默认、默认导出、全部、整个模块、匿名按 hint。父类名(TypeScript extends 后面、JavaScript class_heritage 里、Python 类名后面括号里直接写的名字)源码着色和属性表里都按 type。
种类(标签字色 + 标签底 18% + 块底 7% + 边框 35%;亮色主题同色系):import 导入 #c586c0;function 函数 #dcdcaa;class 类、type 类型 #4ec9b0;field 字段、const 常量、variable 变量 #9cdcfe;if 条件、else 否则 #ce9178;loop 循环 #8f9cff;try 尝试、catch 捕获、finally 最终 #f48771;return 返回 #89d185;statement 语句 #9d9d9d;comment 注释 #6a9955(虚线框)。编辑状态:底色 --vscode-editor-background,边框 --vscode-focusBorder。
代码位置
vscode/src/codeReader/
contract.ts 通用契约
registry.ts 注册表 + CodeReaderApi
runtime.ts web-tree-sitter 初始化、语法包加载、解析
blockView.ts CodeBlock → BlockView(源码着色、去缩进、调用翻译)
attributes.ts 块的筛选属性(从 facts 推出通用属性 + 语言自己报的)、文件里出现过的属性和块数
languages/typescript.ts
languages/python.ts
translators/zhTemplate.ts 模板直译引擎(按语言的表驱动)
translators/typescriptZh.ts TS 表
translators/pythonZh.ts Python 表
explorer/fileTree.ts 侧栏文件树(FileTreeDecorator:给行添信息的接口)
notes/noteMap.ts 注释的键、保存 / 改名 / 删除时怎么改(纯函数)
notes/store.ts .spx/code-notes.json 读写
notes/noteItem.ts 有注释的行怎么显示:描述、悬停提示、contextValue(纯函数)
notes/fileNotes.ts FileNotes:内存里的注释、文件监听、跟着改名 / 删除走、给树画行
notes/commands.ts 「添加注释 / 编辑注释」输入框
editor/previewEditor.ts CustomTextEditorProvider
editor/messages.ts 消息类型
editor/blockFilter.ts 块筛选:workspaceState 读写、通知所有预览
editor/definitions.ts 查定义(vscode.executeDefinitionProvider)、几处定义的挑选框
editor/definitionAction.ts 查到的定义 → 怎么跳(纯函数)
index.ts registerCodeReader(context): CodeReaderApi
vscode/webview-ui/code-reader.html + src/codeReader/ 预览界面(React,Vite 第二个入口;属性表在 PropertyGrid.tsx,Markdown 渲染在 markdown.ts / TextPreview.tsx,筛选栏在 FilterBar.tsx,筛选规则在 filter.ts)
测试
- 语言插件:
envLock.ts、issueLock.ts的代码块树(种类、header 行号、层级)与原型CODE_TREE一致,只是紧贴函数的 JSDoc 算进了函数头部;Python 样例覆盖 import / def / docstring / if-elif-else / for / try-except / with / 推导式;函数的说明:JSDoc 各种挂法(export、默认导出、方法隔着装饰器、函数值的变量、嵌套函数)和不挂的情况,docstring 连同它前面的注释进头部。类:classes.ts、classes.py覆盖 export + 抽象 + 泛型 + extends / implements + 装饰器 + JSDoc、匿名默认导出、赋给变量 / 类字段的类表达式、函数里的类、类里的类、docstring、metaclass 关键字参数、下划线私有。 - 块头部:各样例里块的头部行互不重叠;函数、类说明所在的行在函数、类块里,「编辑」写回原样不变。
- 折叠:默认函数、类块折叠、其余展开;用户的选择盖过默认、重新加载后还在(嵌套子行的也是);各层子行的 id 互不相撞;旧版本存的状态不报错;跳到定义时默认折叠的函数、类也会展开。
- 属性表信息:
envLock.ts、sample.py、routes.py(原型里的 FastAPI 路由)加内联片段,覆盖各种导入写法、形参写法、类成员访问修饰符、解构和多变量声明、Python 下划线规则;字典 / 对象字面量的各项:Python 字符串键、名字键、嵌套字典、**展开、空字典、类属性,TypeScript 名字键、字符串键、计算属性名、简写、展开、方法和 getter(头部到主体之前)、嵌套对象、空对象,不是字面量的值没有 entries。 - 字典 / 对象的属性表:项数、键 | 值两列、嵌套一层层展开、「展开」、方法头部、空的一行;键和值里的名字、字符串带下标(嵌套的也是);每项标的定义是键,展开项不标;跳到定义落在对象属性名上只闪那一项、连外层各层一起展开,只是显示着的名字闪整块、展开到它那一层。
- 翻译:上面两张样例表逐字一致;属性名不被翻;每个被翻的词单独成段;节点模板嵌套时正确、槽位里照样翻、没命中模板的节点照抄;属性表里的值和一行译文用同一套表和模板。
blockView:codeTokens拼起来等于code;去缩进正确;函数、类、导入、常量、变量块只带属性表,其余块只带译文。- 注释的 Markdown:注释块、函数和类「说明」的视图数据是
{ markdown },原文照抄(reST 角色、双反引号、JSDoc 标签、HTML 都不转换),行注释连成的块留得住缩进四格的代码块;渲染时 HTML 当文字、带协议的网址和邮箱成链接、不带协议的文件名不成链接,javascript:/command:/ 相对路径的链接和图片不渲染;跳到定义不认 Markdown 里的名字。 - 行替换:多行块替换后缩进正确。
- 跳到定义:TS / Python 译文和属性表里的名字、点分模块名每一节、模块字符串带下标且指回源码原文,关键字、运算符、说明词不带(前面有中文注释也对得上);查到的定义 → 不动 / 原地 / 预览 / 普通编辑器 / 挑选框;webview 里按下标找目标块、要展开的祖先块和属性行,滚动位置。闪哪一处(TS、Python 源码走完整的语言插件 + 翻译插件 + 视图数据):参数 → 它那个子行,导入项 → 它那一项,多变量声明里的一个 → 它的名称行,类的泛型参数 → 「类型参数」那一行,函数名 / 类名 / 单个变量名(含解构模式)→ 整块,循环变量 → 所在的块,翻译插件没填
defines→ 整块(照样展开属性行);翻译插件给参数、导入项、声明的变量、泛型参数标的范围指回源码原文;块自己的名字只有函数、类和单变量声明有。 - 注册表:注册 / 注销后
languageIds与可选翻译正确;同一语言多个翻译时按globalState选中。 - 筛选:块的属性(TS、TSX 有抽象、仅类型,JavaScript / JSX 没有这两个属性、属性表也没有这两行,Python 有访问级别、异步);语言插件自己报的属性叠上去、盖过推出来的,kind 不变;文件里出现过的属性按契约顺序排、语言的排后面,值按第一次出现排、带所有层的块数,名字取翻译插件的、没有的显示 id,中文直译的名字和属性表一致;下拉框只给两种以上值的属性(或一种值正被去掉的),按钮上的字;同一属性内「或」、不同属性「且」、没有这个属性不受影响、藏整棵子树、「已隐藏」只数直接筛掉的;筛选记在
workspaceState、新实例读得回来、所有订阅的预览收到、形状不对的当没有;跳到被筛掉的块临时露出路径、目标的子块照常筛、文档刷新只留正在编辑的。 - 文件注释:键相对工作区文件夹、
/分隔,根和外面的路径没有键,手写的./、结尾/规范掉;保存去空白、清空即删;文件改名、目录挪位置里面各层一起走、名字只是前缀相同的不动、跨工作区文件夹先取出再放进去;删目录连带里面的;读写:文件缺失 / 解析失败 / 不是对象返回空表、跳过不合法条目、建.spx、键排序、删光留{};有注释 / 没注释的行的描述、悬停提示(Markdown 转义)、contextValue,和package.json里菜单的 when 子句对得上。
收尾
按仓库约定:package.json 版本 patch +1 → pnpm ext:package → 版本号改动一并 commit → 本机 code --install-extension <vsix> --profile augment。