413 lines
38 KiB
Markdown
413 lines
38 KiB
Markdown
# 代码阅读(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 |
|
||
| 多种翻译怎么选 | 预览顶部下拉框,整个文件一起换,按语言记住选择;只有一种时不显示 |
|
||
| 侧栏点文件 | 支持的语言默认打开成「预览」;不支持的语言用普通编辑器打开 |
|
||
| 块内修改 | 改的就是文件本身:未保存标记、⌘S 保存、⌘Z 撤销都照常,切回「预览」按新代码重新翻译 |
|
||
| 折叠 | 有子块的块、属性表里带子行的行能折叠;函数块(含方法、嵌套函数)默认收起主体、属性表照常显示,其余块和属性行默认展开;用户点过的按他点的记住 |
|
||
| 函数的说明 | TypeScript 紧贴在函数前面的 JSDoc、Python 函数主体开头的 docstring 是函数自己的说明:进属性表「说明」一行,不再单独成一块注释;它的源码行算在函数块的头部里,「编辑」时和签名一起改 |
|
||
| 跳到定义 | 和普通编辑器一样 ⌘ / Ctrl + 点击名字跳到定义,定义由 VS Code 装的语言扩展(Pylance、TypeScript 语言服务……)查;目标语言有代码阅读就在预览里打开 |
|
||
| 侧栏文件树 | 只看和打开:展开目录、打开文件、刷新、全部折叠;不做新建 / 重命名 / 删除 |
|
||
| 译文用词 | 结构照抄原语言,只翻关键字、内置类型、运算符;用专业术语(`export` → 公共,`catch` → 捕获,`===` → 全等) |
|
||
| 函数、导入、常量、变量块 | 预览不显示一行译文,显示树形属性表;属性值里的类型、表达式照常直译 |
|
||
|
||
## 用户可见行为
|
||
|
||
### 活动栏与侧栏
|
||
|
||
- 活动栏新增图标(书本,`book-open-text` 样式),视图容器标题「代码阅读」,里面一个视图「代码阅读」。
|
||
- 视图是工作区文件树:每个工作区文件夹一个根节点(默认展开),目录在前、文件在后,名字不区分大小写排序;遵守 `files.exclude`。
|
||
- 树节点用 `resourceUri`,图标跟随用户的文件图标主题,git 改动颜色(M / U)由 VS Code 自动加上,外观与自带资源管理器一致。
|
||
- 视图标题栏:刷新、全部折叠(`showCollapseAll`)。文件变化时自动刷新(防抖)。
|
||
- 点文件:语言受支持 → 用「代码阅读预览」自定义编辑器打开;否则 `vscode.open`。
|
||
|
||
### 编辑器右上角切换
|
||
|
||
- 受支持语言的文件:普通编辑器里标题栏显示「预览」,预览里显示「编辑」。点一下在**同一个标签页位置**换成另一种:用 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`)。
|
||
- 块的组成(从左到右):折叠箭头(只有有子块的块才显示,其余占位对齐)、种类标签、内容、右侧切换按钮。
|
||
- 种类标签和底色按种类区分(颜色见下表);注释块虚线框。
|
||
- 内容两种状态,每块单独切:
|
||
- 预览:函数、导入、常量、变量块是属性表(见下节),其余块是一行译文;按词着色,等宽字体(中文部分用系统中文字体回退)。
|
||
- 编辑:这一块的源码(按语法树着色),可以直接改;蓝色边框。按钮文字「预览」;Esc 也回到预览。
|
||
- 复合语句(函数、条件、循环……)块里只放它自己的头部那几行(主体交给子块);收尾括号、空行不成块。函数的说明(JSDoc / docstring)也算在函数块的头部里。
|
||
- 折叠:函数块打开时默认收起子块(属性表照常显示),其余块默认展开;用户点过折叠箭头的块按他点的,记在视图状态里(见「预览数据与消息」)。
|
||
- 顶部工具栏:翻译下拉框(多于一种翻译时才显示)。
|
||
- 解析失败:显示错误原因,并提示用「编辑」查看源码。
|
||
|
||
### 属性表(函数、导入、常量、变量块)
|
||
|
||
卡片外观(折叠箭头、种类标签、底色、悬停高亮、「编辑 / 预览」切换)和其他块一样,只是内容换成一张属性表:左列属性名(界面字体、`descriptionForeground` 色,宽 76px),右列是值(等宽字体,按词性着色,长了换行),两列间距 12px,行高 20px。「输入参数」「导入项」下面展开子行,属性名前带折叠箭头(图标、大小、颜色同块的折叠箭头;点箭头或属性名切换,默认展开,折叠后值「N 个」照常显示,只收起子行);子行往右缩进 5px,左边一条 1px 竖线(`editorIndentGuide-background1`)。输入参数的子行分名称 / 类型 / 默认值三列对齐,前两列宽 150px / 220px,编辑区窄时按 28% / 38% 收,默认值列占剩下的。
|
||
|
||
| 种类 | 行 |
|
||
| --- | --- |
|
||
| 函数 | 名称(函数色;匿名函数写「匿名」)/ 说明(有说明才显示:JSDoc 去掉 `/**`、`*/` 和每行开头的 `*`,docstring 去掉引号;再去掉共同缩进和首尾空行,保留换行和 `@param` 这类标签行;按 comment 着色,多行照原样换行、长了折行;里面的名字不能点)/ 访问级别 / 异步(是 / 否)/ 装饰器(有才显示,源码原样照抄、按源码着色,多个换行)/ 输入参数(`N 个` 或「无」,每个参数一行:名称 \| 类型(没标注写「未标注」)\| 默认值(`默认 ` + 译文,没有留空))/ 输出参数(返回类型的译文,没标注写「未标注」) |
|
||
| 导入 | 来源(TypeScript 是字符串字面量原样,如 `'node:fs'`,字符串色;Python 是点分模块名,如 `fastapi`、`.models`,类型色)/ 仅类型(是 / 否,只有 TypeScript 有,看 `import type`)/ 导入项(`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` 是私有;嵌套在函数体、条件等语句块里的没有这一行。
|
||
- TypeScript 类成员:写了 `public` / `private` / `protected` 按写的,`#name` 是私有,都没写是公共。
|
||
- Python:只有模块顶层和类主体里的声明有;名字单下划线开头(`_x`、`__x`)是私有,前后双下划线(`__init__`)是公共,其余公共;解构赋值里的名字全是私有才算私有;赋值目标是属性、下标(`self.x = …`)时没有这一行。
|
||
|
||
值里的表达式只翻落在块头部里的那一段:`const v = useMemo(() => { … })` 这种值带语句块主体回调的块,回调主体是子块,值只翻到 `useMemo(() =>`,和块头部一致。
|
||
|
||
### 跳到定义
|
||
|
||
- 能点的:预览里(属性表的名称、类型、来源、导入项、默认值、值,以及一行译文)照抄自源码的名字——标识符、属性名、类型名、点分模块名的每一节(`luffy_agent.core.db` 三节各自能点,和 VS Code 一样)、字符串(TypeScript 的模块路径 `'./x'` 就是字符串)。翻出来的关键字、运算符、说明词不能点。
|
||
- 链接和编辑器里一样:只有按住修饰键(macOS ⌘,其余平台 Ctrl)、鼠标停在**查得到定义**的名字上时,名字加下划线、换成链接色(`--vscode-editorLink-activeForeground`)、光标变手形;松开修饰键或移开就恢复。有没有定义问扩展端(`vscode.executeDefinitionProvider`,文档 uri + 名字在源码里的位置),回答按文档版本缓存,同一版本里每个名字只问一次,旧版本的迟到回答丢掉。
|
||
- ⌘ / Ctrl + 点击:
|
||
- 一处定义,在当前文件:原地滚过去;
|
||
- 一处定义,另一个文件,语言有代码阅读:在同一个编辑器组用预览打开(`vscode.openWith`),滚过去;这个文件的预览已经在这一组开着就切过去、发给它,不重新打开;
|
||
- 一处定义,语言没有代码阅读:在同一个编辑器组用普通编辑器打开,选中定义处的名字;
|
||
- 几处定义:先弹挑选框(每项「相对路径:行号」,下面一行是那行源码),挑一处再按上面跳;
|
||
- 没有定义:不动(本来也不显示链接)。
|
||
- 和普通编辑器里 ⌘ + 点击一样:目标是否按「预览标签」打开听 `workbench.editor.enablePreview`;`workbench.editor.enablePreviewFromCodeNavigation` 为假(默认)时先把当前预览标签固定住,免得被目标替掉。
|
||
- 滚过去:目标块是显示着定义处那个名字的块(找不到就取包含定义那一行的块);它的祖先块折叠着就展开,名字在折叠的属性行子行里(参数、导入项)就展开那一行。整块已在视口里不滚,否则滚到视口正中(比视口高就顶部对齐)。然后目标块的边框和底色往聚焦色(`--vscode-focusBorder`)闪三下,共约 1.5 秒后复原;同一块再跳一次从头闪;系统开了「减少动态效果」时改成静止高亮 1.5 秒。
|
||
- 不按修饰键的点击照旧;⌘ / Ctrl + 点击名字不会触发块的编辑切换、折叠。
|
||
|
||
## 通用契约
|
||
|
||
放在 `vscode/src/codeReader/contract.ts`,语法树类型用 `@vscode/tree-sitter-wasm` 自带的 web-tree-sitter 类型。
|
||
|
||
```ts
|
||
export type BlockKind =
|
||
| 'import' | 'function' | 'class' | 'type' | 'field' | 'const' | 'variable'
|
||
| 'if' | 'else' | 'loop' | 'try' | 'catch' | 'finally' | 'return' | 'statement' | 'comment'
|
||
|
||
/** 源码里的一段,行号从 0 开始,end 为开区间的字节偏移。 */
|
||
export interface SourceSpan { startIndex: number, endIndex: number, 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
|
||
}
|
||
|
||
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 填
|
||
items: ImportItem[]
|
||
}
|
||
|
||
export interface DeclaratorFacts { name: Node, type?: Node, value?: Node }
|
||
|
||
export interface VariableFacts {
|
||
access?: Access
|
||
declarators: DeclaratorFacts[]
|
||
}
|
||
|
||
export interface BlockShape {
|
||
/** 这一块对应的语法树节点(整条语句)。 */
|
||
node: Node
|
||
/** 块自己显示 / 编辑的那一段:复合语句只到主体开始之前,其余是整条语句;函数的说明(doc)也算在里面。 */
|
||
header: SourceSpan
|
||
children: CodeBlock[]
|
||
}
|
||
|
||
/** 预览成属性表的块:种类和 facts 绑在一起,语言插件漏给 facts 编译不过。 */
|
||
export interface FunctionBlock extends BlockShape { kind: 'function', facts: FunctionFacts }
|
||
export interface ImportBlock extends BlockShape { kind: 'import', facts: ImportFacts }
|
||
export interface VariableBlock extends BlockShape { kind: 'const' | 'variable', facts: VariableFacts }
|
||
export type FactBlock = FunctionBlock | ImportBlock | VariableBlock
|
||
|
||
export type TextKind = Exclude<BlockKind, FactBlock['kind']>
|
||
/** 预览成一行译文的块。 */
|
||
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 }
|
||
|
||
/** 属性表里展开的一个子行:几列对齐的格子。 */
|
||
export interface PropertyItem { cells: Segment[][] }
|
||
|
||
/** 属性表的一行:属性名、着色的值、展开在下面的子行(输入参数、导入项)。 */
|
||
export interface PropertyRow { label: string, value: Segment[], items?: PropertyItem[] }
|
||
|
||
/** 语言插件:语法树 → 代码块树。 */
|
||
export interface LanguageAdapter {
|
||
id: string
|
||
/** 覆盖的 VS Code languageId,例如 ['typescript', 'typescriptreact', 'javascript', 'javascriptreact']。 */
|
||
languageIds: string[]
|
||
/** 该 languageId 用的语法包 .wasm 的绝对路径(宿主负责加载,保证和解析器运行时是同一份)。 */
|
||
grammarPath: (languageId: string) => string
|
||
toBlocks: (root: Node, source: string) => CodeBlock[]
|
||
/** 源码着色:给语法树叶子节点定词性。 */
|
||
tokenRole: (leaf: Node) => TokenRole
|
||
}
|
||
|
||
/** 翻译插件:代码块 → 一行译文或属性表。 */
|
||
export interface Translator {
|
||
id: string // 'typescript.zh-template'
|
||
label: string // '中文直译'
|
||
languageIds: string[]
|
||
translate: (block: TextBlock, source: string) => Segment[] // 只翻 header 那一段
|
||
describe: (block: FactBlock, source: string) => PropertyRow[] // 函数、导入、常量、变量块 → 属性表
|
||
}
|
||
|
||
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,引用源码节点,不做正则匹配文字);翻译插件决定属性名、枚举词(公共 / 是 / 默认导出)和值怎么译。第三方语言插件给函数、导入、常量、变量块填 facts 就能用上任何翻译插件的属性表。
|
||
|
||
## 解析运行时
|
||
|
||
- 依赖 `@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 组件、回调常见写法),子块来自主体。
|
||
- `lexical_declaration` / `variable_declaration`:模块顶层的 `const` → const,其余 → variable。
|
||
- `class_declaration` → class,子块:方法 → function,字段 → field,注释 → 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 导入:`import_statement` 的 `source`(`import x = require()` 取 `import_require_clause` 里的);`import_clause` 里的标识符是默认导入,`namespace_import` 是 `* as`,`named_imports` 里每个 `import_specifier` 的 name / alias;语句本身带 `type` 是仅类型。
|
||
- TypeScript 变量:每个 `variable_declarator` 的 name / type / value。
|
||
- 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 导入:`import_statement` 每个 name 一个来源、导入项是整个模块;`import_from_statement` 的 module_name 和各 name(`aliased_import` 拆出 alias),`wildcard_import` 是全部;`future_import_statement` 的来源是 `__future__`。
|
||
- Python 变量:`assignment` 的 left / type / right。
|
||
|
||
## 中文模板直译
|
||
|
||
结构照抄源码(按节点模板重排的语法结构除外,见下文),只替换三类东西,其余(标识符、属性名、字面量、标点)原样保留并按词性着色:
|
||
|
||
1. 关键字:只认语法树里的关键字节点(匿名节点),所以属性名 `i.number`、对象键不会被误翻。
|
||
2. 内置类型:只在类型位置翻(TS 的 `predefined_type`、Python 注解里的内置类型名)。
|
||
3. 运算符:`===` 全等、`!==` 不全等、`==` 等于、`!=` 不等于、`&&` 并且、`||` 或者、`!` 非(Python 的 `not` / `and` / `or` 本身是关键字)。
|
||
|
||
同一套表和节点模板也用来翻属性表里的值:引擎能翻任意一个节点(只取它落在块头部里的那一段),参数类型、默认值、返回类型、变量类型和初始值都走这条路。
|
||
|
||
其他规则:空数组字面量 → 空数组(Python 空列表 → 空列表、空字典 → 空字典);复合语句头部去掉结尾的 `{` 或 `:`;多行合成一行(连续空白压成一个空格,右括号前的尾逗号去掉);注释去掉注释符号,原文照搬。关键字与前后内容之间保留源码里的空格(`!` 翻成「非」后补一个空格)。
|
||
|
||
关键字按所在语法节点选择译法:同一个关键字在不同语法结构里意思不同时,表里写一个默认译法,再加若干「语境 → 译法」。语境是从关键字节点往上的节点类型链,外层在前:`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 …`(三行,照原样换行)/ 访问级别 公共 / 异步 是 / 输入参数 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` |
|
||
|
||
## 预览数据与消息
|
||
|
||
扩展端把代码块树转成可序列化的视图数据发给 webview:
|
||
|
||
```ts
|
||
interface BlockViewBase {
|
||
id: string // 在树里的位置,例如 '6.1.0'
|
||
kind: BlockKind
|
||
startLine: number // header 的起止行,0 起
|
||
endLine: number
|
||
code: string // header 源码,去掉共同缩进
|
||
codeTokens: Segment[] // 源码着色(拼起来等于 code)
|
||
children: BlockView[]
|
||
}
|
||
|
||
/** 属性表块(有 facts)只带 properties,其余块只带 translation;没有翻译插件时是空数组。 */
|
||
type BlockView = BlockViewBase & ({ translation: Segment[] } | { properties: PropertyRow[] })
|
||
```
|
||
|
||
- 扩展 → webview:
|
||
- `{ type: 'document', version, languageId, blocks: BlockView[], translators: { id, label }[], translatorId, error?: string, reveal?: Reveal }`,文档每次变化(防抖 150ms)重发。`reveal = { line, definition? }` 只随新预览的第一份文档走:从文本编辑器切过来时是它顶部那一行(滚到包含这行的块、顶到顶部,折叠的不展开);跳到定义时还带 `definition`(定义处名字在源码里的下标),按「跳到定义」一节展开、滚动、闪。
|
||
- `{ type: 'reveal', line, definition? }`:同上,发给已经开着的预览。页面正在重新加载(标签页切回来)时收不到,扩展端改成随它 `ready` 后的第一份文档带过去。
|
||
- `{ type: 'definitionProbed', version, offset, found }`:回答 `probeDefinition`。
|
||
- webview → 扩展:
|
||
- `{ type: 'probeDefinition', version, offset }`:按住修饰键悬停到带 `offset` 的名字上时问有没有定义;`version` 不是当前文档版本就不答。
|
||
- `{ type: 'goToDefinition', version, offset }`:修饰键 + 点击,按「跳到定义」一节跳。
|
||
- `{ type: 'ready' }`
|
||
- `{ type: 'edit', startLine, endLine, text }`:用 `WorkspaceEdit` 把这几行整行替换成 `text`(每行补回原来第一行的缩进)。
|
||
- `{ type: 'selectTranslator', id }`:按 `languageId` 记到 `globalState`,重发文档。
|
||
- 块的编辑状态和用户的折叠选择在 webview 里按块 id 记(`vscode.setState`):`{ editing: string[], folds: Record<id, boolean>, scroll? }`。`folds` 只记用户点过的(id → 折叠着没有),属性行也记在里面,id 是 `<块 id>/p<行序号>`(例如 `6.1.0/p3`);没记的按默认:函数块折叠,其余块和属性行展开。眼下折叠着哪些由「默认 + 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,未标注、无、N 个、默认、默认导出、全部、整个模块、匿名按 hint。
|
||
|
||
种类(标签字色 + 标签底 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(源码着色、去缩进、调用翻译)
|
||
languages/typescript.ts
|
||
languages/python.ts
|
||
translators/zhTemplate.ts 模板直译引擎(按语言的表驱动)
|
||
translators/typescriptZh.ts TS 表
|
||
translators/pythonZh.ts Python 表
|
||
explorer/fileTree.ts 侧栏文件树
|
||
editor/previewEditor.ts CustomTextEditorProvider
|
||
editor/messages.ts 消息类型
|
||
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)
|
||
```
|
||
|
||
## 测试
|
||
|
||
- 语言插件:`envLock.ts`、`issueLock.ts` 的代码块树(种类、header 行号、层级)与原型 `CODE_TREE` 一致,只是紧贴函数的 JSDoc 算进了函数头部;Python 样例覆盖 import / def / docstring / if-elif-else / for / try-except / with / 推导式;函数的说明:JSDoc 各种挂法(export、默认导出、方法隔着装饰器、函数值的变量、嵌套函数)和不挂的情况,docstring 连同它前面的注释进头部。
|
||
- 块头部:各样例里块的头部行互不重叠;函数说明所在的行在函数块里,「编辑」写回原样不变。
|
||
- 折叠:默认函数块折叠、其余展开;用户的选择盖过默认、重新加载后还在;旧版本存的状态不报错;跳到定义时默认折叠的函数也会展开。
|
||
- 属性表信息:`envLock.ts`、`sample.py`、`routes.py`(原型里的 FastAPI 路由)加内联片段,覆盖各种导入写法、形参写法、类成员访问修饰符、解构和多变量声明、Python 下划线规则。
|
||
- 翻译:上面两张样例表逐字一致;属性名不被翻;每个被翻的词单独成段;节点模板嵌套时正确、槽位里照样翻、没命中模板的节点照抄;属性表里的值和一行译文用同一套表和模板。
|
||
- `blockView`:`codeTokens` 拼起来等于 `code`;去缩进正确;函数、导入、常量、变量块只带属性表,其余块只带译文。
|
||
- 行替换:多行块替换后缩进正确。
|
||
- 跳到定义:TS / Python 译文和属性表里的名字、点分模块名每一节、模块字符串带下标且指回源码原文,关键字、运算符、说明词不带(前面有中文注释也对得上);查到的定义 → 不动 / 原地 / 预览 / 普通编辑器 / 挑选框;webview 里按下标找目标块、要展开的祖先块和属性行,滚动位置。
|
||
- 注册表:注册 / 注销后 `languageIds` 与可选翻译正确;同一语言多个翻译时按 `globalState` 选中。
|
||
|
||
## 收尾
|
||
|
||
按仓库约定:`package.json` 版本 patch +1 → `pnpm ext:package` → 版本号改动一并 commit → 本机 `code --install-extension <vsix> --profile augment`。
|