18 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 |
| 多种翻译怎么选 | 预览顶部下拉框,整个文件一起换,按语言记住选择;只有一种时不显示 |
| 侧栏点文件 | 支持的语言默认打开成「预览」;不支持的语言用普通编辑器打开 |
| 块内修改 | 改的就是文件本身:未保存标记、⌘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 类型。
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。- 复合语句头部到主体开始为止(包含行尾冒号,翻译时去掉)。
中文模板直译
结构照抄源码,只替换三类东西,其余(标识符、属性名、字面量、标点)原样保留并按词性着色:
- 关键字:只认语法树里的关键字节点(匿名节点),所以属性名
i.number、对象键不会被误翻。 - 内置类型:只在类型位置翻(TS 的
predefined_type、Python 注解里的内置类型名)。 - 运算符:
===全等、!==不全等、==等于、!=不等于、&&与、||或、!非(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:
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。