📝 docs: 新增代码阅读设计文档与 Figma 原型插件

This commit is contained in:
cruldra committed 2026-09-25 00:41:45 +08:00
1 parent 46735827d2
commit 83bdfbe9c4
6 files changed
+1892

No files matched your search

+263
View File
@@ -0,0 +1,263 @@
# 代码阅读(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 撤销都照常,切回「预览」按新代码重新翻译 |
| 折叠 | 有子块的块能折叠,默认全部展开 |
| 侧栏文件树 | 只看和打开:展开目录、打开文件、刷新、全部折叠;不做新建 / 重命名 / 删除 |
| 译文用词 | 结构照抄原语言,只翻关键字、内置类型、运算符;用专业术语(`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 也回到预览。
- 复合语句(函数、条件、循环……)块里只放它自己的头部那几行(主体交给子块);收尾括号、空行不成块。
- 顶部工具栏:翻译下拉框(多于一种翻译时才显示)。
- 解析失败:显示错误原因,并提示用「编辑」查看源码。
## 通用契约
放在 `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 }
export interface CodeBlock {
kind: BlockKind
/** 这一块对应的语法树节点(整条语句)。 */
node: Node
/** 块自己显示 / 编辑的那一段:复合语句只到主体开始之前,其余是整条语句。 */
header: SourceSpan
children: CodeBlock[]
}
/** 着色用的词性,源码着色和译文着色共用。 */
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' // 被翻成文字的运算符:全等 与 或 非
| 'variable' | 'function' | 'type' | 'string' | 'number' | 'literal' | 'comment' | 'plain'
export interface Segment { text: string, role: TokenRole }
/** 语言插件:语法树 → 代码块树。 */
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: CodeBlock, source: string) => Segment[]
}
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` 上下文,并让已打开的预览刷新。
## 解析运行时
- 依赖 `@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
- 顶层与语句块内的每条语句一块;连续的行注释合成一块注释。
- `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。
- 赋值:模块顶层且变量名全大写 → 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。
- 复合语句头部到主体开始为止(包含行尾冒号,翻译时去掉)。
## 中文模板直译
结构照抄源码,只替换三类东西,其余(标识符、属性名、字面量、标点)原样保留并按词性着色:
1. 关键字:只认语法树里的关键字节点(匿名节点),所以属性名 `i.number`、对象键不会被误翻。
2. 内置类型:只在类型位置翻(TS 的 `predefined_type`、Python 注解里的内置类型名)。
3. 运算符:`===` 全等、`!==` 不全等、`==` 等于、`!=` 不等于、`&&` 与、`||` 或、`!` 非(Python 的 `not` / `and` / `or` 本身是关键字)。
其他规则:空数组字面量 → 空数组(Python 空列表 → 空列表、空字典 → 空字典);复合语句头部去掉结尾的 `{` 或 `:`;多行合成一行(连续空白压成一个空格,右括号前的尾逗号去掉);注释去掉注释符号,原文照搬。关键字与前后内容之间保留源码里的空格(`!` 翻成「非」后补一个空格)。
TypeScript 关键字表(流程 = keyword,声明 = storage):
| 流程 | 声明 / 字面量 |
| --- | --- |
| import 导入、from 来自、as 作为、export 公共、default 默认、if 如果、else 否则、for 遍历、of 取自、in 在、while 当、do 执行、switch 分支、case 情况、break 跳出、continue 继续、return 返回、throw 抛出、try 尝试、catch 捕获、finally 最终、await 等待、yield 产出 | const 常量、let 变量、var 变量、function 函数、async 异步、class 类、extends 继承、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 来自、as 作为、def 函数、async 异步、await 等待、class 类、return 返回、if 如果、elif 否则如果、else 否则、for 遍历、in(for 头部与推导式里)取自 / (比较里)在、while 当、try 尝试、except 捕获、finally 最终、raise 抛出、with 使用、yield 产出、lambda 匿名函数、pass 跳过、break 跳出、continue 继续、global 全局、nonlocal 非局部、del 删除、assert 断言、not 非、and 与、or 或、is 是、None 空、True 真、False 假、match 匹配、case 情况。
Python 内置类型(仅注解里):str 字符串、int 整数、float 浮点数、bool 布尔、list 列表、dict 字典、set 集合、tuple 元组、bytes 字节串。
样例(必须逐字一致,来自原型 `test.js`):
| 源码 | 译文 |
| --- | --- |
| `export async function findEnvFiles(workspaceRoot: string): Promise<string[]> {` | `公共 异步 函数 findEnvFiles(workspaceRoot: 字符串): Promise<字符串[]>` |
| `async function chmodAll(workspaceRoot: string, mode: number): Promise<EnvLockResult> {` | `异步 函数 chmodAll(workspaceRoot: 字符串, mode: 数值): Promise<EnvLockResult>` |
| `import { promises as fsp } from 'node:fs'` | `导入 { promises 作为 fsp } 来自 'node:fs'` |
| `if (!folder)` | `如果 (非 folder)` |
| `return []` | `返回 空数组` |
| `for (const f of files) {` | `遍历 (常量 f 取自 files)` |
| `failed: { path: string, reason: string }[]` | `failed: { path: 字符串, reason: 字符串 }[]` |
| `const issue = issues.find(i => i.number === issueNumber)` | `常量 issue = issues.find(i => i.number 全等 issueNumber)` |
| `return {⏎ locked: true,⏎ … }`(issueLock.ts 20–24 行) | `返回 { locked: 真, prerequisiteNumber: prereq.number, prerequisiteColumn: prereq.column }` |
| `def find_env_files(root: str) -> list[str]:` | `函数 find_env_files(root: 字符串) -> 列表[字符串]` |
## 预览数据与消息
扩展端把代码块树转成可序列化的视图数据发给 webview:
```ts
interface BlockView {
id: string // 在树里的位置,例如 '6.1.0'
kind: BlockKind
startLine: number // header 的起止行,0 起
endLine: number
code: string // header 源码,去掉共同缩进
codeTokens: Segment[] // 源码着色(拼起来等于 code)
translation: Segment[]
children: BlockView[]
}
```
- 扩展 → webview:`{ type: 'document', version, languageId, blocks: BlockView[], translators: { id, label }[], translatorId, error?: string }`,文档每次变化(防抖 150ms)重发。
- webview → 扩展:
- `{ type: 'ready' }`
- `{ type: 'edit', startLine, endLine, text }`:用 `WorkspaceEdit` 把这几行整行替换成 `text`(每行补回原来第一行的缩进)。
- `{ type: 'selectTranslator', id }`:按 `languageId` 记到 `globalState`,重发文档。
- 块的编辑 / 折叠状态在 webview 里按块 id 记(`vscode.setState`),文档刷新后保留;正在编辑且有焦点的块,刷新时不覆盖输入框内容。
## 颜色
词性(暗色取 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 |
| plain | `--vscode-editor-foreground` | 同左 |
种类(标签字色 + 标签底 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 消息类型
index.ts registerCodeReader(context): CodeReaderApi
vscode/webview-ui/code-reader.html + src/codeReader/ 预览界面(React,Vite 第二个入口)
```
## 测试
- 语言插件:`envLock.ts`、`issueLock.ts` 的代码块树(种类、header 行号、层级)与原型 `CODE_TREE` 一致;Python 样例覆盖 import / def / docstring / if-elif-else / for / try-except / with / 推导式。
- 翻译:上面样例表逐字一致;属性名不被翻;每个被翻的词单独成段。
- `blockView`:`codeTokens` 拼起来等于 `code`;去缩进正确。
- 行替换:多行块替换后缩进正确。
- 注册表:注册 / 注销后 `languageIds` 与可选翻译正确;同一语言多个翻译时按 `globalState` 选中。
## 收尾
按仓库约定:`package.json` 版本 patch +1 → `pnpm ext:package` → 版本号改动一并 commit → 本机 `code --install-extension <vsix> --profile augment`。