# 代码阅读(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 空列表 → 空列表、空字典 → 空字典);复合语句头部去掉结尾的 `{` 或 `:`;多行合成一行(连续空白压成一个空格,右括号前的尾逗号去掉);注释去掉注释符号,原文照搬。关键字与前后内容之间保留源码里的空格(`!` 翻成「非」后补一个空格)。 关键字按所在语法节点选择译法:同一个关键字在不同语法结构里意思不同时,表里写一个默认译法,再加若干「语境 → 译法」。语境是从关键字节点往上的节点类型链,外层在前:`raise_statement` 指父节点是 raise_statement,`for_statement > else_clause` 指父节点是 else_clause、祖父节点是 for_statement(关键字包在一层子句节点里时要看到祖父节点)。几条语境同时匹配时取最长的,都不匹配用默认译法。节点类型以 tree-sitter 语法为准。下面两张表里写成「(语境)译法 / (语境)译法」的关键字就是按这条规则区分的,排第一个的是默认译法。 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 字节串。 样例(必须逐字一致,来自原型 `test.js`): | 源码 | 译文 | | --- | --- | | `export async function findEnvFiles(workspaceRoot: string): Promise {` | `公共 异步 函数 findEnvFiles(workspaceRoot: 字符串): Promise<字符串[]>` | | `async function chmodAll(workspaceRoot: string, mode: number): Promise {` | `异步 函数 chmodAll(workspaceRoot: 字符串, mode: 数值): Promise` | | `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 --profile augment`。