Files
superwork/docs/superpowers/specs/code-reader/spec.md
T

39 KiB
Raw Blame History

代码阅读(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 + 名字在源码里的位置);和编辑器一样,每回指到一个名字上(或指着名字按下修饰键、换了文档版本)都现问,回答不留到下次用——Pylance、tsserver 刚启动那几秒会答「没有」;只认最近一次询问的回答,迟到的丢掉。
  • ⌘ / 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 类型。

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:

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', id, found }:回答第 id 次 probeDefinition。
  • 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,重发文档。
  • 块的编辑状态和用户的折叠选择在 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。