453 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、Shell 脚本(bash / sh / zsh)、Dockerfile |
| Rust | .rs(VS Code 的 rust):读成导入、结构体、枚举、实现、函数、特质;serde、thiserror、axum、sqlx、tauri 各一张库卡片。tokio 不单独做。见「Rust」 |
| 扩展方式 | 写代码注册:语言插件、翻译插件都实现约定接口,通过本插件 activate() 导出的 API 注册;内置实现也走同一个 API |
| 多种翻译怎么选 | 预览顶部筛选栏最右边的下拉框,整个文件一起换,按语言记住选择;只有一种时不显示 |
| 块筛选 | 预览顶部钉住一条筛选栏,按块的属性(种类、访问级别、异步……)筛;属性由语言插件按和语言无关的 id 报(通用的由宿主从 facts 推),名字由翻译插件起,webview 不认识任何语言;筛选整个工作区共用、记住 |
| 块的种类 | 块左边横排这块认出来的概念图标(各 20px,间距紧)。默认是语言的种类(类、函数……):图标名由宿主按内置种类写进预览数据,名字由翻译插件起,颜色在 CSS;语言插件可以再声明概念,认到才出现。库规则认领时语言的概念留着,规则的种类追加在后面,规则还可以再带认出的概念。悬停各自的名字,读屏把名字连起来念。筛选的种类、卡片边框色仍是规则的种类。同一个概念 id 只留第一个。webview 只按图标名画。见「预览(代码块树)」 |
| 侧栏点文件 | 支持的语言默认打开成「预览」;不支持的语言用普通编辑器打开 |
| 块内修改 | 改的就是文件本身:未保存标记、⌘S 保存、⌘Z 撤销都照常,切回「预览」按新代码重新翻译 |
| 折叠 | 有子块的块、属性表里带子行的行、带下一层的子行(字典 / 对象里嵌套的字典 / 对象)能折叠;函数块(含方法、嵌套函数)、类块、枚举块、异常块和库规则的卡片默认收起主体、属性表照常显示,其余块、属性行和子行默认展开;用户点过的按他点的记住 |
| 函数、类的说明 | TypeScript 紧贴在函数、类前面的 JSDoc,Python 函数、类主体开头的 docstring 是它自己的说明:进属性表「说明」一行,不再单独成一块注释;它的源码行算在函数、类块的头部里,「编辑」时和签名一起改 |
| 变量、字段的说明 | Python 模块、类主体里紧跟在一条赋值后面的字符串(属性 docstring,PEP 257 / Sphinx 的写法)是这个变量的说明:常量、变量块进属性表「说明」一行(紧跟名称),字段块排在一行译文下面;不再单独成块,它的源码行算在这一块里,「编辑」时和赋值一起改 |
| 注释挂在它讲的块上 | 注释一定是讲某块代码的:普通注释(#、//、/* */、没挂成说明的文档注释、Python 当注释用的字符串)不单独成块,挂到它讲的那块上,是那张卡片的「注释」一行(排在名称、说明后面);同一层里挂到后面最近的那块(中间隔空行也算),后面没有就挂到前面那块,这一层一块代码都没有就挂到父块。单独成块的只有文件开头的说明:第一块代码之前、和它隔着空行的注释,以及本来就讲整个文件的(Python 模块开头的 docstring、Rust 的 //!、JSDoc 的 @file)。块的编辑范围连同挂在它上面的注释。见「注释挂到它讲的块上」 |
| 注释按 Markdown 显示 | 卡片的「注释」一行、文件开头的说明和函数、类、变量、字段的「说明」按 Markdown 渲染,像 Kotlin 的 KDoc:去掉注释符号、引号和共同缩进后的原文原样交给 Markdown,不按语言转换(reStructuredText 的 :func:、JSDoc 的 @param 都照抄,注释以后慢慢改成 Markdown 写法);段内单个换行是软换行,空行分段;仓库内图片按原图比例显示,点了打开原图,没有就写「找不到图片」 |
| 跳到定义 | 和普通编辑器一样 ⌘ / Ctrl + 点击名字跳到定义,定义由 VS Code 装的语言扩展(Pylance、TypeScript 语言服务……)查;目标语言有代码阅读就在预览里打开 |
| 悬停看定义 | 和普通编辑器里的悬停一样:鼠标停在名字、注释里指到符号的链接上一会儿(VS Code 的 editor.hover.delay),弹出定义所在那块的卡片(别的文件的也行,和那个文件的预览画的是同一张),点卡片标题跳过去 |
| 高亮名字的各处出现 | 和普通编辑器一样单击名字就高亮它在这个文件里的各处出现,由语言扩展按语义认(同名的别的东西不算),颜色用 VS Code 的单词高亮色,再用 focusBorder 色补一笔(当前那一处描框、别的画细线);看不见的(折叠、筛掉的)点的时候不自动展开,筛选栏上写「名字 · 2/5」;⌘↓ / ⌘↑(可改绑的命令)在各处之间跳,跳到折叠着的地方会展开 |
| 侧栏文件树 | 只看和打开:展开目录、打开文件、刷新、全部折叠,外加给目录 / 文件记注释、按审查工单只看 PR 改过的文件;不做新建 / 重命名 / 删除 |
| 审查工单 | 标题栏第一个按钮「选工单」列出看板「审查」列的工单和「进行中」列里已经有 PR 的工单(按列分组),选中后树里只剩这张工单的 PR 改过的文件(目录层级照留、全部展开,按 M / A / D 着色,删掉的列出来但打不开);点开的是本机这张工单的工作副本里 PR 最新的代码,不标差异、不开 diff;本机没有工作副本就拉 PR 分支建一个。弹框列的是看板上现在摆着的工单(不再加载一遍),选中后重读那张工单和它的 PR。见「审查工单」 |
| 文件注释 | 读代码时给目录 / 文件随手记一句:存在各工作区文件夹的 .spx/code-notes.json,跟着仓库走;在 VS Code 里改名、挪位置、删除时注释跟着变 |
| 入口 | 只认项目自己写明的启动项:pyproject.toml 的 [project.scripts] / [project.gui-scripts]、Cargo.toml 的 src/main.rs 和 [[bin]] 的 path、go.mod 模块里 package main 加 func main 的文件、package.json 的 bin;不按文件名、函数名猜。侧栏里入口文件的图标换成绿色播放图标,预览里名字对上的块左边的种类图标换成同样的绿色播放图标。见「入口」 |
| 问 AI | 块右上角「问 AI」,在块下面问 Claude Code(claude -p,只读,用输入框旁边选的 profile,记住上次选的,没选过用插件默认的)这块代码,回答逐字出来按 Markdown 显示、能追问(接着这块最近一次的会话,收起再展开、重启 VS Code 也接得上)、能停;输入框里能粘贴 / 拖进图片随问题发(存在 .spx/code-qa/,跟着问答删);问答存进各工作区文件夹的 .spx/code-qa.json,灰字挂在块下面免得重复问,理解了就删。问答按块的位置记号挂,记号只由和语言无关的块数据(种类、facts 里的名字、源码哈希)算出,文件别处改了照样认得;认不回来的不显示,留在文件里,代码改回来又挂回去。见「问 AI」 |
| 译文用词 | 结构照抄原语言,只翻关键字、内置类型、运算符;用专业术语(export → 公共,catch → 捕获,=== → 全等) |
| 函数、类、导入、常量、变量块 | 预览不显示一行译文,显示树形属性表;属性值里的类型、表达式照常直译 |
| 字典 / 对象、列表字面量 | 常量、变量的值是 Python 字典 / TypeScript 对象字面量时不挤成一长行:值写「字典 N 项」/「对象 N 项」,各项展开成「键 | 值」两列的子行;跨几行写的列表(元组、集合、数组)同样写「列表 N 项」,各项展开成「第 N 项 | 值」,整项是调用的排成「新建 / 调用 …」带自己的实参表;项的值又是字典、跨几行的列表时一层层往下展开。实参的值、一行译文的块(return [...]、表达式语句……)里跨几行写的字典、列表同样拆,一行写「列表 N 项」,各项排在下面;参数默认值照旧一行 |
| 输入参数 | 排成带表头和边框的表格:名称 | 类型 | 默认值,表头写了「默认值」,格子里不再写「默认 」 |
| 库规则 | 第四种插件:按库认出一种写法,把那块整个换成专门的卡片(内置 SQLAlchemy 规则:模型类 → 「数据表」卡片;内置 Celery 规则:Celery(...) → 「Celery 应用」卡片、beat_schedule → 「定时任务」卡片;内置 dataclasses 规则:数据类 → 「数据类」卡片;内置 FastAPI 规则:挂在路由对象上的函数 → 「接口」卡片,请求、响应各一张表),或把那块认成契约里现成的一种属性表块(内置 enum 规则:Python 自带 enum 库的类 → 枚举块)。和语言插件、翻译插件一样写代码注册、只经契约拿信息;文件导入了规则关心的库才启用,由宿主按语言插件给的导入判断;卡片的用词归规则自己,认成的现成块照常由翻译插件排,webview 只画结构。见「库规则」 |
| 枚举 | 枚举是一种块(种类「枚举」,紫色 #b180d7 / 亮色 #652d90),属性表:名称 / 说明 / 访问级别 / 装饰器 / 继承(有才显示)/ 成员 N 个,成员排成表格「成员 | 值 | 说明」。TypeScript 的 enum 是语言自己的写法,由语言插件报;Python 没有枚举语法,自带 enum 库的写法由库规则认成同一种块。两边同一种块、同一张表、同一个筛选种类,webview 只有一种画法 |
| 异常 | 异常类是一种块(种类「异常」,红色 #f14c4c / 亮色 #cd3131,和「捕获」同一色系),属性表:名称 / 说明 / 访问级别 / 装饰器 / 继承 / 属性 N 个(表格「属性 | 类型 | 值 | 说明」)/ 构造参数 N 个(表格同函数的输入参数),有才显示。异常是语言自己的概念,Python、TypeScript 都由语言插件认:父类是语言自带的异常类、同一文件里认出来的异常类,或名字按命名习惯以 Error / Exception(Python 还有 Warning)结尾。方法(含构造函数)照常是子块、默认收起。见「异常」 |
| 规则自己查定义 | 规则可以把自己给的名字标成「引用」(Celery 任务表里的任务名),⌘ / Ctrl 悬停、点击这样的名字时由规则查定义(Celery 在整个工作区里找任务函数),其余名字照旧问语言扩展;webview 只有一条链接 / 跳转的路,样子、闪、挑选框、在预览里打开都一样。规则要在全工作区找东西时只描述要找什么(WorkspaceIndex),找文件、解析、缓存、跟着改动更新归宿主。见「规则自己查定义」 |
| 中文名 | 代码里给定义起的中文名(Annotated[T, Comment("当前用户")]、@Comment("…")、行尾 # @Comment 当前用户,没有时取参数在 docstring / JSDoc 里的说明):预览里这个定义和用到它的各处都显示中文名,悬停看原名,定义处本身在中文名后面淡淡写原名。语言插件认写法,用到它的各处由语言扩展按语义认(同一文件问 document highlights,别的文件定义的先查工作区索引、再问定义提供者),webview 只按源码下标换字。见「中文名」 |
| 推导式(写在表达式里的遍历) | Python 的列表 / 集合 / 字典推导式、生成器表达式出现在译文任何地方(一行译文、属性表的值、默认值、字典项的值),都原地换成一个带循环图标和结果类型(生成器 / 列表 / 集合 / 字典)的小框,颜色同「循环」块,框里按读的顺序重排:「遍历 来源 的每个 变量,仅当 条件,取 值」。语言插件报结构,翻译插件定用词,webview 只认「框」不认语言 |
| 调用与构造 | 实参列表跨了几行写的调用不挤成一长行:写成「[等待 ]新建 / 调用 被调用的名字 · N 个参数」,实参摊成一张表「参数 | 值」(按名字传的写名字,按位置传的写「第 N 个」,*x / ...x 写「展开」,**x 写「展开关键字」);一行写完的调用照旧一行译文。实参的值又是跨几行写的调用时,值那一格写它的一行、它的表套在格子里,一层层往下套;埋在式子里、后面还跟着东西的(X(\n…\n).model_dump())括起来「(新建 X · 3 个参数).model_dump()」。新建还是调用:TypeScript 看有没有 new;Python 写法一样,查被调用的名字的定义,是类就是「新建」,是函数、查不到都是「调用」,先按调用画、查到了再刷新。表在变量 / 常量属性表的初始值 / 值那一行下面(能折叠),在返回、语句这些一行译文的块里排在那一行下面。语言插件报结构,翻译插件定用词,webview 只画表。见「调用与构造」 |
| Shell 脚本 | .sh、.bash、.zsh 和首行写着 shell 解释器的无扩展名文件(VS Code 的 shellscript):读成解释器、选项(set -euo pipefail 每个开关一行)、变量 / 常量、分情况(短的一张「情况 | 做什么」表,长的每个情况一块子块)、函数(参数按位置「位置 | 函数里叫」、输出、返回)、命令、管道、条件、遍历;$1 → 第 1 个参数,${x:-d} → x(没设就用 d),$(cmd) → 执行〔…〕的输出。语言插件在 tree-sitter-bash 上报块和事实,翻译插件定用词,webview 不认识 shell。同一文件里的函数、变量由语言插件认,⌘ / Ctrl + 点击、单击高亮照常。见「Shell 脚本」 |
| 命令规则 | 第五种插件:按命令名注册(rm、curl、tar……),把一条命令连同选项读成一句(「删除 dest(连同里面的东西、不提示)」);规则不认识的选项照抄在后面,没有规则的命令照抄命令名,重定向由翻译插件统一读。见「命令规则」 |
| Dockerfile | Dockerfile、Dockerfile.*、*.dockerfile、Containerfile:读成解析指令、阶段(每个 FROM 一个,后面的指令是子块;阶段名能跳过去)、执行(RUN 按命令拆,只有一条命令时一行)、复制、变量(构建参数 / 环境变量)、设置、标签、健康检查、启动;连着写的 ENV / ARG / COPY(从同一个地方)/ LABEL 并成一块加一张表。语法包取自 npm 包 tree-sitter-wasm。见「Dockerfile」 |
| 嵌着的代码 | 一种语言里嵌着另一种语言的代码(Dockerfile 的 RUN 是 shell):语言插件报出是哪几段,宿主交给那种语言的语言插件、翻译插件、命令规则读,只解析这几段;外层的翻译插件只问读出来的样子,不再读一遍。见「Dockerfile」 |
| 选项多的命令 | 命令规则认出的选项有 4 个以上、或同一个选项写了几遍时,读成「… · N 个选项」加一张「选项 | 意思」表,写了几遍的并成一行;少的照旧写在括号里 |
用户可见行为
活动栏与侧栏
- 活动栏新增图标(书本,
book-open-text样式),视图容器标题「代码阅读」,里面一个视图「代码阅读」。 - 视图是工作区文件树:每个工作区文件夹一个根节点(默认展开),目录在前、文件在后,名字不区分大小写排序;遵守
files.exclude。 - 树节点用
resourceUri,图标跟随用户的文件图标主题,git 改动颜色(M / U)由 VS Code 自动加上,外观与自带资源管理器一致。 - 视图标题栏:选工单(见「审查工单」)、刷新、全部折叠(
showCollapseAll)。文件变化时自动刷新(防抖)。 - 点文件:语言受支持 → 用「代码阅读预览」自定义编辑器打开;否则
vscode.open。 - 侧栏跟着当前文件走,规则同 VS Code 资源管理器:激活的标签页换成一个文件时,树展开到它并选中(
TreeView.reveal,select: true、focus: false,焦点留在编辑器)。点仓库链接、⌘ / Ctrl + 点击跳到定义、在标签栏切换、侧栏点开都走这一条(explorer/treeLocator.ts盯着window.tabGroups的变化)。文本编辑器、代码阅读预览取它开的文件,对比编辑器取改后那边,路径页(问文件夹的预览)取它问的文件夹(文件夹顺带展开);同一个标签页别的变化(改了没保存)不重新定位。explorer.autoReveal:false不跟;focusNoScroll(选中但不滚动)当作开着——插件的TreeView.reveal选中就会滚过去,做不到不滚动。explorer.autoRevealExclude命中它自己或路上任何一层目录的(默认node_modules、bower_components)不跟。- 只在代码阅读这棵树露着时跟,不把侧栏抢过来;树再露出来时补上当前文件。
- 树上没有的不动:工作区外的、
files.exclude藏了的目录里的。开着「未提交的更改」「审查」时只在摆出来的文件里找:在就定位,同一个文件摆在两组里(暂存的、没暂存的)取最先摆的那行;不在就不动,不退出筛选。
- 鼠标停在目录 / 文件行上,行尾一排按钮(
view/item/context的inline组):先「复制」,再「添加注释 / 编辑注释」(见下两节)。工作区根节点没有。
复制
原型画板「侧栏 · 复制菜单」。
- 行尾第一个按钮「复制」(
$(copy)):点开一组复制方式——文件:复制文件名(带后缀)、复制文件名(不带后缀)、复制相对路径、复制绝对路径;目录:复制文件夹名、复制相对路径、复制绝对路径(没有「不带后缀」)。右键菜单里也有同样几项,排在注释前面。有没有注释都一样;工作区根节点没有。 - 做法:
contributes.submenus里一个带$(copy)图标的子菜单(superpowers.codeReader.copy),挂在view/item/context的inline@1。VS Code 的树把行内子菜单画成点开的下拉按钮(treeView.ts行尾按钮走createActionViewItem,子菜单是SubmenuEntryActionViewItem;子菜单的项用同一份带viewItem的上下文算when、同样把这一行传给命令),1.97 起就支持。原型里按钮旁的小箭头、菜单右边淡色的「会复制出什么」VS Code 的菜单做不出,是和原型的有意差异;复制后状态栏说一声。 - 已知的 VS Code 问题:菜单用 VS Code 自己画的样式时(
window.menuStyle不是 native;macOS 默认跟着自定义标题栏,也是这种),在下拉菜单里点了一项,命令照常执行,但菜单不会自己收起,要点别处或按 Esc。原因在 VS Code:树给行尾按钮换执行器时把旧的执行器销毁了,下拉菜单还拿着旧的,「执行了就收起」的通知收不到。右键菜单不受影响。 - 复制出什么(
explorer/copyText.ts,纯函数):名字取Uri.path的最后一段;不带后缀只去掉最后一个扩展名(a.test.ts→a.test),点开头、后面没有别的点的名字(.env)没有扩展名、原样,.env.local→.env(和files.exclude的$(basename)同一个函数);相对路径相对所在的工作区文件夹、/分隔;绝对路径是Uri.fsPath(本机写法)。 - 写剪贴板用
vscode.env.clipboard.writeText,状态栏已复制:<内容>停 3 秒,连着复制只留最新一条。 - 命令(
copyFileName/copyFolderName/copyFileStem/copyRelativePath/copyAbsolutePath)只挂在侧栏行上,命令面板里藏起来(when: false)。文件名、文件夹名复制的是同一样东西,分两个命令只为菜单上写对字,按viewItem =~ /^file( noted)?$/、/^folder( noted)?$/挑。
在系统文件管理器中显示
- 右键菜单里复制那组后面单独一组(中间有分隔线),再往后是注释:在系统的文件管理器里显示这一行。字和 VS Code 自带资源管理器一样按系统叫:macOS「在 Finder 中显示」、Windows「在文件资源管理器中显示」、Linux「打开所在的文件夹」。文件、目录都有(有没有注释、在不在「更改」组都一样,审查时也有);工作区根节点、分组行没有。只在右键菜单里,行尾没有按钮。
- 做法:交给 VS Code 自带的命令
revealFileInOS,传这一行的Uri(explorer/revealCommands.ts):文件是打开所在文件夹并选中它,目录是在上一层里选中它。 - 命令(
revealInFinder/revealInFileExplorer/openContainingFolder)做的事一样,分三个只为菜单上按系统写对字:when分别带isMac/isWindows/isLinux,每个系统只出自己那一项;命令面板里藏起来(when: false)。 - 排在复制和注释中间靠组名:VS Code 按组名的字母序排右键菜单的组,组名
fileManager落在copy和notes之间。
文件注释
读代码时给目录或文件随手记一句(原型画板「文件注释 · 悬停出按钮 / 填写 / 已保存」)。
- 鼠标停在目录 / 文件行上,行尾「复制」后面的按钮(
inline@2):没注释时「添加注释」($(note)),有注释时「编辑注释」($(edit))。右键菜单里也有同一个命令。工作区根节点没有。 - 两个命令只挂在侧栏行上,命令面板里藏起来(
when: false):面板里没有「哪一行」可言,按当前文件记还得另判断它有没有注释,没有必要。 - 点按钮弹 VS Code 顶部的单行输入框(
window.showInputBox):标题添加注释 · <名字>/编辑注释 · <名字>,编辑时预填原注释(整段选中),提示按 Enter 保存,按 Esc 取消;清空后保存即删除注释。回车保存(去掉首尾空白),Esc 或点别处不改,清空后保存即删除。VS Code 会在提示后面自己再补一句「(Press 'Enter' to confirm or 'Escape' to cancel)」,关不掉。 - 显示:有注释的行名字后面灰字是注释(
TreeItem.description,放不下 VS Code 自己截断);悬停提示是 Markdown,第一行粗体的相对路径,下一行注释全文,注释里的 Markdown 符号原样显示。没注释的行和原来一样。 - contextValue:工作区根
root,目录folder,文件file;有注释的后面加noted。菜单按viewItem =~ /^(file|folder)$/(添加)和/^(file|folder) noted$/(编辑)切换。 - 存储:每个工作区文件夹一份
<工作区文件夹>/.spx/code-notes.json,{ "相对路径": "注释" },路径用/分隔、相对这个工作区文件夹;和.spx里别的文件同一套写法:文件缺失或解析失败当作没有注释,写时按需建.spx,两格缩进、键排好序、末尾换行。存第一条注释前不生成文件;注释删光后文件留着({})。 - 在 VS Code 里改名、挪位置(资源管理器拖动、
WorkspaceEdit等,走workspace.onDidRenameFiles):注释跟着走,目录连带里面各层的注释一起走;挪到另一个工作区文件夹就搬到那边的文件里,挪出工作区就不要了。删除(onDidDeleteFiles):连带目录里各层的注释一起删。在 VS Code 外面(终端、git)改名删除不跟。 - 外部改了
code-notes.json(git pull、手改):文件监听重读,树上跟着变。 - 刷新:只重画注释变了的那几行(文件树按 Uri 记住交给 VS Code 的节点对象,VS Code 按对象认行);还没展开过的行不用管。
- 注释和代码阅读的解析、翻译无关,单独放在
notes/;文件树只留一个「给行添信息」的接口(FileTreeDecorator),由explorer/rowDecorator.ts的RowDecorator把注释和项目入口(见下节)合成一行的灰字、提示和图标(纯函数explorer/rowDecoration.ts),index.ts接上。
入口
读一个陌生项目先找它从哪儿启动:侧栏标出项目的入口文件,点进预览再标出入口函数。只认项目自己写明的启动项,不靠文件名、函数名猜——叫 main.py、里面有个 main 函数、写了 if __name__ == "__main__"、src/lib.rs、index.js 都不算入口。
- 认这四种,每种只有一条(
entries/entryPoints.ts的findEntryPoints,纯函数:传入找到的文件路径,读内容、问「这个路径在不在」都经传进来的FolderReader):项目 读哪儿 入口文件 预览里标的名字 Python pyproject.toml的[project.scripts]、[project.gui-scripts],值模块.路径:名字相对 pyproject.toml 所在目录按顺序找第一个存在的: src/<模块路径>.py、<模块路径>.py、src/<模块路径>/__init__.py、<模块路径>/__init__.py(点换成/);都没有就不标:后面的名字(带点的取第一节,[extra]不管);只写了模块的只标文件Rust Cargo.toml所在目录的 src/main.rs;[[bin]]写了path的,那个路径(相对 Cargo.toml 所在目录,不存在的不标)mainGo go.mod这个模块自己的目录里(递归,但别的 go.mod 的子目录归那个模块)同时有 package main和最外层func main(的.go文件;注释、字符串里的,方法func (T) main()不算mainJavaScript / TypeScript package.json的"bin":字符串,或{ "命令名": "文件" }那些文件(相对 package.json 所在目录,不存在的不标) 不标,只标文件 - 不认的:
[tool.poetry.scripts]等别的表、[[bin]]没写path的、src/bin/*.rs自动发现、src/lib.rs、package.json 的main/exports/scripts。 - 一个工作区文件夹里可以有很多个项目(子目录各有自己的清单),各算各的;同一个文件被几条指到,名字合在一起。不进
node_modules、.venv、vendor、dist、__pycache__(还有.git)。 - 什么时候算(
entries/workspaceEntries.ts的WorkspaceEntries):打开时每个工作区文件夹扫一遍(findFiles找清单和.go,排除上面那几个目录);清单、.go改了,或文件增删了(入口文件出现、消失),过 500ms 重扫那个工作区文件夹;入口变了的文件报给侧栏重画那几行、开着的预览重新发文档。 - 侧栏:入口文件那一行的文件图标换成绿色播放图标(VS Code 自带图标字体的
play,颜色是主题色testing.iconPassed,即测试通过的那种绿;explorer/rowDecoration.ts的ENTRY_ICON)。只换代码阅读侧栏这棵树的行,资源管理器、标签页不变;git 改动的颜色和 M / A 字母照旧。名字后面的灰字只是注释,没注释时没有灰字,不写「入口」;悬停提示在注释下面再写一行「入口,从app开始读」(没有名字的写「入口」)。contextValue 不变,菜单照旧只按有没有注释换。目录、工作区外面的行(审查的工作副本)不标。 - 预览:入口文件里,文件最外层、名字对上的那一块(函数、常量、变量、库规则的卡片,谁对上就是谁;Python 的
app = typer.Typer()是变量块),卡片左边的种类图标换成绿色播放图标,名称后面不加字。走概念图标那条路:入口是一个概念(identry、名字「入口」、图标play、颜色var(--vscode-testing-iconPassed),和侧栏同一个主题色;entries/entryMarks.ts的ENTRY_CONCEPT),换掉概念列表的第一个(种类),后面别的概念照旧(库规则的卡片画几个图标的,入口替掉第一个,规则的种类和另认的照旧);种类、筛选、卡片边框色都不变。webview 照常按概念列表画,不特判入口。宿主在翻译好的结果上换(entries/entryMarks.ts的markEntryBlocks,documentMessage里调),各语言的翻译插件不用管:块的名字按定义处(defines)在源码里的那一段认;对不上就什么都不改。标差异时旧版本按同样的入口标,两边的入口块一样,不会被当成改动。
审查工单
看板「审查」列常压着好几张工单,逐张读 PR 改了什么时,侧栏只看这张工单的 PR 改过的文件(原型画板「审查 · 选工单」「审查 · 改动的文件」)。开发完了却没走自动审查的工单会停在「进行中」列,它们的 PR 一样要看,所以「进行中」列里已经有 PR 的也能选。不开 diff 编辑器、没有「改动」筛选;打开的预览标出这个文件相比 PR 目标分支改了什么,见「代码差异」。
- 能审查的列只有一条规则、写在一处(
reviewRules.ts的REVIEW_COLUMNS:先「审查」、再「进行中」):弹框列哪些、选中 / 刷新 / 重开窗口时还能不能审查都按它。 - 选工单:视图标题栏第一个按钮(
view/title的navigation@1,$(git-pull-request),命令superpowers.codeReader.pickReviewIssue)。点开 VS Code 顶部的快速选择(window.createQuickPick):标题选工单 · 看板「审查」「进行中」列,占位输入工单号或标题筛选(按第一行筛)。工单马上列出来,各 PR 改了几个文件随后一项项补上,都补完才停止转圈(busy):- 工单:看板上现在摆着的,不再加载一遍(整个看板重新加载要翻每张工单的评论,几百张工单时很慢,哪个请求卡住就一直转)。面板这次会话里加载过就用它最近一次加载的(
KanbanWebviewPanel.issuesOnBoard→snapshotIssues()),否则用上次存下的看板快照(workspaceState的superpowers.kanbanSnapshot,重开窗口时看板先画的也是它);二者都是看板加载时算好的(state JSON 定列、看板的范围、叠好本机记录)。快照里 PR 号是字符串("454"),和Issue.pr的类型一致,按正整数认。 - 列哪些(
reviewCandidates):只要 Gitea 工单(PR 在 Gitea 上);「审查」列的都列(没 PR 的也列,见下);「进行中」列的只列已经有 PR 的——没 PR 的还在开发,没东西可看,列出来只会添乱。 - 分组:按列用快速选择的分隔行(
QuickPickItemKind.Separator,写列名)分成两组,先「审查」再「进行中」,没有工单的列不出分隔行;组里次序同看板列(工单号大的在前)。 - 看板还没加载过(两样都没有):占位换成
看板还没加载过,先打开一次看板,不转圈。两列都没有能审查的工单:占位换成看板「审查」「进行中」列里没有能审查的工单。 - 快照可能落后于看板上后来的挪动:刚挪进这两列、看板还没重新加载过的工单要等看板刷新后才列出来;选中时会重读这张工单(见下),已经挪走的不会进审查。
- 每项第一行
#<工单号> <标题>,后面淡色PR #<PR 号>(description);第二行(detail)<文件数> · 本机有工作副本或<文件数> · 本机没有工作副本,选中后拉取 PR 分支。文件数先写正在读改动文件数…,读到换成改了 N 个文件(listPullRequestFiles,各项一起读),没读到写读不到改动文件数、原话记进日志。每补一项整组换一次items,光标按工单号留在原来那项上。本机有没有工作副本按盘上现在的看。 - 「审查」列里 state JSON 没有 PR 号的工单也列出来,第二行只写
没有关联 PR;选中它只提示#<工单号> 没有关联 PR,什么都不变。 - 当前工作区没有 Gitea 仓库、没配 token:关掉弹框,报错。
- 日志(Superpowers 输出通道,来源
codeReader):列了几张、数据来自看板这次加载的还是存下的快照;哪张的文件数没读到和原话;进审查的每一步、不进的原因、出错。
- 工单:看板上现在摆着的,不再加载一遍(整个看板重新加载要翻每张工单的评论,几百张工单时很慢,哪个请求卡住就一直转)。面板这次会话里加载过就用它最近一次加载的(
- 选中后进审查:先重读这张工单(看板的单张加载
loadSingleIssue,弹框里的是看板上摆着的,可能旧了),确认它还在「审查」或「进行中」列、有 PR,PR 还开着(没了、合并了、关了都不进,提示原因),再读 PR 改过的文件、备好工作副本,都好了才换树。这期间视图上转进度(withProgress的viewId),标题下面那行写正在做哪一步(正在读取 PR #447 改过的文件…、正在拉取 PR #447 的分支…)。出错:报错,树不换(原来在审查别的工单就还是那张)。 - 工作副本(PR 最新的代码):
- 本机有这张工单的工作副本就用它:看板记着的 worktree(
worktreePath,盘上还在)优先,其次是之前审查它时建的。用之前先git fetch origin refs/pull/<PR 号>/head(Gitea 给每个 PR 留的引用),能不丢东西时快进到 PR 最新的提交(headSha):有没提交的改动(未跟踪的文件不算)、本地另有 PR 之外的提交时照原样用;拉取、快进出错也照原样用,提醒一声。目录必须是工作副本的根,否则不动(git -C会找到外面的仓库上去)。 - 没有就拉 PR 的分支(同上的
refs/pull/<n>/head)建一个:和实施的 worktree 同一个目录模板(插件设置里的 worktree 目录,$feature_name换成review-<工单号>),停在 PR 最新的提交上、不挂分支(git worktree add --detach),不和实施用的feature/…分支抢名字。不写进工单的本机记录:看板上删 worktree 会连带删工单的 feature 分支,审查建的这个不该走那条路。
- 本机有这张工单的工作副本就用它:看板记着的 worktree(
- 审查时的树:根只有一个,是工作副本,名字照写工作区文件夹的;下面只有 PR 改过的文件和通往它们的目录,目录层级照留、全部展开,每层排法同普通的树。节点照旧用
resourceUri(工作副本里的文件),点文件和平时一样打开(受支持的语言用预览),打开的就是 PR 最新的代码。 - 改动的颜色:
FileDecorationProvider挂在这些文件的 Uri 上,颜色用 VS Code 源代码管理的主题色——修改gitDecoration.modifiedResourceForeground徽标M,新增gitDecoration.addedResourceForeground徽标A,删除gitDecoration.deletedResourceForeground徽标D;目录不上色。Gitea 的状态added→ A,deleted→ D,别的(changed、改名的renamed在新路径上、copied……)→ M。编辑器标签页上打开的这些文件也带着,退出审查就清掉。 - 删掉的文件:列出来但打不开(没有命令),悬停提示
PR 里删掉了。原型把名字划掉,VS Code 的树行没有删除线,这是和原型的有意差异,只靠红色和D。 - 标题栏:视图
description写审查 #<工单号>(标题后面淡色),message写#<工单号> <标题> · PR #<PR 号> · 改了 N 个文件;上下文键superpowers.codeReader.reviewing为真时出「显示全部」($(close),navigation@2,命令superpowers.codeReader.showAllFiles,命令面板里也只在审查时有),点了回到工作区文件树。真实的视图没有新建按钮,原型里被「显示全部」换掉的那两个不用管。 - 行上的按钮:「复制」照旧(相对路径相对工作副本的根,就是仓库里的路径);注释按钮审查时不出(
when带!superpowers.codeReader.reviewing):注释记在工作区文件夹里,工作副本不是工作区文件夹。 - 刷新:审查时重读工单和 PR 改过的文件,工作副本照上面的规则更新;工单两列都离开了(「审查」↔「进行中」之间挪动不算)、PR 不能看了就提示原因、回到工作区文件树。
- 记住:选中的工单号存
workspaceState(superpowers.codeReader.reviewIssue),重开窗口后视图第一次露出来时接着审查(没打开侧栏不去拉);工单两列都离开了、没了 PR、PR 没了 / 合并了 / 关了,悄悄回到工作区文件树并忘掉;读的时候出错,报错后同样回去。 - 代码:
review/。纯函数(改动状态、改动的文件树、能不能审查、标题的字、弹框的各项)在changedFiles.ts、reviewRules.ts、pickList.ts;看板数据在reviewSource.ts(看板上摆着的工单由看板给:panel/kanbanSnapshot.ts的boardIssues;仓库和 token 与看板「改动」tab 共用gitea/repoContext.ts);工作副本在reviewWorktree.ts(git 操作在git/worktree.ts);着色在changeDecorations.ts;弹框、进出审查、记住在reviewMode.ts。文件树只多一个「换成这些文件」的入口(showChanges)。
编辑器右上角切换
- 受支持语言的文件:普通编辑器里标题栏显示「预览」,预览里显示「编辑」。点一下在同一个标签页位置换成另一种:用 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)。
预览(代码块树)
-
每一句是一块:导入、函数、类、枚举、异常、类型、字段、常量、变量、条件、否则、循环、尝试、捕获、最终、返回、语句、文件开头的说明(种类「注释」);Shell 脚本另有解释器、选项、分情况、命令、管道(见「Shell 脚本」);Dockerfile 另有解析指令、阶段、执行、复制、设置、标签、健康检查、启动(见「Dockerfile」)。
-
嵌套的块放在父块下面、往右缩进;每一层左边一条竖线(颜色
--vscode-editorIndentGuide-background)。 -
块的组成(从左到右):折叠箭头(只有有子块的块才显示,其余占位对齐)、概念图标、内容、右上角一排图标按钮。
-
概念图标(原型
kindBadge、画板「Prototype / 块种类图标」):块左边横排这块认出来的概念,各一个图标、不带字,和一行字等高(20px 的框,图形 16px,lucide 的线宽 2.2),间距紧。每个图标悬停自己的名字(data-tooltip,见下「悬停提示」);整排是一个role="img",读屏把名字用顿号连起来念。和折叠箭头、内容同一行顶对齐。webview 不自己用种类查图标:图标名由宿主放进预览数据的概念列表(BlockView.concepts),webview 只按名字画(kindIcons.tsx的KIND_ICON_COMPONENTS)。一个概念(
BlockConcept):稳定 id、悬停名、图标名(只能从KIND_ICON_NAMES里挑)、颜色。列表按这个顺序,同一个 id 只留第一个:- 语言的种类,就是这块原来的种类(类、函数、变量……)。图标是内置种类的(
contract.ts的KIND_ICONS),名字是翻译插件给kind值起的(见「筛选」;没有翻译插件时是种类 id)。不另填颜色:图标按 id 用 CSS 的--cr-kind-*(见「颜色」)。 - 语言插件另声明的概念(
LanguageAdapter.concepts,可选)。认到才出现(比如异步、抽象)。不填就只有种类那一个。宿主不按语言写判断。 - 库规则认领成自己的卡片时,上面的留下,不换掉。规则的种类追加在后面:id 是
RuleCard.kind,名字、图标、颜色是badge的。规则还可以再带认出的概念(RuleCard.concepts)。筛选的kind、卡片边框色仍是规则的种类(badge.color),不因为多了图标就改。认成现成属性表块的(RuleFacts,枚举)没有 badge,概念就是换成的那种块的语言种类。
每种内置种类由插件定图标、颜色、名字。同一类语义共用一个颜色,图标区分具体是哪种:
种类 图标 种类 图标 种类 图标 import 导入 importfield 字段 tagtry 尝试 shieldfunction 函数 square-functionconst 常量 lockcatch 捕获 shield-alertclass 类 boxvariable 变量 variablefinally 最终 shield-checktype 类型 bracesif 条件 splitreturn 返回 corner-down-leftenum 枚举 list-orderedelse 否则 corner-down-rightstatement 语句 codeexception 异常 triangle-alertloop 循环 repeatcomment 注释 message-square-textinterpreter 解释器 terminaloptions 选项 sliders-horizontalcase 分情况 signpostcommand 命令 square-terminalpipeline 管道 square-terminaldirective 解析指令 terminalstage 阶段 layersrun 执行 square-terminalcopy 复制 copysetting 设置 sliders-horizontallabel 标签 taghealth 健康检查 activitystart 启动 playtrait 特质 networkimpl 实现 square-functionmodule 模块 layersattribute 编译属性 sliders-horizontal库规则的种类用规则给的图标(
KindBadge.icon),跟在语言的种类图标后面:SQLAlchemy 数据表database、数据类package、Pydantic 数据模型braces(和类型块同一个图标,颜色不同)、Celery 应用server-cog、定时任务calendar-clock、FastAPI 接口globe、FastAPI 路由器network、React 组件atom、状态toggle-right、引用anchor、数据源download、派生值sigma、副作用zap、事件处理函数mouse-pointer-click、渲染输出layout-template、serde 可序列化braces、thiserror 错误triangle-alert、axum 接口globe、sqlx 查询database、tauri 桌面命令square-function。数据类写了frozen=True再加一把锁(lock,悬停「不可修改」)。实现块、特质块再由规则rust.impl、rust.trait换成卡片,左边因此有两个「实现」「特质」图标。这组图标由插件自己定义,是一个封闭的集合:契约里的
KIND_ICON_NAMES(上表 26 个——命令、管道、执行共用一个,解释器、解析指令共用一个,选项、设置、编译属性共用一个,字段、标签共用一个——加库规则用的 14 个(React 的组件、状态、引用、数据源、派生值、副作用、事件处理函数、渲染输出的 8 个也在里面),lucide 的 kebab-case 名字),类型KindIconName是它们的联合,概念的icon只能写其中一个,写别的是类型错误。内置种类 → 图标名在contract.ts的KIND_ICONS(宿主写进概念列表)。webview 给每个名字配一个逐个 import 的 lucide 图标组件(kindIcons.tsx的KIND_ICON_COMPONENTS,打包只带这些,预览脚本约 157 kB);webview 的KindIconName照契约手抄,kindIconNames.test.ts核对两边一样。规则要新图标就把名字加进这组、在 webview 配上图标,没有按任意名字找图标的路。 - 语言的种类,就是这块原来的种类(类、函数、变量……)。图标是内置种类的(
-
右上角的图标按钮(原型组件
Reader / BlockTools,普通块、属性表块、库规则的卡片共用):只有图标、不带字,名字写在悬停提示和aria-label里;整排是role="group"(aria-label="块操作")。最前面是「按注释规范重写」(注释图标,块还没有注释时叫「按注释规范写注释」,重写过、记录收着时叫「展开重写记录」;不能写注释的块没有,见「重写注释」),然后是「问 AI」(sparkles 图标,点开 / 收起块下面的问答区,展开时带按下的底色、aria-expanded;文件不在工作区文件夹里时没有,见「问 AI」),再是「编辑 / 预览」切换:预览时铅笔「编辑」,编辑时眼睛「预览」、带按下的底色(--vscode-toolbar-activeBackground)。按钮宽 24px、高 20px(和种类图标同高),图标 14px、descriptionForeground色;悬停--vscode-toolbar-hoverBackground,键盘聚焦描 1px--vscode-focusBorder;Tab 能走到,回车 / 空格切换。 -
悬停提示(整个插件的网页只走这一条路,预览和看板共用同一个组件):带提示的元素只写
data-tooltip="提示"(名字照旧写在aria-label里,只有图标的按钮必须有aria-label),页面根上挂一个TooltipHost(webview-ui/src/components/ui/tooltip.tsx)统一弹,不用原生title;提示里的换行照样换行(看板摘修复弹框里「迁移」列出的几个文件一行一个);注释、回答里 Markdown 链接的标题([文字](网址 "标题"))也写成data-tooltip。网页的 TSX 里写原生title属性是 lint 错误(vscode/eslint.config.mjs),只有iframe这种必须带标题的元素例外,组件自己叫title的属性不算。原因:macOS 上 VS Code 带的 Electron(1.139 是 Electron 43)只在鼠标压着带title的元素自身边缘时才弹原生提示,停在它里面的图标、文字上不弹(electron/electron#49843,Electron 38 起),种类图标 20px 的框几乎被 16px 的图形占满,基本弹不出来;样子也是系统的。样子照 VS Code 的悬停框:--vscode-editorHoverWidget-background底、--vscode-editorHoverWidget-border边框、--vscode-editorHoverWidget-foreground字、0 2px 8px --vscode-widget-shadow阴影,11px 字,圆角 4px,最宽 22rem;优先在元素上方,上方放不下翻到下方,水平对着元素中间,贴边留 8px,不接鼠标。鼠标停 500ms 才弹,在同一个元素里移动不重新计时,已经弹着时移到另一个带提示的元素上立刻换;移开、移出页面、按下鼠标、按任意键(含 Esc)、滚动就收,收了之后鼠标不离开这个元素不再弹。键盘把焦点移到带提示的元素上(:focus-visible)立刻弹,焦点离开就收;提示不是元素自己的名字(aria-label或它的字)时用aria-describedby挂到元素上,读屏当描述念。 -
种类图标和底色按种类区分(颜色见「颜色」);文件开头的说明(种类「注释」)虚线框。
-
「注释」一行:讲这块的普通注释不单独成块,是这张卡片上的一行(见「注释挂到它讲的块上」)。属性表卡片(语言的、翻译插件排的、库规则的卡片都一样,宿主统一插,规则、翻译插件不用管)插在第一行(名称这类讲它是谁的一行)后面,有说明(值是 Markdown 的那一行)时插在说明后面,所以是「名称、说明、注释、别的」;一行译文的卡片(条件、循环、语句、调用、返回……)排在译文、摊开实参的表、说明的下面,同属性表的一行(左边「注释」、右边 Markdown)。名字是翻译插件给种类 comment 起的名字(中文直译「注释」)。几段注释(中间隔着空行、或前后两处)各是一段 Markdown,照源码顺序,空行隔开。按「注释的 Markdown 排版」显示(仓库路径链接、不带路径的引用、仓库图片都照常能点),标差异时同属性表的行逐词标。这一行上不放按钮,「按注释规范重写」排在卡片右上角那排按钮里(见「重写注释」)。
-
内容两种状态,每块单独切:
- 预览:函数、类、枚举、异常、导入、常量、变量、解释器、选项、解析指令、阶段、健康检查、启动块是属性表(见下节),文件开头的说明是 Markdown(见「注释的 Markdown 排版」),其余块是一行译文;按词着色,等宽字体(中文部分用系统中文字体回退)。一行译文里有跨几行写的字典 / 列表时,拆开的各项排在译文下面(见「字典 / 对象字面量、跨几行写的列表」),有跨几行写的调用时,摊开实参的表排在译文下面(见「调用与构造」);块带说明(Python 字段的属性 docstring)时,说明按 Markdown 再排在下面;块挂着注释时「注释」一行排在最下面。
- 编辑:这一块在源码里的完整代码(按语法树着色):挂在它上面的注释、头部连同各子孙块、收尾括号,可以直接改,改完按这整段写回;蓝色边框。按钮换成眼睛「预览」;Esc 也回到预览。
-
复合语句(函数、条件、循环……)块的预览只讲它自己的头部那几行(主体交给子块);收尾括号、空行不成块。挂在块上的注释算在块的完整范围里(写在块上面的从注释开头算起,挂在这一层最后一块上的、子块交上来的到注释结尾):差异、跳过去、钉住卡片、总览条、「只看改动」、按行找块都按它,注释那几行改了标的是它挂着的那块。函数、类的说明(JSDoc / docstring)也算在函数、类块的头部里;Python 常量、变量、字段的属性 docstring 算在它那一块里(中间隔着的空行也是)。
-
编辑范围是块的完整范围(
blockView.ts的blockSpans,和语言无关):从头部和挂在它上面的注释里最靠前的开头,到头部、整条语句、挂在它上面的注释、各子孙块(连同它们的注释)里最靠后的那个字符。else、elif、catch 这类展平成兄弟块的子句,整条 if / try 按各块的开头(连同挂在它上面的注释)切开:每块到下一块开头之前,最后一块到语句结尾(Shell 的fi归它);} else {写在一行时两块都含这一行。 -
库规则认领的块(数据表卡片等,见「库规则」):种类的图标、颜色和名字由规则给,内容是规则给的属性表(宿主再插「注释」一行);编辑范围同别的块,是原块整条语句(类就是整个类)连同挂在它上面的注释,「编辑」时整段一起改。规则没并进卡片的子块(方法、别的成员)不管写在类主体哪儿(两个字段中间也算),都照源码顺序是卡片的子块,点它们的「编辑」只改它们自己连同子孙那几行。
-
同一行不会同时在两处能改:一块在编辑时它的子孙都不画(代码都在它的编辑框里),回到预览就照原样出来;点一块的「编辑」时,编辑范围和它重叠的别的块(祖先、子孙、共用一行的兄弟块)退出编辑。跳到定义的目标块正被盖住时闪盖住它的那块。
-
折叠:函数、类、枚举、异常块和库规则的卡片打开时默认收起子块(属性表照常显示),其余块默认展开;用户点过折叠箭头的块按他点的,记在视图状态里(见「预览数据与消息」)。
-
顶部筛选栏:见「筛选」;翻译下拉框(多于一种翻译时才显示)也在栏里。
-
解析失败:显示错误原因,并提示用「编辑」查看源码。
筛选
预览顶部钉住一条筛选栏,按块的属性藏掉一部分块(原型画板「筛选 · 默认 / 按访问级别 / 按种类」)。
- 三层分工,webview 不认识任何语言:
- 语言插件给每块报和语言无关的属性「属性 = 值」,都是稳定的英文 id,不放显示文字。通用词表由契约定(
COMMON_ATTRIBUTES):kind(块的种类)、access(public/private/protected,语言可以加自己的值)、async、abstract、typeOnly('true'/'false')。通用属性由宿主从块的 kind 和 facts 推出来(FunctionFacts.access/async、ClassFacts.access/abstract、EnumFacts.access、ExceptionFacts.access、ImportFacts.typeOnly、VariableFacts.access),只有这一条路,语言插件不用另报;facts 里没填的(语言没有这个概念,例如 JavaScript 没有抽象类、仅类型导入)块就没有这个属性,不是填「否」。语言自己的属性(Rust 的unsafe = 'true')、facts 表达不了的通用属性值(Kotlin 的access = 'internal')填在块的attributes里,和推出来的合在一起,同一属性以它为准;kind总是块的种类。 - 翻译插件给属性和值起名(
Translator.attributeNames:access→ 访问级别、public→ 公共、kind的function→ 函数、布尔值 → 是 / 否),和属性表里的属性名、值是同一套叫法(中文直译里属性表的「访问级别」「异步」「抽象」「仅类型」行和筛选共用一份表);种类图标的悬停提示也取它。库规则的卡片自己报的属性(RuleCard.attributes,FastAPI 接口的httpMethod)由规则起名(LibraryRule.attributeNames,用词归规则、按翻译插件的显示语言挑):宿主先取翻译插件给的名字,没给的再取规则给的(attributes.ts的mergeAttributeNames)。没起名的显示 id。 - webview 只按属性 id / 值 id 筛,显示翻译插件给的名字。
- 语言插件给每块报和语言无关的属性「属性 = 值」,都是稳定的英文 id,不放显示文字。通用词表由契约定(
- 下拉框:这份文件里有两种以上值的属性各一个(只有一种值的不给——筛它等于按种类筛);只有一种值、但这个值正被去掉的也给,不然藏起来的块在栏上找不到原因。排列:通用属性按契约的顺序(种类、访问级别、异步、抽象、仅类型),语言自己的属性按在文件里第一次出现的先后排在后面。下拉菜单顶上一行「全选 · 取消全选」,分隔线下面是勾选列表,列出这份文件里出现的值(按第一次出现的先后)和各有几块(所有层的块都算,例如「私有 3 / 公共 3」)。
- 全选 / 取消全选:把这份文件里这个属性出现的值一起勾上 / 去掉,和逐个勾一样记下来;别的文件才有的值记着的去掉状态不动(筛选整个工作区共用)。取消全选后有这个属性的块全藏起来,没有这个属性的照常显示;菜单不关,可以接着勾。
- 种类「注释」只剩文件开头的说明:挂在块上的注释跟着它的块显示、藏起来,按种类去掉「注释」只藏文件开头的说明(文件里没有时这个值不出现)。
- 规则:同一属性里勾着的值满足一个就显示,不同属性要同时满足;块没有某个属性就不受那个属性的筛选影响(条件、返回块没有访问级别,按访问级别筛时照常显示);块被筛掉,连同它的子块一起藏,不留空位,层级竖线跟着变短;子块都被筛掉的块不给折叠箭头。
- 汇总:什么都没筛掉时右边写「共 N 块」(N 是整份文件所有层的块数);筛掉了写「已隐藏 N 块 · 清除」,N 只数直接被筛掉的块,跟着藏起来的子块不算;点「清除」整份筛选清空。页面上一块都不剩时写「所有块都被筛选隐藏了。」
- 下拉框上的字:没去掉值时是属性名;去掉了(边框换
--vscode-focusBorder)勾着一种写「访问级别:公共」,几种写「种类:已选 11 种」,一种都不勾写「种类:未选」。 - 记住:记的是每个属性去掉的值(
{ 属性 id: 值 id[] }),所以文件里第一次出现的值默认显示。整个工作区共用一份(不按文件),存在扩展端的workspaceState(superpowers.codeReader.filter),重新加载窗口后还在;一个预览改了,所有开着的预览跟着变。某个属性在这份文件里没出现就没有它的下拉框,记着的去掉的值照样对它出现的文件起作用。 - 跳到定义到被筛掉的块:临时显示目标和通向它的路径(被筛掉的祖先块下面只露出这条路径,目标自己的子块照常按筛选显示,函数照样能展开看主体),本来被筛掉的块右边带小提示「已被筛选隐藏,临时显示」,照常展开、滚过去、闪。筛选一变,或文档刷新(正在编辑的块和它的祖先除外,不能打着字就消失)就回到筛选状态。
- 折叠、跳到定义、属性行 id、滚动位置恢复照旧;被筛掉的块不在页面上,按行滚动(从文本编辑器切过来)落到它前面最近的一块。视口顶部按筛选栏下沿算:记位置、恢复、滚过去都让开筛选栏。
- 样子:筛选栏钉在预览顶部(
position: sticky,块从它下面滚过去),横贯编辑区、下边一条--vscode-panel-border、底色--vscode-editor-background,里面的内容和代码块同宽对齐:「≡ 筛选」(宽 52px)、每个属性一个下拉框(宽 150px、高 24px,--vscode-dropdown-background/--vscode-dropdown-border/--vscode-dropdown-foreground)、右边汇总(descriptionForeground,「清除」--vscode-textLink-foreground)、再右边翻译下拉框;放不下就换行。下拉菜单盖在代码块上面:宽 200px,--vscode-editorWidget-background、--vscode-editorWidget-border、--vscode-widget-shadow阴影;顶上「全选」「取消全选」两个链接(--vscode-textLink-foreground,间距 12px),下面 1px 分隔线(--vscode-menu-separatorBackground),列表太长时只滚列表、这一行不动;每项勾选框(勾着是--vscode-focusBorder底)、值、右边块数(--vscode-editorLineNumber-foreground);悬停、键盘高亮--vscode-list-hoverBackground。 - 键盘和读屏:下拉框按钮
aria-haspopup="listbox"、aria-expanded;菜单role="listbox"、aria-multiselectable,每项role="option"、aria-selected表示勾着。按钮上 ↓ / 回车 / 空格打开(↑ 打开并停在最后一项),焦点落在列表上;列表里 ↑ ↓ Home End 移动,空格 / 回车勾选;Shift+Tab 回到「取消全选」「全选」(普通按钮,回车 / 空格触发),Tab 再回到列表;Esc 关掉并回到按钮;焦点离开下拉框(Tab 出去、点外面)就关。鼠标点选项、全选 / 取消全选时焦点留在列表上,方向键接着能用。
代码差异
审 AI 写的代码时,从只列改动的文件的地方(侧栏「未提交的更改」一栏、「选工单」审查)点开,预览的卡片标出这个文件相比改之前加了什么、改了什么、删了什么(原型插件菜单「代码差异」:画板「代码差异 · 全部 / 只看改动 / 标记说明」)。从主栏、收藏栏点开,点链接、跳定义这些别处打开的预览和平时一样,不标。
- 对比基准(
diff/diffBase.ts的DiffBase,previewDiffBases按文件在哪、这个标签页是从哪儿打开的给):- 从侧栏「未提交的更改」一栏点开的、文件在工作区的仓库里有没提交的改动(
workspaceOrder,审查的工作副本不算):对比上一次提交(HEAD),文案「对比:上一次提交」。从主栏、收藏栏、「了解一块代码」栏点开的不比;点仓库链接、⌘ / Ctrl + 点击跳到定义、点调用树、侧栏「问 AI」新开的预览也不比。 - 从哪儿打开的是打开请求自带的(
editor/previewEditor.ts的OpenRequest.from:uncommitted/other):只有侧栏各栏点开文件带上,文件行的打开命令带上这一栏的from(explorer/fileTree.ts的FileOpenCommand),PreviewEditorProvider.open把请求交给新建的预览;打开的是已经开着的预览时交给切到的那个,以最后一次从侧栏哪一栏点开为准(从「未提交的更改」点开就比,再从主栏点开同一个文件就不比)。跳定义、点链接、点调用树、「问 AI」只是去看一眼,不带from:新开的预览不比,打到已经开着的预览照旧;只切标签页也不变。 - 编辑器右上角「编辑」再切回「预览」照切走前的比(
editor/switchEditor.ts的EditorSwitch:切到文本编辑器时按编辑器组和文件记下预览是从哪儿打开的,切回时带上;那个文本编辑器标签页关了就忘掉,之后再点「预览」算新开的、不比)。 - 页面把它记进 webview 状态,VS Code 重启后恢复的标签页没有打开请求,随
ready报回来,照关掉前的比;页面也没记过的(新开的、VS Code 自己换编辑器打开的)不比。 - 审查工单、文件在这张工单的工作副本里(不管从哪儿打开):对比 PR 目标分支与 PR 头的合并基(Gitea PR 的
merge_base,没给时退回目标分支的提交,同看板「改动」tab)上的版本,改名的取改名前的路径(reviewBaseFile);文案「对比:PR 目标分支 <base 分支名>」(PR 的base.ref)。合并基是 PR 头的祖先,拉 PR 的提交时已经在本机,直接在工作副本里取。 - 取旧内容:
git/fileAt.ts的fileAtRef(git ls-tree看有没有这个文件、git show <提交>:<路径>取内容,走runGit);基准里没有这个文件(未跟踪、新加的、PR 里新加的、仓库还没有提交)不逐张标绿,顶部一条横幅「新文件,全部是新增」,后面淡色写原因(「还没被 git 跟踪,上一次提交里没有这个文件」「上一次提交里没有这个文件」「PR 目标分支 master 里没有这个文件」「仓库里还没有提交」)。
- 从侧栏「未提交的更改」一栏点开的、文件在工作区的仓库里有没提交的改动(
- 什么时候比(
diff/previewDiff.ts的PreviewDiff,每个预览一个):每发出一份文档(编辑、保存、块编辑写回、换翻译)之后按眼下的基准重新比;基准挪了(盯着的 git 状态变了,比如提交后 HEAD 动了;审查进 / 重读工单)、开着的预览又被点开换了从哪儿打开的,打开着的预览也重新比。预览先照常画文档,差异算好了再补发(diff消息),打开不变慢。取过的旧内容按基准的 key(提交 + 路径)记着,编辑时不用每次问 git;旧版本每次用同一个语言插件、翻译插件、库规则重新排成视图数据,规则查定义、调用的种类用这份文档眼下查到的答案,两边同样的写法翻得一样。取不到旧内容、旧版本解析失败时记日志、不标。 - 块怎么对上(
diff/blockDiff.ts,纯函数):同一父块下的兄弟块对齐——有名字的块(defines:函数名、类名、只声明一个变量时的变量名、规则卡片讲的名字)按「种类 + 名字」配,同名的照先后,挪了位置也配得上;没名字的块(语句、调用、注释……)以此为锚,最长公共子序列找出种类、源码都没变的,夹在相邻两个锚中间的按先后找种类相同、内容最像(按词比,相似度到一半)的配成修改。配不上的旧有新无算删除、旧无新有算新增;改名算一删一增。配上的块整段源码(去共同缩进)一样算没变,否则算修改,接着比子块;自己的内容改了、或源码变了子块却都没变的,是「自己改了」(上一处 / 下一处、总览条停在它上面)。 - 卡片里标什么(
diff/viewDiff.ts,纯函数):修改的卡片在渲染用的视图数据这一层比(翻译好的属性表、一行译文、摊开实参的表、说明),所有语言、库规则的卡片走同一条路:- 属性表按属性名配行;配上的行值变了:着色的词按词(一段着色的词一个)对齐,大部分没变时逐截标(新加的一截绿底、删掉的一截红底划掉),没变的不到四成时整段写成旧值(红底划掉)紧跟新值(绿底);说明这类 Markdown 按词比,中文按字、标点各算一个(
diffWordsWithSpace)。带子行的行子行改了时只标子行,值(「3 个」「字典 2 项」这类汇总)跟着变、不另标——原型里参数多了一行时「输入参数」的个数也没标。 - 子行(参数表、导入项、字典的各项、摊开实参的表)按第一列的字配对:多出来的(新参数)整行淡绿底、左边 +,没了的放回原来的位置、整行淡红底划掉、左边 −;配上的逐格比。有加行、删行的表格每行第一格前多一个记号位(+ / −,没变的空着),列照样对齐。
- 多出来的属性行、没了的属性行同样整行淡绿 / 淡红底,+ / − 画在行左边外侧。删掉的内容取自旧版本,词去掉源码下标(不能跳定义、不参与高亮)。
- 属性表按属性名配行;配上的行值变了:着色的词按词(一段着色的词一个)对齐,大部分没变时逐截标(新加的一截绿底、删掉的一截红底划掉),没变的不到四成时整段写成旧值(红底划掉)紧跟新值(绿底);说明这类 Markdown 按词比,中文按字、标点各算一个(
- 删掉的块:放回原来的位置——旧版本里它前面最近一个配上的兄弟所配的新块后面,再越过紧跟着的新增块(新增的在前、删掉的在后,原型里删掉的
_is_owner在新增的renew和release之间);排在最前面的在这一层最前面。子块里删掉的挂在父块下面,整块删掉的子块不再单列。 - 统计「新增 N · 修改 N · 删除 N」只数最外层带标记的块:新增块的子块、修改块里改动的子块不再数(原型的 2 / 3 / 1)。
- 页面(原型组件
Reader / DiffBar、Reader / UnchangedStrip、Reader / OverviewRuler、Reader / NewFileBanner、Diff / 已删除):- 卡片左边外侧 3px 的改动色条(和卡片等高,间隔 5px):新增绿、修改蓝、删除红,没变的留一条透明的,同一层照样对齐;子卡片各带各的。悬停色条提示「新增 / 修改 / 删除」。
- 新增的卡片连同子卡片底色淡绿(10%),最外面那张左上角压着「新增」小标(绿底白字 10px)。修改的卡片只有蓝条,里面按上面标。
- 删除卡片:红虚线边、淡红底、半透明,没有编辑、问 AI 按钮;默认收成一行「已删除 · <种类> <名字>」(名字是属性表第一行的值,或译文的第一行;种类名取旧版本的筛选属性名),点箭头或这一行展开,下面是旧内容、整张划掉;子块不画。按块筛选同样藏。
- 标了改动的块默认展开(函数、类本来默认收起子块),用户点过的照他的。
- 筛选栏第二行(高 30px,只有有差异数据时才有):比较图标、「对比:<基准>」、「·」、统计(各按改动色,没有的那种不写,都没有写「没有改动」),右边「只看改动」开关、上一处 / 下一处(↑ ↓,悬停提示、
aria-label)。基准里没有这个文件时只写基准。 - 只看改动:每一层连着没变的块折成一条「… N 块没变 点开」(虚线框),被筛掉的块不算也不打断,删掉的块在哪儿就在哪儿断开;改过的块照常,它下面没变的子块同样折;点开一段就恢复这一段(筛选一变重新折)。开关按工作区记住(
BlockFilter,workspaceState的superpowers.codeReader.onlyChanges),所有预览共用;跳到定义落在折起来的块上时临时露出来。 - 上一处 / 下一处:在各处改动(新增的最外面那块、自己改了的修改块、删掉的块,照页面先后,被筛掉的不算)之间跳,展开折住它的块,滚到筛选栏下面(外层还钉着的卡片下面,见「钉住卡片」)、闪几下;跳完没滚过页面就接着刚跳到的那处走,自己滚过了按视口顶上那块的行找;到头绕回另一头。
- 总览条:预览右边缘 14px 宽、从筛选栏下面贴到底,每处改动按它在新文件里的行画一段刻度(修改块只画它自己那几行,删掉的块一条 2px 细线),点刻度跳过去(同上一处 / 下一处)。
- 文档刚改、差异还没重算时:块按记号认,没动过的块照样带色条;卡片里的标记按下标认,只在页面上正是差异算的那一版时画。
- 颜色:取 VS Code 编辑器行号旁的改动色带
--vscode-editorGutter-addedBackground/modifiedBackground/deletedBackground(主题没给时用 Dark Modern 的默认值),行内增删的字用--vscode-diffEditor-insertedTextBackground/removedTextBackground;整行底色是改动色的 14%,新增卡片底色 10%。
属性表(函数、类、枚举、异常、导入、常量、变量块)
卡片外观(折叠箭头、种类图标、底色、悬停高亮、「编辑 / 预览」切换)和其他块一样,只是内容换成一张属性表:左列属性名(界面字体、descriptionForeground 色,宽 76px,这张表里有更长的属性名时整列按最长的那个放宽,值照样对齐在同一处,属性名不折行:shell 的「管道里任一步失败都算失败」),右列是值(等宽字体,按词性着色,长了换行),两列间距 12px,行高 20px。「输入参数」「导入项」和值是字典 / 对象字面量、跨几行的列表的「值」「初始值」下面展开子行,属性名前带折叠箭头(图标、大小、颜色同块的折叠箭头;点箭头或属性名切换,默认展开,折叠后值「N 个」「字典 N 项」照常显示,只收起子行);子行往右缩进 5px,左边一条 1px 竖线(editorIndentGuide-background1);输入参数例外,是一张表格(见下)。字典 / 对象的子行分键 / 值两列,键列宽 150px(编辑区窄时按 28% 收),值列占剩下的。子行下面还能再展开一层(嵌套的字典 / 对象、列表):带下一层的子行第一列前面有折叠箭头(点箭头或第一列切换,默认展开),同一层里其余子行在同样的位置留空、第一列对齐;下一层同样往右缩进 5px、左边一条竖线,竖线对着上一层的箭头,每层自己的两列对齐。同一层里没有带下一层的子行时不留箭头的位置(和导入项的子行一样)。列表的一项带实参表(整项是调用)时,表排在这一项下面、同样往右缩一层、左边一条竖线,总是展开、没有折叠箭头。表格的行(实参表)的值拆成列表时,各项排在最后一格里、文字下面,同上面的子行(项下面的下一层照样能折叠),表格的行自己不折叠。
输入参数的表格(原型 paramRows):往右缩进 5px,第一行是表头「名称 | 类型 | 默认值」,下面每个参数一行。列宽按整列的内容定,webview 不认是哪种表格(库规则的字段表同样排,见 tableTemplate):整张表格一个网格、各行借用它的列,前几列宽裕时放到整列不换行,最后一列(默认值、说明)分剩下的、至少留 min(20%, 8em);放不下时先让最后一列换行,编辑区再窄前几列平分剩下的宽度、在格子里换行(长名字从中间断开),不撑出卡片、不藏掉哪一列。格子左右留 8px、上下 2px,长类型、长默认值在格子里换行,不挤掉别的列。外框 1px、圆角 4px,格子之间、行之间 1px 分隔线,颜色都是主题的表格分隔色(--vscode-tree-tableColumnsBorder,主题没给时退到 --vscode-editorWidget-border);表头垫主题的表头底色(--vscode-keybindingTable-headerBackground,比编辑区深一档或浅一档),列名界面字体 12px、descriptionForeground 色。没有默认值的格子留空。格子可以是 Markdown(参数的「说明」、「异常」表的两列、对不上的参数的说明):按「注释的 Markdown 排版」显示(界面字体、注释色),着色的词照旧排在等宽字体的段落里,差异逐词标。属性行跟着的说明(PropertyRow.note,输出参数的 @return)接在值后面,同一行放得下就并排,剩下的地方不到 12 个字宽时换到下一行。折叠「输入参数」那一行时整张表收起;子行 id、跳到定义闪「它那个参数」、高亮名字的各处出现都和列表一样,闪的是表格里那一行(行边框和相邻行共用,改在行里面描一圈聚焦色)。哪些子行排成表格由翻译插件定:给了列名(PropertyRow.columns)的排成表格,没给的(导入项、字典 / 对象的各项)照旧是列表,webview 只看有没有列名。
| 种类 | 行 |
|---|---|
| 函数 | 名称(函数色;匿名函数写「匿名」)/ 说明(有正文才显示:JSDoc 去掉 /**、*/ 和每行开头的 *,docstring 去掉引号;再去掉共同缩进和首尾空行,拆掉块标签(见「注释的 Markdown 排版」),剩下的正文按 Markdown 渲染)/ 对不上的参数(说明里有 @param 的名字在输入参数里没有时才显示:N 个,展开成表格「名称 | 说明」,名称是 hint 色、悬停「没有这个参数」)/ 访问级别 / 异步(是 / 否)/ 装饰器(有才显示,源码原样照抄、按源码着色,多个换行)/ 输入参数(N 个 或「无」,展开成表格,列名 名称 | 类型 | 默认值,每个参数一行:名称 | 类型(没标注写「未标注」)| 默认值(译文,没有留空);任何一个参数对上了 @param 时多一列「说明」(Markdown,没对上的格子留空),按名字对,*args / **kwargs / ...rest 去掉前缀对)/ 输出参数(返回类型的译文,没标注写「未标注」;写了 @return 时类型后面跟着这段说明,Markdown、注释色)/ 异常(写了 @throws 才显示:N 个,展开成表格「异常 | 触发条件」,一条一行,都是 Markdown,异常名能点,见「注释的 Markdown 排版」的引用) |
| 类 | 名称(类型色;匿名类 export default class {} 写「匿名」;赋给变量、类字段的类表达式写变量名、字段名)/ 说明(有正文才显示,规则同函数)/ 访问级别 / 抽象(是 / 否,只有 TypeScript / TSX 有,看 abstract class,TypeScript 的类都有这一行;JavaScript / JSX 没有)/ 装饰器(同函数)/ 类型参数(有才显示:TypeScript <K, T extends X = Y> 每个参数的译文,逗号隔开,如 K, T 限定为 X = Y;Python 3.12 的 class G[T] 同样)/ 父类(有才显示:TypeScript extends 后面的表达式连着泛型实参的译文,如 Base<字符串>;Python 类名后面括号里的参数列表照写的顺序逐项翻,关键字参数、*bases 也在这一行,如 Base, metaclass=ABCMeta;逗号隔开)/ 实现的接口(有才显示:TypeScript implements 后面每个类型的译文,逗号隔开)/ 构造参数(说明里写了 @param 才显示:类卡片没有构造参数表可对,照写成「名称 | 说明」表)/ 异常(同函数) |
| 枚举 | 名称(类型色)/ 说明(有说明才显示,规则同函数)/ 访问级别(同类)/ 装饰器(有才显示,同函数)/ 继承(有才显示:Python 类名后面括号里照写的父类,如 str, enum.Enum;TypeScript 没有)/ 成员(N 个 或「无」,展开成表格,列名 成员 | 值 | 说明,每个成员一行:成员名(variable 色)| 值(写了值是值的译文;没写值时是语言算出来的值,TypeScript 数值枚举自增的 0、1、2……,算不出来和 Python 的 auto() 写「自动」,这两种按 hint 着色)| 说明(成员的注释、JSDoc、属性 docstring 的原文用空格连起来,comment 色;一个成员都没有说明时没有这一列);讲的定义是成员名,跳到成员名只闪那一行)。原型「枚举」一节(ENUM_SAMPLE、enumCard) |
| 异常 | 名称(类型色;匿名类写「匿名」)/ 说明(有说明才显示,规则同函数)/ 访问级别(同类)/ 装饰器(有才显示,同函数)/ 继承(有才显示:照写的父类,同类的「父类」,如 Exception、errors.HttpException<字符串>)/ 属性(有类属性才显示:N 个,展开成表格,列名 属性 | 类型 | 值 | 说明,每个属性一行:属性名(variable 色)| 类型(写了标注是它的译文,一个属性都没标注时没有这一列)| 值(写了值是值的译文,没写留空)| 说明(同枚举成员的说明,一个属性都没有说明时没有这一列);讲的定义是属性名)/ 构造参数(写了构造函数才显示:表格同函数的「输入参数」,Python 去掉 self,类说明里的 @param 同函数对进「说明」列、对不上的排在说明下面;讲的定义是参数名;没写构造函数时同类,说明里的 @param 照写成表)/ 异常(同函数)。见「异常」 |
| 导入 | 来源(TypeScript 是字符串字面量原样,如 'node:fs',字符串色;Python 是点分模块名,如 fastapi、.models,类型色)/ 仅类型(是 / 否,只有 TypeScript / TSX 有,看 import type;JavaScript / JSX 没有)/ 导入项(N 个 或「无」,每项一行:name、name 作为 alias;默认导入写「默认导出 作为 x」,* as x 写「全部 作为 x」,Python from m import * 写「全部」,Python import a.b as c 写「整个模块 作为 c」,一条 import a, b 导入多个模块时写模块名) |
| 常量 / 变量 | 名称(解构声明写整个模式,如 { p, q })/ 说明(有说明才显示:Python 的属性 docstring,规则同函数;一条语句一个,排在第一个名称后面)/ 访问级别 / 类型(写了标注才有)/ 值(常量)或初始值(变量):初始化表达式的译文,没有写「无」;是字典 / 对象字面量时见下面「字典 / 对象字面量」。一条语句声明几个变量(let a = 1, b = 2)就重复几组 |
访问级别(公共 / 私有 / 受保护):
- TypeScript 模块里的声明(函数、类、变量):
export了是公共(含export default),顶层没export是私有;嵌套在函数体、条件等语句块里的没有这一行。 - TypeScript 类成员(方法、函数值字段、类表达式字段):写了
public/private/protected按写的,#name是私有,都没写是公共。 - Python:只有模块顶层和类主体里的声明(函数、类、变量)有;名字单下划线开头(
_x、__x)是私有,前后双下划线(__init__)是公共,其余公共;解构赋值里的名字全是私有才算私有;赋值目标是属性、下标(self.x = …)时没有这一行。
值里的表达式只翻落在块头部里的那一段:const v = useMemo(() => { … }) 这种值带语句块主体回调的块,回调主体是子块,值只翻到 useMemo(() =>,和块头部一致。
字典 / 对象字面量、跨几行写的列表(常量、变量的值,含 Python 类主体里不带注解的类属性;实参的值、一行译文的块里跨几行写的也拆,见本节最后;参数默认值照旧是一行译文):
-
值写项数:Python
字典 N 项、TypeScript对象 N 项(按 hint 着色),各项展开成子行,照源码顺序,注释不算一项。 -
每项两列:键 | 值。键照源码写法:字符串键带引号、按字符串着色,名字键按名字着色,计算属性名
[k]照常翻;值同常量、变量的值:字典 / 对象、跨几行的列表接着拆,跨几行写的调用排成「新建 / 调用 …」、实参表排在这一项下面,别的照常直译(等待 …、空数组……)。 -
值又是字典 / 对象字面量时,它这一项的值也写项数,它的各项展开成下一层子行,一层层往下套。
-
展开:Python
**other、TypeScript...other是一项,键写「展开」(hint),值是展开的表达式。 -
TypeScript 简写
{ a }:键a,值a。对象里的方法、getter、setter:键是方法名(函数色),值是方法头部(到主体开始之前)的译文,如异步 load(id: 字符串): Promise<无>、get size()。 -
空字面量不展开:值是一行
空字典/空对象(一行译文里也这样翻,同空数组)。 -
as const、satisfies包着的对象({ … } as const)不算字面量本身:写在一行里的照旧一行译文,跨几行写的是埋在式子里的字面量(见下面「一段排一个」),写成「(对象 N 项) 断言为 常量」、各项照常拆。 -
列表(Python 的列表 / 元组 / 集合、TypeScript 的数组、Rust 的数组 / 元组)跨几行写时同样拆:值写
列表 N 项(元组、集合、数组各用自己的叫法,展开的也算一项),每项两列:第 N 项(hint,只数不是展开的项)| 值;*other、...other左边写「展开」,值是展开的表达式。列表的项不讲哪个定义(不标 defines)。写在一行里的列表照旧一行译文([a, f(b)]),空的照旧空列表。[值; 个数]不是一项项写出来的,不拆。 -
列表的一项整个是一次调用时,不管写成一行还是几行都排成「新建 / 调用 被调用的名字 · N 个参数」(见「调用与构造」),它的实参表排在这一项下面、往右缩一层,同一个列表里样子一致;别处写在一行里的调用照旧。项的值是字典 / 对象照字典拆,是跨几行的列表接着拆,一层层往下套。
-
实参的值是跨几行写的列表、字典 / 对象时也这样拆,各项排在实参表值那一格里、文字下面(见「调用与构造」),同一张实参表里跨行的列表、字典样子一致;实参里写在一行里的字典 / 对象照旧一行。
-
一行译文的块(
return [...]、yield {...}、表达式语句、+=赋值、字段……)里跨几行写的列表、字典 / 对象同样拆:译文里字面量的位置写「列表 N 项」「对象 N 项」(措辞同常量、变量的值),各项排在这一行下面(TextTranslation.items→ 视图数据的items),排法同变量初始值下面的子行(往右缩一层、左边一条竖线,项下面的下一层能折叠,项带的实参表排在这一项下面);这一行下面不再另挂实参表,各项各管各的。 -
一段排一个:和「调用与构造」的跨几行调用是同一条规则——一行译文的块、常量 / 变量的值、实参表里每个值各是一段,各自排自己里面先遇到的(先外后里)那一个跨几行写的调用或字面量,同一段里后面再有的照常一行。所以
return f([⏎ a,⏎ b⏎ ]) + 1排的是外面的 f(「返回 (调用 f · 1 个参数) + 1」,列表拆在它实参表那一格里);return [⏎ make(…)⏎ ] + [⏎ b⏎ ]排的是先遇到的列表,后面那个照旧一行;字面量后面还紧跟着同一个式子的别的部分时整个括起来,同调用:「返回 (列表 1 项) + [ b ]」,常量 / 变量的值也一样(LIMITS = [⏎ 1,⏎ ] + DEFAULTS→ 「值 (列表 1 项) + DEFAULTS」)。值整个是字典 / 对象字面量的写在一行里也拆,只在常量、变量的值和它们一层层的项里(实参的值、一行译文里写在一行里的照旧一行)。 -
用户那段代码读出来是:
初始值 列表 2 项 第 1 项 新建 Filter · 3 个参数 参数 值 field "user_id" operator FilterOperator.EQ value current_user.id 第 2 项 新建 Filter · 3 个参数 参数 值 field "archived_at" operator 如果 archived 则 FilterOperator.NE 否则 FilterOperator.EQ value 空同一段写成
return [...]时一行译文是「返回 列表 2 项」,下面「第 1 项 / 第 2 项」同上、各带自己的实参表。 -
语言插件给各项(
LanguageAdapter.collection:节点 →CollectionFacts),翻译插件按字面量的节点类型查叫法(ZhTable.collections)、定怎么拆;webview 只画子行和表,不认语言。
异常
原型没画,照枚举卡片的画法。异常是语言自己的概念,不是某个库的写法,所以和 TypeScript 的 enum 一样由语言插件认,报成契约里的异常块(ExceptionBlock,种类 exception,facts 是 ExceptionFacts);Python、TypeScript 同一种块、同一张属性表、同一个筛选种类,用词归翻译插件,webview 只按种类上色、默认收起。
- 认哪些类(
languages/shared.ts的withExceptions,各语言给一份ExceptionRules):父类里有一项是异常类就是异常类。- Python:父类名(
X、pkg.X取 X,Base[T]取 Base;关键字参数、*bases、调用不看)是自带的异常类(builtins 里的全部:BaseException、Exception、ValueError、KeyboardInterrupt、StopIteration……,含各种Warning),或以Error、Exception、Warning结尾。 - TypeScript / JavaScript:
extends后面的类名(X、ns.X取 X,泛型实参不看;Mixin(Base)这类调用不看)以Error或Exception结尾。自带的Error、TypeError、RangeError、AggregateError……都以 Error 结尾,DOMException以 Exception 结尾,不用另列。 - 两种语言都算:父类是同一文件里认出来的异常类,按名字认,不论写在前面还是后面、嵌套在哪,一层层往下传(
class Deeper(Late)、class Late(Base)、class Base(Exception)三个都是)。
- Python:父类名(
- 按命名习惯认的部分和它的限制:父类从别的文件导入时语言插件看不到那个文件,只能按名字认(PEP 8 要求异常类名以 Error 结尾,
AppException这种以 Exception 结尾的也常见)。所以父类名不合习惯(从别处导入的NotFound、Forbidden当父类)的认不出来,照常是类块;名字合习惯却不是异常的父类(普通类起名RenderError)会被当成异常。同一文件里按名字认,不看作用域(同名的两个类当成一个)。 - 类属性(进「属性」表):Python 是类主体里给一个名字赋值的语句(
x = 1、x: int = 1、只写注解的x: int),连着赋值(a = b = 1)、解构赋值不算;TypeScript / JavaScript 是类字段(public_field_definition/field_definition,含 static;值是带语句块主体的函数、类表达式的照常是方法、类块)。说明同枚举成员:讲它的注释(挂在它上面的)、这一行行尾的注释、Python 紧跟在后面的属性 docstring,原文用空格连起来。类 docstring 里谷歌写法的Attributes:一节照原文留在「说明」里,不拆到各属性的说明格子。 - 构造参数:Python 是
__init__的参数去掉第一个(self;第一个是*args这类时不去),TypeScript / JavaScript 是constructor(...)的参数(参数属性public code?: string也是一个参数);写了几个取最后一个(重载的实现写在最后)。*、/分隔符同函数的输入参数,不算参数。类里没写构造函数(继承父类的)时没有这一行。 - 并进属性表的子块:各个类属性(讲它的注释挂在它上面,跟着并进来,是这个属性的说明);构造函数、方法、别的语句照常是子块(不管写在哪,讲它们的注释是它们卡片的「注释」一行),默认收起。异常块的头部(编辑范围)是整个类:从说明、装饰器开始,到类连同各子块的最后一行,「编辑」时整段一起改,盖住留下的子块(同库规则的卡片)。
- 语言插件报的已经是异常块,库规则只认类块,所以异常类不交给库规则:
@dataclass装饰的异常类照样是异常卡片,@dataclass写在「装饰器」一行,注解的字段在「属性」表里。 - 跳到定义:类名是块自己的名字(闪整块),属性名只闪「属性」表里那一行,构造函数的参数只闪「构造参数」表里那一行(构造函数自己的「输入参数」也标了同一个参数,页面上先找到的是卡片里这一行);中文名(
Annotated[…, Comment("…")]、行尾# @Comment …)照常换属性格子里的名字,行尾记号不进说明。
样例(fixtures/appExceptions.py 是 luffy core/exceptions/schemas.py 的快照,测试里逐字核对;str | None 译成「字符串 | 空」):
名称: AppException
说明: 应用异常基类. …(docstring 原文,含 Attributes: 一节)
访问级别: 公共
继承: Exception
属性: 3 个
status_code | 整数 | 500
error_code | 字符串 | "INTERNAL_ERROR"
message | 字符串 | "服务器内部错误"
构造参数: 3 个
message | 字符串 | 空 | 空
detail | Any | 空
error_code | 字符串 | 空 | 空
名称: NotFound
说明: 资源不存在 (404).
访问级别: 公共
继承: AppException
属性: 3 个
status_code | 404
error_code | "NOT_FOUND"
message | "资源不存在"
AppException 的 __init__、to_dict 是卡片的子块。fixtures/commerceExceptions.py(luffy commerce/exceptions.py 的快照)里 16 个类都是异常,只有说明的是名称 / 说明 / 访问级别 / 继承四行,写了 __init__ 的多一行构造参数。
推导式(写在表达式里的遍历)
原型画板「遍历 · 推导式」。Python 的列表 / 集合 / 字典推导式、生成器表达式(含 async for、几节 for、if、套着的推导式)不管出现在哪(一行译文、属性表的值 / 初始值 / 默认值、字典项的值),都原地换成一个小框:
- 框:嵌在前后文字里(
display: inline-block),框头是「循环」块的种类图标(KIND_ICONS.loop,luciderepeat,12px)和结果类型,后面是重排后的译文。边框、底色、框头字的颜色都取「循环」块的种类色(--cr-kind-loop:边框 45%、底色 14%,圆角 4px,左右留 5px);框头界面字体 11px、500 字重。上下边框算在 20px 行高里,框和前后文字基线对齐、不把一行撑高;框里套的框行高再减 2px。整个框放不下时挪到下一行,比一行还长时占满一行、在框里换行。 - 结果类型:生成器 / 列表 / 集合 / 字典(
[…]列表、{…}集合、{k: v …}字典、(…)生成器)。 - 框里按读的顺序重排:每节
for写「遍历 <来源> 的每个 <变量>」,紧挨着的几节for用顿号接着写(「遍历 xss 的每个 xs、xs 的每个 x」),async for写「异步遍历」(同步、异步换了就另起一句「,异步遍历 …」),if在它所在的位置写「,仅当 <条件>」(后面再有for就另起一句「,遍历 …」),最后「,取 <值>」,字典是「<键>: <值>」。来源、变量、条件、值照常直译,里面还有推导式就是框里的框。「遍历」「的每个」「仅当」「取」「异步遍历」按关键字着色,逗号、顿号、空格按普通文字。 - 括号:框代替了推导式自己的方括号 / 花括号 / 圆括号,不再写出来;Python 调用唯一的参数是生成器时只写一对括号(
f(x for x in y)),这对括号属于调用,照常写在框外,紧贴着框:"".join(框)。生成器是几个参数之一时(f(a, (x for x in y)))括号是它自己的,不写。 - 框里的名字照样带源码下标:⌘ / Ctrl + 点击跳到定义、单击高亮各处出现都照旧;跳到定义的目标只显示在框里(推导式的变量)时,目标是框所在的块。
- 样例(必须逐字一致;框写成
⟦类型|框里的文字⟧):
| 源码 | 译文 |
|---|---|
return "".join(secrets.choice(ALPHABET) for _ in range(length)) |
返回 "".join(⟦生成器|遍历 range(length) 的每个 _,取 secrets.choice(ALPHABET)⟧) |
return [u.id for u in users if u.is_active] |
返回 ⟦列表|遍历 users 的每个 u,仅当 u.is_active,取 u.id⟧ |
return {u.id: u for u in users} |
返回 ⟦字典|遍历 users 的每个 u,取 u.id: u⟧ |
- TypeScript 没有这种写法,不涉及。几种读不成遍历的写法(语法树认、Python 3 不认的
for x in a, b,写到一半缺了变量 / 来源 / 条件 / 值的)照常逐词直译。
调用与构造
原型画板「调用 · 构造」(code.js 的「调用与构造」一节,嵌套见 nestedArgRow、NESTED_CALL_SAMPLE)。实参多的调用一般跨几行写,挤成一行译文读不下去;把实参摊成一张表,实参的值又是这样的调用时在格子里接着摊。
- 哪些调用:实参列表(括号连同里面,
CallFacts.argumentList)跨了几行的调用,而且整个落在要翻的这一段里(块头部;TypeScriptdescribe('x', () => { … })这种主体是子块的调用不算)。一行写完的调用照旧一行译文,不问定义。链式调用跨了几行、每段的括号都写在一行里的(items\n .filter(…)\n .map(…))也不算;式子跨了几行、里面的调用各自写在一行里的(bool(x)\n and Path(x).is_file())是式子,也不算。拆开的列表里整项是调用的例外,写成一行也排(见「字典 / 对象字面量、跨几行写的列表」)。 - 一段排一个:一行译文的块、常量 / 变量的值、实参表里每个值各是一段,各自排自己里面遇到的第一个(先外后里)这样的调用或跨几行写的字典 / 列表(见「字典 / 对象字面量、跨几行写的列表」);同一段里后面再有的照常一行译文(挤成一行时圆括号贴着里面的内容:
f(\n x\n)写成f(x),方括号、花括号照旧空一格)。被调用的表达式里的调用(get_driver(\n m\n).query(\n …\n)排的是外面的 query)照常一行。 - 一行:
[等待 ]新建 / 调用 被调用的名字 · N 个参数。「等待」就是await照常的译法(在调用前面),「新建」「调用」按声明关键字着色;被调用的名字照常翻、带源码下标(⌘ / Ctrl + 点击、单击高亮、中文名都照旧),前面几节(对象、模块)按变量着色,最后那个名字新建时按类型着色、调用时按函数着色;「· N 个参数」按说明词着色,N 数的是表里的行。 - 埋在式子里:调用后面还紧跟着同一个式子的别的部分(
.model_dump()、[0]、or x;后面是右括号、逗号、分号、冒号或什么都没有的不算),这一行整个括起来:「(新建 X · 3 个参数).model_dump()」「(调用 make · 1 个参数) 或者 fallback」,表照样排在这一段下面。前面有东西的(fallback or make(\n…\n))不括。 - 新建还是调用:写法说明了的看写法(
CallFacts.construct:TypeScript 的new X(…)是新建,不带 new 的是调用,都不问定义);Python 新建对象和调用函数写法一样,翻译插件问SourceDocument.calleeKind:名字定义成类(含异常、枚举)是「新建」,函数、别的、查不到、还没查到都是「调用」。两种语言都用「新建」这个词。 - 查定义(宿主,
calleeKinds.ts+editor/definitions.ts的definitionKind):排视图数据是同步的,先按已经查到的排(第一次都按调用画),记下问了哪些调用;发出预览后问语言扩展被调用的名字(CallFacts.name)定义在哪(和 ⌘ + 点击同一套),打开定义所在的文件、用它那种语言的语言插件分块(readDefinitionKind:自己的名字盖住定义处的最里层那块),几处定义取第一处认得出的。查到的和已知的不一样就重排、重发。按被调用的表达式原文记(user_service.update_user),同一份文档里写法一样的按一处查;文档改了先用上一版查到的,每个版本查一次,语言扩展还没起来(没答)的照 RETRY_DELAYS 隔一会儿再查(和库规则查名字的定义同一套DeferredAnswers)。被调用的不是名字(handlers[k](…))的不查,按调用算。 - 表:两列「参数 | 值」,每个实参一行、照源码顺序。按名字传的(Python
name=value)写名字,按变量着色、带源码下标;按位置传的写「第 N 个」(说明词,只数按位置传的);*args、...args写「展开」,**kwargs写「展开关键字」(说明词)。值照常一行译文(名字带下标);唯一的实参是生成器时(sum(\n x for x in xs\n)),共用的那对括号属于调用,格子里只有框。表的样子同输入参数表(PropertyTable:表头、圆角外框、格子之间分隔线,第一列按内容定宽)。 - 嵌套:值(一段)里有跨几行写的调用时,值那一格写它的一行,它的实参表排在同一格里、文字下面(
PropertyItem.table),一层层往下套,多深都一样;套着的表不能折叠,总是展开。值是跨几行写的列表、字典 / 对象时值那一格写「列表 N 项」「字典 N 项」,各项排在同一格里、文字下面(PropertyItem.items),项里整项是调用的接着带自己的实参表;实参里写在一行里的字典 / 对象照旧一行。每一层都和最外层一样:先按「调用」画、查到定义再刷新成「新建」,名字带源码下标,跳到定义、高亮各处出现、中文名照旧。排版:每套一层往右缩 5px(PropertyTable外面那层的左边距),第一列按内容定宽、窄了能收,长的值在格子里换行,不出横向滚动条。 - 放在哪:
- 变量、常量块:初始值 / 值那一行写上面那一行字,表是这一行展开的子行(
PropertyRow.columns = ['参数', '值']),带折叠箭头、默认展开。值是字典 / 对象字面量、跨几行写的列表时按项展开(见「字典 / 对象字面量、跨几行写的列表」),里面的调用跟着各自那一项。 - 一行译文的块(返回、语句、字段……):译文里调用的位置换成那一行字(「返回 新建 UserDetailResponse · 11 个参数」),表排在译文下面(
TextTranslation.table→ 视图数据的table),总是展开;块带说明时说明排在表下面。这一段先遇到的是跨几行写的字典 / 列表时排的是它拆开的各项(TextTranslation.items),里面的调用跟着各自那一项。
- 变量、常量块:初始值 / 值那一行写上面那一行字,表是这一行展开的子行(
- 分工:语言插件报结构(
LanguageAdapter.call:被调用的表达式、最后那个名字、写法是不是新建、实参列表、各个实参),翻译插件定用词、挑哪个调用、怎么排;webview 只画表(PreviewTable),不认语言;跳到定义、高亮各处出现、中文名在表里照旧(renderedNames、reveal.ts都算上这张表、一行译文下面拆开的各项和一层层套着的表)。 - 样例(
fixtures/routes.py、paymentConsoleRoutes.py、chainedCalls.py是 luffy 的代码,calls.ts.txt、nestedCalls.ts.txt是本仓库的代码):
| 源码 | 预览 |
|---|---|
return UserDetailResponse(\n id=user.id,\n …(11 个)\n)(定义是类) |
一行 返回 新建 UserDetailResponse · 11 个参数,表第一行 id | user.id |
user = await user_service.update_user(\n db,\n user_id=user_id,\n …(10 个)\n) |
初始值 等待 调用 user_service.update_user · 10 个参数,表第一行 第 1 个 | db、第二行 user_id | user_id |
user = await user_service.get_user(db, user_id=user_id) |
初始值 等待 user_service.get_user(db, user_id=user_id)(一行写完,不展开) |
payload = await file.read() |
初始值 等待 file.read() |
const url = new URL(\n \…/comments`,\n)` |
初始值 新建 URL · 1 个参数,表 第 1 个 | \…/comments`` |
return concatAll(\n base,\n ...extra,\n) |
一行 返回 调用 concatAll · 2 个参数,表 第 1 个 | base、展开 | extra |
return PaymentCapabilityDiagnostic(\n …\n credentials_present=CredentialPresence(\n mp_app_secret=…,\n …(4 个)\n ),\n …(15 个)\n)(都定义成类) |
一行 返回 新建 PaymentCapabilityDiagnostic · 15 个参数;credentials_present 那一格 新建 CredentialPresence · 4 个参数,下面套它的 4 行表;其中 wechat_pay_private_key=bool(private_key_path)\n and Path(private_key_path).is_file() 是式子,照旧一行 bool(private_key_path) 并且 Path(private_key_path).is_file() |
self._admin_basic = base64.b64encode(\n f"…".encode()\n).decode() |
初始值 (调用 base64.b64encode · 1 个参数).decode(),表 第 1 个 | f"…".encode() |
result = await config.get_payment_driver(\n order.payment_method\n).query_refund_status(\n out_trade_no=…,\n out_request_no=…,\n) |
初始值 等待 调用 config.get_payment_driver(order.payment_method).query_refund_status · 2 个参数 |
context.subscriptions.push(\n treeView,\n commands.registerCommand('…', () => {\n …\n }),\n …\n) |
一行 调用 context.subscriptions.push · 4 个参数;第 2 ~ 4 个那一格 调用 commands.registerCommand · 2 个参数,下面套它的 2 行表 |
Shell 脚本
原型画板「Shell 脚本」(code.js 的「Shell 脚本」一节:SHELL_KINDS、SHELL_BLOCKS、shellBlock;README「Shell 脚本」)。样例是 luffy 桌面端的 apps/desktop/scripts/fetch-runtime.sh(复制成 fixtures/fetchRuntime.sh)。原型里的中文是手写的目标,插件读出来的逐行和它对得上;原型略去的 95–100、116–121 行也照样读。
- 哪些文件:VS Code 认成
shellscript的文件,.sh、.bash、.zsh,还有首行写着 shell 解释器的无扩展名文件(VS Code 自己按首行认)。sh、zsh 也按 bash 的语法读(tree-sitter-bash.wasm)。 - 分工和别的语言一样:语言插件(
languages/bash.ts)在 tree-sitter-bash 上报块和事实,不认识中文;翻译插件(translators/bashZh.ts,中文模板直译)定用词;一条命令怎么读交给命令规则(见「命令规则」),按命令名注册;webview 不认识 shell,只多认几种块的种类。 - 块(图标、颜色见「预览(代码块树)」「颜色」):
- 解释器(interpreter,属性表):首行
#!。「解释器」写程序名(bash),「怎么找」写按 PATH 去找(/usr/bin/env bash),写死路径的写写死的路径(/bin/sh -e)。 - 选项(options,属性表):一条
set -euo pipefail一块,每个开关一行:行名是开关的意思(出错就停、用到没设的变量就报错、管道里任一步失败都算失败、打印执行的每条命令……),值是「是 / 否」,后面淡淡写源码里的写法(-e、-o pipefail、+x)。set -- a b这种不是开关,照常是命令。 - 变量、常量(variable / const,属性表,同别的语言的变量块):赋值和
local/export/readonly/declare;local url="$1" sha="$2" out一句几个是一块,没给值的写「无」。常量是readonly的,和脚本顶层名字全大写、值里没有展开的。 - 分情况(case):每个情况都短(最多 3 句、每句一行写完、没有注释和嵌套的语句块)时整条一块,一行「按 x 分情况」,下面一张表「情况 | 做什么」,做的几句在格子里一句一行;否则头部是「按 x 分情况」,每个情况一块子块「情况:以 -apple-darwin 结尾」,要做的各句是它的子块。
- 函数(function,属性表):紧贴在上面的注释(中间没空行)是「说明」,不单独成块。shell 的函数没有参数表,参数按位置取:「输入参数」写「N 个,按位置」,表「位置 | 函数里叫」,函数里用
local x="$N"(或x=$N)起了名字的写名字,没起的写$N。「输出」是函数自己打印到输出的那几句(echo/printf,重定向到别处的不算;进 if、循环、分情况,不进$(…)、管道、嵌套的函数),同一文件里有$(函数名 …)时后面淡淡写「(调用处用 $(fetch …) 拿到)」。「返回」:写了return N的写那几个值,什么都不打印、也没写 return 的写「最后一句成不成功」。主体各句是子块,同别的语言的函数,默认收起。 - 命令(command):一条命令一行,由命令规则读(「删除 dest(连同里面的东西、不提示)」「下载 url 到 out.part(失败就报错、跟着跳转走、最多重试 3 次)」);没有规则的命令照抄命令名,后面的词照常翻(引号去掉,展开读成中文)。重定向统一读(不归命令规则):
>&2→「(打印到错误输出)」,2>/dev/null→「(丢掉错误输出)」,> f→「(写到 f)」,>> f→「(追加到 f)」,2>&1→「(错误输出并进输出)」,< f→「(从 f 读)」。用&&连起来的几条命令读成「a 然后 b」,a || b读成「a,失败就 b」。tree-sitter-bash 把a && b > f读成整串带着重定向,其实重定向只属于 b:语言插件把它算在 b 上(「a 然后 b(写到 f)」)。 - 管道(pipeline):
a | b | c读成「a → b → c」;最后一段是循环(find … | while read -r link; do … done)时读成「找出 dest 下面所有符号链接 → 逐行读进 link」,循环的主体是子块。 - 条件、遍历(if / else / loop):
if/elif/else同 Python(elif/else展平成兄弟块,读成「否则如果」「否则」)。条件:[ -f x ]→ 文件 x 存在,-d目录 x 存在,-e存在,-L是符号链接,-z/-n为空 / 不为空,-r/-w/-x可读 / 可写 / 可执行,-eq/-ne/-lt/-gt/-le/-ge等于 / 不等于 / 小于 / 大于 / 不大于 / 不小于;=/==/!=等于 / 不等于,[[ … ]]里右边是通配时读成「以 .sh 结尾」「不以 .sh 结尾」;!→ 不,&&/||→ 并且 / 或者。一句[ … ](或一串全是条件)自己就是一句语句时是条件块,读成「成立:…」;条件后面跟一句的([ -f x ] && rm x、cond || continue)读成「如果 文件 x 存在,删除 x」「如果不是〔cond〕,跳过这一个」。遍历:for f in "$gc"/git-*→ 遍历 gc/git-* 匹配到的每个文件 f,for x in a b→ 遍历 a、b 中的每个 x,for a in "$@"→ 遍历 全部参数 的每个 a;while cond→ 只要 cond 就一直重复,until cond→ 直到 cond 之前一直重复,while read -r x→ 逐行读进 x。
- 解释器(interpreter,属性表):首行
- 值和展开(一行译文、变量的值、表格里都一样):
$1→ 第 1 个参数,$0→ 脚本路径,$@/$*→ 全部参数,$#→ 参数个数,$?→ 上一条命令的退出码,$$→ 当前进程号;${x:-d}→ x(没设就用 d),第几个参数写「没给」:${1:-}→ 第 1 个参数(没给就是空);${x:=d}→ x(没设就设成 d),${x:?m}→ x(没设就报错:m),${x:+y}→ y(x 设了才有),${#x}→ x 的长度,${x#p}/${x##p}→ x(去掉开头的 / 开头最长的 p),${x%s}/${x%%s}→ x(去掉结尾的 / 结尾最长的 s),${x/a/b}→ x(其中第一个 a 换成 b),${x//a/b}→ x(其中 a 全换成 b),${x:1:2}→ x(从第 1 个字符起,取 2 个)。整个值是$(cmd)时读成「执行〔…〕的输出」,写在别的字中间的读成「〔…〕」;命令规则读出来是一个东西的直接写它:$(dirname "$0")→ 脚本所在目录。双引号字符串去掉引号,写死的部分按字符串色,展开的变量按变量色;全大写的名字(PY_VERSION、HOME)按常量色。 - 通配(分情况的情况、
[[ … ]]的右边):*.zip→ 以 .zip 结尾,*-x→ 以 -x 结尾,x*→ 以 x 开头,*x*→ 含有 x,别的写「匹配 …」;a | b→ a 或 b;分情况里单独一个*→ 其他。 - 照旧能用的:按种类筛选、单击名字高亮各处出现、⌘ / Ctrl + 点击跳到定义、中文名(行尾
# @Comment 安装目录)、文件注释、问 AI。VS Code 自己没有 shell 的定义、高亮提供者,所以同一文件里的名字由语言插件认(LanguageAdapter.symbols):函数的定义是函数名,用到的是同名的命令;变量的定义是赋值、local/export声明、for的变量、read读进的名字,用到的是$x、${x…};第几个参数、$?这类不算。跳到用到的那处之前最近的一处定义(同一个变量前后赋过几次值),前面没有就跳到第一处;高亮时定义处算写入、别处算读取。装了给 shell 提供定义、高亮的扩展时,先问它,它答不上才用语言插件认的。 - 样例(
fixtures/fetchRuntime.sh,translators/bashZh.test.ts的FETCH_READING逐行核对):
| 源码 | 预览 |
|---|---|
#!/usr/bin/env bash |
解释器 bash / 怎么找 按 PATH 去找(/usr/bin/env bash) |
set -euo pipefail |
出错就停 是 -e / 用到没设的变量就报错 是 -u / 管道里任一步失败都算失败 是 -o pipefail |
target="${1:-}" |
名称 target / 初始值 第 1 个参数(没给就是空) |
here="$(cd "$(dirname "$0")/.." && pwd)" |
名称 here / 初始值 执行 〔进入 脚本所在目录/.. 然后 打印当前目录〕 的输出 |
cache="${XDG_CACHE_HOME:-$HOME/.cache}/luffy-desktop-runtime" |
名称 cache / 初始值 XDG_CACHE_HOME(没设就用 HOME/.cache)/luffy-desktop-runtime |
PY_RELEASE=20260924 |
常量:名称 PY_RELEASE / 值 20260924 |
case "$target" in x86_64-pc-windows-msvc) … ;; *) echo "用法: …" >&2; exit 2 ;; esac |
按 target 分情况,表「情况 | 做什么」:x86_64-pc-windows-msvc | GIT_URL = …⏎GIT_SHA256 = …⏎PY_SHA256 = …、其他 | 打印 "用法: 脚本路径 <…>"(打印到错误输出)⏎退出,退出码 2 |
sha_ok() { [ -f "$1" ] && [ "$(shasum -a 256 "$1" | cut -d' ' -f1)" = "$2" ]; } |
名称 sha_ok / 输入参数 2 个,按位置:第 1 个 | $1、第 2 个 | $2 / 返回 最后一句成不成功;子块「成立:文件 第 1 个参数 存在 并且 〔算 第 1 个参数 的 sha256 → 按空格切,取第 1 段〕 等于 第 2 个参数」 |
# 下载 …⏎fetch() { local url="$1" sha="$2" out … echo "$out"; } |
名称 fetch / 说明 下载 (已缓存且校验通过就不重下), 回显本地文件路径 / 输入参数 2 个,按位置:第 1 个 | url、第 2 个 | sha / 输出 打印 out(调用处用 $(fetch …) 拿到) |
out="$cache/$(basename "${url//%2B/+}")" |
名称 out / 初始值 cache/〔取文件名 url(其中 %2B 全换成 +)〕 |
if ! sha_ok "$out" "$sha"; then |
如果 不 sha_ok out sha |
curl -fL --retry 3 -o "$out.part" "$url" |
下载 url 到 out.part(失败就报错、跟着跳转走、最多重试 3 次) |
case "$git_pkg" in *.zip) unzip -q … ;; *) tar -xzf … ;; esac |
按 git_pkg 分情况:以 .zip 结尾 | 解压 zip git_pkg 到 dest/git(不打印过程)、其他 | 解压 tar.gz git_pkg 到 dest/git |
*-apple-darwin)(情况里有注释、好几句) |
子块「情况:以 -apple-darwin 结尾」 |
find "$gc" -mindepth 1 -maxdepth 1 -type d ! -name mergetools -exec rm -rf {} + |
找出 gc 下一层的目录,除了 mergetools,逐个 删除(连同里面的东西、不提示) |
for f in "$gc"/git-*; do |
遍历 gc/git-* 匹配到的每个文件 f |
[ -L "$f" ] && [ "$(readlink "$f")" = git ] || continue |
如果不是 〔f 是符号链接 并且 〔读出 f 指向的路径〕 等于 git〕,跳过这一个 |
find "$dest" -type l | while read -r link; do |
找出 dest 下面所有符号链接 → 逐行读进 link(循环主体是子块) |
du -sh "$dest/git" "$dest/python" >&2 |
统计占用空间 dest/git、dest/python(按人读的单位、只看总数)(打印到错误输出) |
- 和原型不一样的地方(插件生成的,比手写的更一致):函数里的变量也是属性表块(同别的语言,不写成一行「局部 …」);
$N一律写「第 N 个参数」;选项的意思之间一律用顿号(「连同里面的东西、不提示」);[ "$(readlink "$f")" = git ]照结构读成「〔读出 f 指向的路径〕 等于 git」,不合写成「f 指向 git」。
Dockerfile
原型画板「Dockerfile」(code.js 的「Dockerfile」一节:DOCKER_KINDS、NEXT_DOCKERFILE、BACKEND_DOCKERFILE;README「Dockerfile」)。样例是 luffy 的 frontend/client-nextjs/Dockerfile 和 backend/docker/Dockerfile.backend(复制成 fixtures/clientNextjs.Dockerfile、fixtures/backend.Dockerfile);原型里的中文是手写的目标,下面的译文都由插件生成,逐行核对见 translators/dockerfileZh.test.ts 的 NEXT_READING、BACKEND_READING,原型略去的几段也照样读。
- 哪些文件:VS Code 认成
dockerfile的文件:Dockerfile、Dockerfile.*、*.dockerfile、Containerfile。语法包是 tree-sitter-dockerfile,@vscode/tree-sitter-wasm里没有,取自 npm 包tree-sitter-wasm(预编译、每个 wasm 都签过名、上游提交钉死),打包时和别的语法包一起复制进dist/tree-sitter/(见「解析运行时」)。 - 分工同别的语言:语言插件(
languages/dockerfile.ts)报块和 facts;翻译插件(translators/dockerfileZh.ts)定用词;RUN、shell 写法的 CMD / ENTRYPOINT、HEALTHCHECK 的检查命令是嵌着的 shell(EmbeddedCode),宿主交给 shell 的语言插件、翻译插件和命令规则读(见下「嵌着的代码」),Dockerfile 这边不再读一遍 shell;数组写法的命令(["node", "server.js"])不经过 shell,由 Dockerfile 语言插件报成命令(LanguageAdapter.command),同样交给命令规则。webview 不认识 Dockerfile,只多认几种块的种类。 - 块(图标、颜色见「预览(代码块树)」「颜色」):
- 解析指令(directive,属性表):文件最开头连着的
# syntax=…、# escape=…、# check=…,一条一行:「语法 docker/dockerfile:1.7(BuildKit 按这个版本读这份文件)」「转义字符 …」「检查 …」。别的注释、空行、指令之后的不算。 - 阶段(stage,属性表):每个 FROM 开一个阶段,到下一个 FROM 之前的指令、注释都是它的子块;第一个 FROM 之前的(全局的 ARG)照常是顶层的块。卡片:名称(
AS后面的名字;没起名的写「没起名,按序号 N 引用」)/ 说明(紧贴在 FROM 上面、中间没空行的几行注释,算在阶段的头部里)/ 基础镜像(「node,标签 22-alpine」,写了摘要的加「,摘要 sha256:…」,接着前面某个阶段建的写「阶段 builder」,写成构建参数的读成「BASE(构建参数)」)/ 平台(写了--platform才有)。阶段名能点:COPY --from=deps、RUN --mount=…,from=deps、FROM deps里的 deps ⌘ / Ctrl + 点击跳到那个阶段,单击高亮各处出现。阶段默认展开。 - 执行(run):RUN 里的 shell 由 shell 的语言插件分步(
LanguageAdapter.steps):只有一条命令时一行,就是那条命令读成的(「pnpm 装依赖(严格按 lock 文件、不跑安装脚本)」),一条循环、条件的子块归这个 RUN;只用&&连着的一串拆开,一条一步,头一行写「执行」,3 条以上加「(N 条命令,一条失败就停)」(原型 2 条时只写「执行」,10 条时带条数);分几句写的(heredoc 里一行一句)是「(N 条命令)」。续行的\去掉、各段拼起来读(RUN apt-get install -y \⏎ curl git);RUN <<EOT的 heredoc 正文当一段脚本读;管道照 Shell 写「a → b」;a && echo x > f && b这种整串带重定向的(tree-sitter-bash 这样读),重定向只算在 echo 那一步上。RUN 的选项写在括号里:--mount=type=cache,target=X→ 缓存目录 X 跨构建保留,type=secret,id=X→ 临时挂上密钥 X,type=ssh→ 转发 SSH 密钥,type=tmpfs,target=X→ X 用内存临时目录,type=bind(默认)→ 临时挂上 [阶段 F 的] 源 到 目标,--network=none / host / default→ 不联网 / 用宿主机的网络 / 用默认网络,--security=insecure / sandbox→ 不隔离权限 / 隔离权限,别的照抄。 - 复制(copy):「[从阶段 X | 从镜像 Y] 复制 源 → 目标(属主 u:g)」;源是
.写「整个构建目录」,目标是.写「./」;--chmod→ 权限,--parents→ 保留目录层级,--exclude→ 跳过 X,--link→ 单独成层。ADD:源全是网址写「下载」,全是本地的压缩包(.tar、.tar.gz、.tgz……,ADD 会自动解开)写「解压」,git 仓库写「拉取仓库」,--checksum→ 校验;COPY <<EOF /f写「写入 /f(内容写在下面)」。 - 变量(variable,属性表):ARG 是「构建参数 X / 默认值 …」,没写默认值的写「没写,构建时传」;ENV 是「环境变量 X / 值 …」。值里的
$X、${X}读成变量(带源码下标,全大写的按常量色),在它之前(同一阶段里,FROM 那一行看文件开头的)用 ARG 声明过的后面注「(构建参数)」,BuildKit 自动给的(TARGETPLATFORM、TARGETARCH……)注它的意思,${X:-d}注「没设就用 d」,${X:+d}注「设了就是 d」。 - 并块:连着写的同种指令(紧挨着下一行,中间没空行、没注释、没别的指令)并成一块。ENV、ARG 并成一个变量块:一行「环境变量 4 条连着写」(一条 ENV 写了几个的是「3 个」),下面一张「名称 | 值」(构建参数是「名称 | 默认值」)表,每行讲的定义是那个变量;COPY 从同一个地方复制的(都从构建目录,或
--from一样)并成一行「复制(N 条连着写,属主都是 u:g)」,下面一张「从 | 源 | 到」表(都从构建目录复制的没有「从」这一列,从阶段写「阶段 X」、从镜像写「镜像 名字:标签」),属主都一样才写在头一行,别的选项写在「到」那一格里;LABEL 并成一张「名称 | 值」表。ADD 不并(各条做的事不一样),带 heredoc 的 COPY 不并。 - 设置(setting):WORKDIR「工作目录 /app」,USER「之后以 nextjs 身份运行」(
u:g照写),EXPOSE「开放端口 80(udp)、443」,VOLUME「声明数据卷 /data、/logs」,STOPSIGNAL「停止容器时发 SIGTERM 信号」,SHELL「之后的 RUN、CMD、ENTRYPOINT 用 /bin/bash -o pipefail -c 执行」,MAINTAINER「维护者 …」,各一行。ONBUILD 是一行「以它为基础镜像构建时再执行」,包着的那条指令是它的子块。 - 标签(label):「标签」,下面一张「名称 | 值」表(一条 LABEL 写几个、连着写的几条都在表里)。
- 健康检查(health,属性表):间隔 / 超时 / 启动后宽限 / 宽限期里的间隔(时长写成「1 分 30 秒」「500 毫秒」)/ 失败几次算不健康(「3 次」),写了才有;检查命令照 Shell 读(shell 写法)或交给命令规则(数组写法)。
HEALTHCHECK NONE只有一行「检查 不做健康检查(基础镜像里设的也关掉)」。 - 启动(start,属性表):CMD 是「命令」,ENTRYPOINT 是「入口命令」,读法同检查命令;「写法」是「数组,不经过 shell」或「经过 shell」。
- 解析指令(directive,属性表):文件最开头连着的
- 选项多的命令(
uv sync带一长串选项):命令规则认出的选项有 4 个以上(同一个选项写几遍算一个),或同一个选项写了几遍时,这一行后面写「· N 个选项」,意思排成一张「选项 | 意思」表,写了几遍的并成一行、选项后面写「× 11」(--no-install-package × 11 | 不装这些包:agent-dsl、…);3 个以内、都只写一遍的照旧写在括号里。表的样子同调用的实参表(见「调用与构造」)。 - 照旧能用的:按种类筛选、单击名字高亮各处出现、⌘ / Ctrl + 点击跳到定义(阶段名、ARG / ENV 的变量,同一文件里由语言插件认,同 Shell;RUN 里 shell 自己的变量由 shell 的语言插件认)、中文名(紧挨在 ARG、ENV(一个变量的)、
FROM … AS上面一行、只写了# @Comment 名字的注释,这行注释算在那条指令的块里、不单独成块)、文件注释、问 AI。 - 和原型不一样的地方(插件生成的,比手写的更一致):
- 并成一块的 ENV 是属性表的一行「环境变量 4 条连着写」加表,不写成一行字「环境变量(4 条连着写)」。它还是变量块,每行能跳到定义。
uv sync是一行「uv 按 lock 同步依赖 · 4 个选项」加表,不写成「命令 / 选项」两行(同调用的「调用 X · N 个参数」)。curl没写-o时写「请求 url」(原型的 RUN 里写「下载」);. /etc/os-release写「读入 … 里的变量和函数」;echo "…" > f写「打印 "…"(写到 f)」(原型「写入 … 到 f(覆盖)」,重定向统一由翻译插件接上);echo 里的[signed-by=…]照源码写全。- 检查命令
curl -f … || exit 1照 Shell 读成「请求 …(失败就报错),失败就 退出,退出码 1」(原型「或者」)。 - 原型第 12、13 行(
COPY --from=deps …、COPY . .)分开画,所以 COPY 只在从同一个地方复制时才并。 - 后端文件开头三行注释紧贴着 FROM,读成这个阶段的「说明」(原型没画这三行)。
- RUN 拆开的各步是 shell 的块,种类照 shell 的(命令、管道、变量……),不都叫「执行」。
- 嵌着的代码(通用的做法,见「通用契约」的
EmbeddedCode):语言插件报出这份源码里嵌着的别的语言的代码(LanguageAdapter.embedded:装着它的节点、语言、源码里属于它的几段,去掉了外层的记号)。宿主按语言找注册的语言插件、选中的翻译插件和命令规则,只解析这几段(includedRanges,节点下标照旧是整份源码的),按LanguageAdapter.steps分步:只有一步时这一步的子块归嵌着它的块,几步时各步是它的子块,都用那种语言的翻译插件翻、那种语言的词性着色(外层的源码着色上面叠一层);翻译插件翻嵌着它的块、属性表的行时问SourceDocument.embedded,拿到各步读成的一行,连词、「N 条命令」由它自己定。那种语言没注册时照抄、没有子块。同一文件里的名字把嵌着的代码里认出的一并交给宿主。 - 样例(
fixtures/clientNextjs.Dockerfile、fixtures/backend.Dockerfile):
| 源码 | 预览 |
|---|---|
# syntax=docker/dockerfile:1.7 |
语法 docker/dockerfile:1.7(BuildKit 按这个版本读这份文件) |
FROM node:22-alpine AS deps |
名称 deps / 基础镜像 node,标签 22-alpine |
RUN corepack enable && corepack prepare pnpm@latest --activate |
执行,子块「开启 corepack」「准备 pnpm latest(并设为当前用的版本)」 |
COPY package.json pnpm-lock.yaml ./ |
复制 package.json、pnpm-lock.yaml → ./ |
RUN pnpm install --frozen-lockfile --ignore-scripts |
pnpm 装依赖(严格按 lock 文件、不跑安装脚本) |
COPY --from=deps /app/node_modules ./node_modules |
从阶段 deps 复制 /app/node_modules → ./node_modules |
COPY . . |
复制 整个构建目录 → ./ |
ARG DEPLOYMENT_ID ⏎ ENV NEXT_DEPLOYMENT_ID=$DEPLOYMENT_ID |
构建参数 DEPLOYMENT_ID / 默认值 没写,构建时传;环境变量 NEXT_DEPLOYMENT_ID / 值 DEPLOYMENT_ID(构建参数) |
RUN pnpm build |
pnpm 构建 |
ENV NODE_ENV=production ⏎ …(4 条) |
环境变量 4 条连着写,表 NODE_ENV | production、NEXT_TELEMETRY_DISABLED | 1、PORT | 3000、HOSTNAME | 0.0.0.0 |
RUN addgroup --system --gid 1001 nodejs && \ ⏎ adduser --system --uid 1001 nextjs |
执行,子块「新建系统用户组 nodejs(gid 1001)」「新建系统用户 nextjs(uid 1001)」 |
COPY --from=builder --chown=nextjs:nodejs …(2 条) |
复制(2 条连着写,属主都是 nextjs:nodejs),表 阶段 builder | /app/.next/standalone | ./ |
USER nextjs、EXPOSE 3000 |
之后以 nextjs 身份运行、开放端口 3000 |
CMD ["node", "server.js"] |
命令 node server.js / 写法 数组,不经过 shell |
LABEL maintainer="Cruldra" \ ⏎ description="…" |
标签,表 maintainer | Cruldra、description | luffy-agent-platform - … |
RUN apt-get update && \ ⏎ …(10 条) |
执行(10 条命令,一条失败就停),子块「更新软件包列表」「安装 ca-certificates、curl、gnupg、postgresql-client、git(不装推荐的包、不用确认)」「建目录 /usr/share/keyrings」「请求 https://…/ACCC4CF8.asc(不打印进度、失败就报错、跟着跳转走) → 转成二进制密钥 写到 /usr/share/keyrings/postgresql.gpg」「读入 /etc/os-release 里的变量和函数」「打印 "deb …"(写到 /etc/apt/sources.list.d/pgdg.list)」…「打印 pg_repack 的版本」「删除 /var/lib/apt/lists/*(连同里面的东西、不提示)」 |
COPY --from=node:22-slim …(2 条) |
复制(2 条连着写),表 镜像 node:22-slim | /usr/local/bin/node | /usr/local/bin/node |
COPY --chown=appuser:appuser …(12 条) |
复制(12 条连着写,属主都是 appuser:appuser),表「源 | 到」12 行 |
RUN uv sync --frozen --no-dev --no-install-project \ ⏎ --no-install-package agent-dsl \ …(11 个) |
uv 按 lock 同步依赖 · 4 个选项,表 --frozen | 严格按 lock 文件,不改它、--no-dev | 不装开发依赖、--no-install-project | 不装本项目、--no-install-package × 11 | 不装这些包:agent-dsl、… |
HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \ ⏎ CMD curl -f http://localhost:8000/health || exit 1 |
间隔 30 秒 / 超时 5 秒 / 启动后宽限 10 秒 / 失败几次算不健康 3 次 / 检查命令 请求 http://localhost:8000/health(失败就报错),失败就 退出,退出码 1 |
CMD ["uv", "run", "uvicorn", "luffy_agent.main:app", "--host", "0.0.0.0", "--port", "8000"] |
命令 用 uv 运行 uvicorn luffy_agent.main:app,监听 0.0.0.0:8000 / 写法 数组,不经过 shell |
注释的 Markdown 排版
- 用在:卡片的「注释」一行(
#、//、/* */、没挂成说明的 JSDoc、Python 当注释用的单独字符串)、文件开头的说明(同样的注释,和 Python 模块开头的 docstring、Rust 的//!)的预览,函数、类、常量、变量属性表的「说明」,参数表的「说明」列、「异常」表、输出参数后面的@return,字段译文下面的说明。视图数据里它们是{ markdown }(见「通用契约」的Markdown),webview 按数据决定怎么渲染,不猜。 - 原文:翻译插件去掉注释符号(
#、//、/**/和每行开头的*)、引号和中文名记号(SourceDocument.markers,见「中文名」),再去掉行尾空白、首尾空行和共同缩进,保留换行,不做别的转换。一个注释节点(块注释、docstring)的第一行紧跟在注释符号 / 引号后面,不参与算共同缩进(同 PEP 257);几条行注释连成的一块每行地位相同,第一行也算,缩进四格的代码块才留得住。 - 块标签:说明(
Translator.doc读的:函数、类这些定义的说明,字段、成员的说明,库规则取的说明)的原文按 KDoc 拆成正文和块标签(Doc,解析在docTags.ts,各卡片共用);「注释」一行和文件开头的说明照原文显示,不拆。- 认
@param、@return(@returns同义)、@throws(@exception同义)。一行(去掉开头空白)以标签开头算一条;后面接着的行(不是空行、不以@开头)续在这条后面(去掉开头空白、换行留着);空行结束这条,以@开头的行也结束上一条。围栏代码块里的行不认标签。 @param 名字 说明;JSDoc 的@param {类型} 名字 - 说明、@param [名字] 说明、@param [名字=默认值] 说明也认(类型、横杠、默认值去掉)。@return 说明、@returns {类型} 说明同样去掉类型。@throws 异常 触发条件:异常是一个名字、[Foo]、[说法](路径#Foo)或 JSDoc 的{Foo},只是名字的写成不带路径的引用[Foo]。几条@return用空行连成一段。- 正文是拿掉这几种标签后剩下的行(标签前后的正文都留着),去掉首尾空行,连着的几个空行并成一个;只写了标签时没有正文。别的标签(
@see、@since)留在正文里。 - 放到哪见「属性表」和各库规则:
@param对到参数表多出的「说明」列(按名字对,*args/**kwargs/...rest去掉前缀),对不上的在说明下面列成「对不上的参数」;@return跟在输出参数 / 返回那一行的值后面(PropertyRow.note);@throws是「异常」一行。卡片共用的几行在rules/docRows.ts。 - 中文名的兜底(
languages/typescriptNames.ts、pythonNames.ts从@param/Args:取参数的中文名)是另一回事,照旧读源码。
- 认
- 渲染:webview 用 markdown-it(
html: false、linkify: true),CommonMark 加 GFM 的表格、删除线:段落、强调、行内代码(含双反引号)、代码块(围栏、缩进四格)、列表、链接、标题、引用、表格。段内单个换行是软换行,空行分段。同一段文字只渲染一次(缓存最近 500 段)。 - 安全:源码里的 HTML 当文字显示;链接认 http / https / mailto,带协议的网址和邮箱自动成链接(
sample.py这种不带协议的不算),别的写法(javascript:、command:、./、..、绝对路径、盘符)照原文显示。http / https / mailto 由 VS Code 的 webview 自己拦下、交给宿主在外部打开,不用另发消息。http / https 图片不渲染(webview 的 CSP 不放行外部图片),显示成!加链接。仓库内图片按原图比例显示,点了打开原图,没有就写「找不到图片」和路径。 - 仓库路径链接(
[名字](从仓库根起的路径),可带#类名或#类名.成员)能点,点了打开文件并跳到符号,和 ⌘ / Ctrl + 点击跳到别的文件里的定义是同一条路(editor/openTarget.ts的openingFor,见「跳到定义」):语言有代码阅读的在预览所在的编辑器组用预览打开、滚过去、闪符号那处(不带符号的只打开预览),没有代码阅读的用 VS Code 默认的打开方式(普通编辑器选中符号的名字,图片看图);指到这份文件里的符号时在本预览里滚过去、闪一下(同下面的引用)。指到目录的在代码阅读侧栏的树上定位(展开并选中,和侧栏跟着当前文件走是同一个定位;侧栏没露着时亮出来),不切到 VS Code 资源管理器;树上没有(开着筛选时没摆它)时提示「侧栏的树上没有 …」,不退出筛选。 - 不带路径的引用(KDoc 的
[Name]、[Class.member])能点:- 认哪些:只在注释的 Markdown 里认(说明正文、参数说明、
@return、「异常」表、「注释」一行、文件开头的说明,连带增删的;问 AI 的回答不认)。方括号里是标识符(ASCII 字母、数字、下划线、$,点分开的几段);中文这类不是标识符的([会话进展])照原样是文字。反引号里的是代码;紧跟在名字、]、)后面的方括号是下标(items[i]、a[0][1]、f()[k]);后面紧跟(…)的是完整链接、紧跟[的是参考式链接,都不算。webview 在markdown.ts加一条行内规则(排在 markdown-it 的 link 后面,渲染时env.symbolLinks为真才认),写成地址为symbol:名字的链接(作者写不出这种地址,validateLink不放行)。 - 指到哪(扩展端,
symbolLinks.ts的resolveReference,按 KDoc 的规矩):① 本文件定义的:文档符号(vscode.executeDocumentSymbolProvider,同仓库链接找符号那一套)顶层名字对上的(类、函数、常量、变量;落在导入语句里的名字不算),Class.member在它底下找成员;② 本文件导入进来的:名字(Class.member的第一段)是某条导入引进来的(预览已有的导入块 facts,ImportFacts.items:起了别名的按别名认,问原来那个名字;整模块导入按第一节认,起了别名的问最后一节),在导入语句里这个名字的位置上问语言扩展定义在哪(vscode.executeDefinitionProvider,同跳到定义,几处取第一处),Class.member再在目标文件的文档符号里找:定义落在哪个符号里就在它底下找,落在文件开头(导入的是模块)就从顶层找;③ 本文件代码里用到、本文件又没定义的(语言自带的ValueError、str……):语言插件认的名字绑定(LanguageAdapter.bindings)里不是本文件定义的那些,按名字记下用在代码里的各处(outsideUses,注释、字符串、obj.name里的name不算),先挑用在这段注释说明的那个声明里面的,没有再挑整个文件的第一处,在那里问语言扩展定义(同代码里 ⌘ / Ctrl + 点击它,Python 的内置名跳到 Pylance 自带的builtins.pyi),Class.member同 ② 再找成员;④ 前面都没问到定义、名字(第一段)又是这门语言自带的(LanguageAdapter.builtins:Python 是dir(builtins)里公开的名字,TypeScript 是 ECMAScript 标准的全局对象):是builtin,页面当普通文字;⑤ 都不是就找不到。本文件定义了这个名字、或问到了定义,却没有这个成员时也算找不到,不再往后找。 - 问答同仓库路径链接(
useRepoLinks、RepoLinks):画出来后把页面上的仓库路径和引用一起发去问(checkRepoLinks { paths, symbols }→repoLinksChecked { missing, unresolved, builtin }),问到之前是普通链接,找不到的画成坏链接(红色波浪线,悬停「找不到」),builtin的(语言自带、又问不到定义的名字)画成普通文字(cr-link-plain:颜色同旁边的字、不带下划线,点了不发消息);文档换了版本(收到document)再问一次,引用跟着源码变。点了发openRepoLink(不带path,只带symbol):找到了同完整链接的后半段打开目标(语言有代码阅读的用预览、滚过去、闪,没有的用默认的打开方式选中名字),目标在这份文件里时在本预览里滚过去、闪一下(同跳到定义的闪);找不到就标坏;是语言自带的名字就不打开,回repoLinkBuiltin,页面改当普通文字。
- 认哪些:只在注释的 Markdown 里认(说明正文、参数说明、
- 样子:正文用界面字体(
--vscode-font-family、--vscode-font-size)、comment 色、行高 20px,一行注释和一行译文一样高;行内代码用编辑器字体、--vscode-textCodeBlock-background底;围栏代码块用编辑器字体和字号、同样的底,长行折行;段落、列表、代码块、表格之间 6px;标题只比正文稍大(h1 1.15em、h2 1.08em,其余同正文,加粗);链接--vscode-textLink-foreground;引用左边 3px--vscode-textBlockQuote-border竖线、--vscode-textBlockQuote-background底;表格 1px 细线。 - 「编辑」状态照旧是源码,编辑写回不变;Markdown 里的名字(反引号里的代码)不能点,不参与跳到定义,能点的只有上面的链接和引用。
- 定义卡片浮窗里的注释同注释一样渲染、认不带路径的引用,引用按它写在的那份文件找:浮窗根上的
data-peek记着卡片那份文件(DefinitionPeek.uri),commentLinkOf给浮窗里的引用带上file;扩展端(resolveSymbolLink)见file是别的文件,就按那份文件读成的文档(DocumentReads,同卡片画的那份)的块树、导入、用到的名字找(同一套ReferenceTargets,各预览共用、按文件记),没有file或就是这份文件的按本预览找。问坏没坏、点了打开、标坏、语言自带的当普通文字都同页面上的引用(repoLinkBroken/repoLinkOpened/repoLinkBuiltin带回file),同一个名字和块记号写在不同文件里分开记;浮窗里的链接悬停不弹卡片(同浮窗里的仓库路径链接)。
跳到定义
- 能点的:预览里(属性表的名称、类型、来源、导入项、默认值、值、字典 / 对象各层子行的键和值、类型参数、父类、实现的接口,以及一行译文)照抄自源码的名字——标识符、属性名、类型名、点分模块名的每一节(
luffy_agent.core.db三节各自能点,和 VS Code 一样)、字符串(TypeScript 的模块路径'./x'就是字符串)。翻出来的关键字、运算符、说明词不能点。 - 链接和编辑器里一样:只有按住修饰键(macOS ⌘,其余平台 Ctrl)、鼠标停在查得到定义的名字上时,名字加下划线、换成链接色(
--vscode-editorLink-activeForeground)、光标变手形;松开修饰键或移开就恢复。有没有定义问扩展端(vscode.executeDefinitionProvider,文档 uri + 名字在源码里的位置;库规则标了引用的名字问规则,见「规则自己查定义」,webview 不分这两种);和编辑器一样,每回指到一个名字上(或指着名字按下修饰键、换了文档版本)都现问,回答不留到下次用——Pylance、tsserver 刚启动那几秒会答「没有」;只认最近一次询问的回答,迟到的丢掉。 - ⌘ / Ctrl + 点击:
- 一处定义,在当前文件:原地滚过去;
- 一处定义,另一个文件,语言有代码阅读:在同一个编辑器组用预览打开(
vscode.openWith),滚过去;这个文件的预览已经在这一组开着就切过去、发给它,不重新打开; - 一处定义,语言没有代码阅读:在同一个编辑器组用 VS Code 默认的打开方式(
vscode.open,一般是普通编辑器)打开,选中定义处的名字; - 几处定义:先弹挑选框(每项「相对路径:行号」,下面一行是那行源码),挑一处再按上面跳;
- 没有定义:不动。
- 和普通编辑器里 ⌘ + 点击一样:目标是否按「预览标签」打开听
workbench.editor.enablePreview;workbench.editor.enablePreviewFromCodeNavigation为假(默认)时先把当前预览标签固定住,免得被目标替掉。 - 打开别的文件只有一条路(预览的
openElsewhere,用哪种编辑器由editor/openTarget.ts的openingFor按目标文件的语言定,侧栏点文件也按它):跳到定义、注释里的仓库链接、不带路径的引用都走它。
- 闪哪一处:页面上就是这处定义的最具体的那一处,只按源码位置判断(定义提供者给的定义处下标,对上块和属性行带的源码范围),不看属性名文字:
- 定义是块自己的名字(函数名、类名;只声明一个变量时的变量名,解构模式整个算)→ 整块。块的这段范围由扩展端从 facts 取(
BlockView.defines),和翻译插件无关。 - 定义是属性表里某一行 / 子行讲的东西(参数、导入项、一条语句声明几个变量时的某一个、类的泛型参数、对象字面量一项的键——TypeScript 对象的属性名就是定义,嵌套几层都算;一行译文下面拆开的各项、实参表里的子行也算)→ 只闪这一行 / 子行。范围是翻译插件从 facts 取的节点(
PropertyRow.defines、PropertyItem.defines;字典 / 对象的项标的是键,展开项、列表的项不标),定义处落在范围里就算(*args的定义在args上、解构参数里的名字都对得上)。 - 其余(定义在一行译文里,例如循环变量;翻译插件没填
defines)→ 显示着定义处那个名字的块,名字在属性行的子行里时展开那一行(在嵌套的子行里时连它上面各层一起展开);页面上找不到这个名字就取包含定义那一行的块。
- 同一文件原地跳、跳进另一个文件的预览,规则一样。
- 定义是块自己的名字(函数名、类名;只声明一个变量时的变量名,解构模式整个算)→ 整块。块的这段范围由扩展端从 facts 取(
- 滚过去:目标所在块的祖先块折叠着就展开,目标在折叠的属性行子行里(参数、导入项、字典 / 对象的项)就展开那一行,在嵌套的子行里就连它上面折叠着的各层子行一起展开。目标已在视口里(筛选栏和钉住的卡片下面)不滚,否则滚到视口正中(比视口高就顶部对齐)。然后目标的边框和底色往聚焦色(
--vscode-focusBorder)闪三下,共约 1.5 秒后复原;同一处再跳一次从头闪;系统开了「减少动态效果」时改成静止高亮 1.5 秒。目标是属性行 / 子行但块正在编辑(页面上没有属性表)时闪整块。 - VS Code 里没有这种语言的定义提供者(shell):语言扩展答不上时,同一文件里的名字由语言插件认(
LanguageAdapter.symbols,见「Shell 脚本」),跳到用到的那处之前最近的定义。 - 不按修饰键单击名字是高亮它的各处出现(见下一节);⌘ / Ctrl + 点击名字不会触发块的编辑切换、折叠,也不动高亮。
悬停看定义卡片
- 能弹的:带源码下标的名字(同「跳到定义」能点的那些),和注释里指到符号的链接(带
#类名/#类名.成员的仓库路径链接、不带路径的引用[Name]、[Class.member]);只指到文件、目录的链接不弹。不用按修饰键。 - 什么时候弹(同 VS Code 编辑器里的悬停):鼠标停在上面
editor.hover.delay毫秒后问扩展端要卡片(扩展端按这份文件读 VS Code 的设置,语言自己的设置优先;ready后发给页面,设置改了再发,页面不写死),没到时间就移开的作罢;回答带卡片才弹,没有就什么都不显示。同一时刻只有一个浮窗:指针在浮窗里时不弹别的,移到另一个名字、它有卡片时换成它的。只认最近一次询问的回答,迟到的丢掉。 - 什么时候收:移开名字和浮窗两者
editor.hover.hidingDelay毫秒后收(期间移进浮窗、移回名字就不收;从名字移向浮窗时扫过别的名字不把时间往后推);Esc、滚动(页面或页面里能滚的框,浮窗自己里面滚不算)、在浮窗外按下鼠标、换了文档版本、跳到某处(reveal)立刻收;收了之后指针不离开这一处不再弹。 - 卡片是哪块(扩展端,
editor/definitionPeek.ts的peekBlock):名字的定义同 ⌘ / Ctrl + 点击查的(规则标了引用的问规则,其余问语言扩展),几处定义取第一处;链接的符号同点这个链接时找的(symbolTarget:不带路径的引用按它写在的那份文件找,仓库链接在那个文件的文档符号里找)。定义处是哪块自己的名字(defines)就取那块,否则取编辑范围盖住定义那一行的最里层那块(参数、导入项、字段落在它们所在的块里),连同子块;挂在父卡片子行上的子块去掉slot,在浮窗里是最外层那张。定义在这份文件里用最近发出去的那份文档的块(版本对不上就没有);在别的文件就打开它,按它的语言、选中的翻译插件、库规则、项目入口排视图数据(同documentMessage;规则查名字的定义、调用的种类不等,规则出的错不记,由那个文件自己的预览记)。没有卡片:查不到定义、目标语言没有代码阅读、解析失败、没有块盖住定义处、指着的名字就是定义本身(同一文件、定义处下标就是这个名字的)。 - 中文名:同一文件的带上这个版本眼下已知的;别的文件只认起了中文名的定义本身(定义处标
definition,后面淡淡写原名),用到它们的各处要问语言扩展,浮窗不等。都只带卡片上画出来的名字(namesOnCard)。 - 样子(
DefinitionPeekPopup.tsx):portal 到页面根上的浮窗,外框用 VS Code 悬停框的边框、底色、阴影(--vscode-editorHoverWidget-*);标题一行「相对路径:行号」(路径从工作区文件夹算起,同挑选框),垫悬停框状态栏的底色、链接色,点了跳到定义(名字的同 ⌘ / Ctrl + 点击,发goToDefinition;链接的同点链接,发openRepoLink),浮窗跟着收。下面是卡片,卡片区最宽 640px、最高 420px,再大在里面滚。默认放在名字下方、左边对齐名字;下方放不下、上方空间更大就放上方,底边贴着名字往上长(卡片展开时不盖住名字);宽高再限到那边剩下的空间,钳在视口内。层级低于悬停提示,浮窗里名字的提示照样盖在它上面。 - 卡片只读:同页面上那张卡片(类、函数默认收起子块,能在浮窗里折叠展开,只记在浮窗里),块树不带编辑那部分(
BlockTreeState.edit),没有编辑、问 AI、重写注释的按钮,不标差异、不筛选。浮窗里的名字下标是那个文件的,页面上管名字的几处都不认它们(浮窗根上标data-peek):不按 ⌘ / Ctrl 变链接、不能跳,单击不高亮也不清页面上的高亮,不画页面上的高亮,不再弹卡片;注释里的仓库路径链接、不带路径的引用照样能点(引用按卡片那份文件找,见「注释的 Markdown 排版」),悬停不弹卡片。 - 问过 AI:卡片那块自己下面问过 AI 的带在卡片底下一节「问过 AI(N)」,所有语言、所有定义卡片都有(本文件的定义、工作区里别的文件、工作区外的库源码)。
- 范围:只带那块自己的问答(同一个块记号下面的那些),子块上问的不带。
- 对应规则(扩展端
peekCard):按「卡片那份文件 + 那块的块记号」精确对应,数据就是那块下面的对话那一份(.spx/code-qa.json,记在哪见「问 AI」:工作区外的库文件记在第一个工作区文件夹,键是~/…或绝对路径)。块记号是那块在它自己那份文件的块树里的anchor:卡片的块和那个文件自己的预览用同一套视图数据(BlockAnchors)算出,同那个文件的预览挂对话用的。从内存里的问答表取(CodeQa.blockThreads,同页面上块下面画的那份),不等别的;带的图片换成页面能用的地址。cargo 库换了版本、路径变了就对不上,不另找。问答写进去后卡片不实时刷新,下次悬停重新取。 - 样子(
AskThread.tsx的AskedList):同块下面的问答区(左边一条竖线、灰字),标题一行「问过 AI(N)」,下面按那块下面对话的先后(按时间)只列问题,回答收着;点一条展开 / 收起它的回答,回答同对话页(Markdown 排版、带的图片缩略图,点缩略图看大图)。大图画在浮窗里,看图时浮窗不收。跟卡片一起在卡片区里滚。没有问答就没有这一节。 - 只读:不能接着问、不能删,也没有「去那里接着问」的入口。
- 页面装好之前就放行各工作区文件夹的
.spx/code-qa/(CodeQa.imageRoots),别的工作区文件夹里的文件的问答图片也画得出。
- 按住 ⌘ / Ctrl 变链接、⌘ / Ctrl + 点击跳转、单击高亮各处出现都照旧;在浮窗外按下鼠标(单击名字、⌘ + 点击)先收起浮窗。
钉住卡片(Sticky Scroll)
- 同 VS Code 编辑器的 Sticky Scroll,一张卡片就是一层:卡片的开头(名称那一行)已经滚到筛选栏后面、卡片还有一部分看得见时,筛选栏下面钉一行这张卡片的头;套着几层就摞几行,外层在上、里层在下。卡片管到哪:卡片自己,连同它画出来的子块(挂在子行上、展开着的子块也算)。折叠着的卡片、没有子块的卡片照样钉(函数默认收着,属性表本身就高);折起来的子块、「只看改动」折起来的块没画出来,不算;被筛掉的不在页面上,正在编辑的卡片不钉;删掉的块那张不钉。和语言无关(只看块树),标差异的页面(未提交的更改、审查工单)一样。
- 钉哪几张(
stickyScroll.ts的pinnedLines,照 VS Code 的StickyScrollController.findScrollWidgetState):每行一样高(24px),卡片那一行在筛选栏下沿往下 n × 行高,n 是它外面套着几张卡片;卡片上沿高过它那一行顶、下沿(连同子块)还没过这一行顶时钉住。卡片快滚完、下沿高过这一行的下沿时,这一行跟着被顶上去,从上一行下面滑走(层级低一级),整块跟着变矮;下一张同层卡片接上来时它已经滑走。 - 行数:
editor.stickyScroll.enabled关着不钉;最多钉editor.stickyScroll.maxLineCount行,且不超过筛选栏下面可视区域的四分之一(四舍五入,同 VS Code);套得比这深时留外层的,同 VS Code。设置由扩展端按这份文件读(语言自己的设置优先),ready后发给页面,改了再发,页面不写死;没收到之前不钉。 - 一行的样子(
StickyLines.tsx):和那张卡片左右对齐(越里层越往右),折叠箭头那格留空、概念图标、标题,位置同卡片上的;标题同卡片上的写法(属性表第一行(名称)的值或一行译文,中文名照换、定义处后面淡淡写原名;Markdown 只取第一行),一行放不下省略。底色--vscode-editorStickyScroll-background、悬停--vscode-editorStickyScrollHover-background、下沿阴影--vscode-editorStickyScroll-shadow,同编辑器(主题没给时用编辑器底色、列表悬停色、控件阴影)。 - 点一行:把那张卡片滚到顶上,正好在外层还钉着的几行下面(外层各占一行,最多同上面的上限),闪几下(同跳到定义)。行里的名字照旧:⌘ / Ctrl + 点击跳到定义、悬停看定义卡片;单击名字高亮它的各处出现,同时滚过去;钉住的行里不画高亮(页面上找当前那一处只认卡片里的)。
- 层级:在页面内容之上,低于总览条、筛选栏和它的下拉菜单、定义卡片浮窗、下拉列表和悬停提示。页面的
scroll-padding-top是筛选栏加钉着的几行的高度:浏览器自己滚的时候(输入框拿到焦点、打字跟着光标走)让开它们,问 AI 的输入框、块的编辑框不被盖住。跳到定义、键盘跳高亮时「已经看得见就不滚」按钉着的几行下面算;上一处 / 下一处改动把卡片放在外层还钉着的几行下面。从文本编辑器切过来、记滚动位置仍只按筛选栏算(同编辑器里顶上那几行被钉住的行盖着),来回切不漂。 - 滚动、窗口大小变了、页面内容的大小变了、页面状态变了(展开、折叠、筛选、换文档)时排一次,每帧最多量一次、算一次(
requestAnimationFrame),钉的没变不重画;只有钉住的那几行跟着重画,块树不动。
高亮名字的各处出现
- 能点的名字和「跳到定义」一样:带源码下标的段(属性表、一行译文、字典 / 对象各层子行的键和值,点分模块名每一节各算各的)。「编辑」状态的块里的源码、Markdown 里的名字不算,既不能点也不被高亮。
- 单击名字:问扩展端(
vscode.executeDocumentHighlights,文档 uri + 名字在源码里的位置),由语言扩展(Pylance、TypeScript 语言服务……)按语义答,同名的别的东西(例如同名的参数)不算。页面上源码范围和回答里任何一处重叠的段都高亮,一个名字被拆成几段(luffy_agent.core.db)时每段各按自己的范围认。 - 讲别处名字的段(
Segment.refers):写在字符串里、语言扩展不认得的名字,库规则标出它讲的是源码里哪个名字(FastAPI 路径"/{user_id}"里的user_id讲的是函数的参数user_id)。页面把这段当那个名字:单击问那个名字的各处出现,⌘ / Ctrl + 点击跳到那个名字的定义(参数本身),那个名字的某处出现盖住refers时这段也高亮(单击参数user_id时 URL、路由器接口表里的{user_id}一起亮)。只有一处认这件事:webview 的occurrences.ts的sourceOf(段讲的是源码里哪一段名字:有refers取它,否则是照抄的位置),data-offset、高亮、当前那一处都按它;扩展端不用管。 - 颜色和编辑器里一样:写入(定义、赋值,
DocumentHighlightKind.Write)用--vscode-editor-wordHighlightStrongBackground,读取和按文字认出的用--vscode-editor-wordHighlightBackground;主题给了wordHighlightStrongBorder/wordHighlightBorder(高对比主题)才描边,描边画在字框里面不挤动排版。同一段碰上两种按写入画。 - 很多主题没配单词高亮色(例如 Gerry Oceanic),默认的灰底很淡,所以再用主题的
--vscode-focusBorder补一笔(换什么主题都跟着主题走):当前那一处(点中的、用键盘跳到的)在底色之外描一圈 1px 框;别的各处在底色下面画一条 1px 细线(内阴影,不占text-decoration,⌘ 指着名字时的链接下划线照旧)。写入的那处照样是强高亮底色。框和线都画在字框里面,不挤动排版。 - 看不见的出现(在折叠着的块、属性行、子行里,或块被筛掉)点的时候不自动展开;筛选栏右边、块数前面写「名字 · 2/5」(当前那一处是照源码先后排的第几处 / 回答里共几处,看不见的也算),名字取点中的那一段的文字;点中的名字不在回答里(没有当前那一处)时写「名字 · N 处」。之后自己展开,里面的出现照样亮着。
- 键盘跳:两条命令「代码阅读:跳到下一处高亮」(
superpowers.codeReader.nextOccurrence)、「代码阅读:跳到上一处高亮」(superpowers.codeReader.previousOccurrence),默认键 mac 上 ⌘↓ / ⌘↑、别的系统 Ctrl+↓ / Ctrl+↑,when是activeCustomEditorId == superpowers.codeReader.preview && superpowers.codeReader.hasOccurrences;用户可以在「键盘快捷方式」里改绑,webview 里不写死处理哪个键。命令面板里只在代码阅读预览是活动编辑器时出现。- 上下文键
superpowers.codeReader.hasOccurrences:webview 报「眼下能不能用键盘跳」(有高亮、页面有焦点、焦点不在输入框 / 块的「编辑」里),变了才报;扩展端有一个预览能跳就设成真(同一时刻只有一个页面有焦点,几个预览的消息先后不定也对),页面失去焦点、高亮清掉、页面被销毁(标签页切走)、预览关掉都变假。 - 命令发给报了能跳的那个预览;从命令面板运行时页面没焦点,发给活动的预览。
- 跳:照源码先后,从当前那一处往后 / 往前找下一处页面上找得到的出现(显示着和它重叠的段),到头了从另一头接着找;在被筛掉的、正在编辑的、被正在编辑的祖先盖住的块里的跳过,Markdown 里的不算,筛选栏上的总数照旧。点中的名字不在回答里时从点中的地方数起。当前那一处换成它,展开把它折叠住的块、属性行和子行(同一处在页面上显示几次取页面上的第一处),再把它滚进视口(已经整个看得见就不滚,否则放到视口正中,同「跳到定义」)。
- 快捷键只写在
package.json里,扩展、页面的代码里都不写死哪个键,页面不拦任何键:VS Code 把 webview 里按的每个键都原样转给工作台的快捷键系统(webview 外壳pre/index.html的handleInnerKeydown发did-keydown),焦点在预览里时快捷键照常生效;用户改绑后原来的 ⌘↓ / ⌘↑ 照常是浏览器的滚到底 / 顶。 - 浏览器对触发命令的那个键自己的动作(⌘↓ 滚到底)照常发生,由跳转盖掉(
occurrences.ts的KeyScroll):页面在按下任何键时记下滚在哪;命令发来时,按键后 1 秒内的(KEY_TO_COMMAND_MS)先回到按键那一刻的位置,再把目标滚进视口;按键后 0.5 秒内(KEY_SCROLL_MS)浏览器又滚走了就拉回跳到的位置。按键后点了鼠标、滚了滚轮、又按了键,或从命令面板运行(没有刚按的键),都不回、不守。 - 实测(隔离的真 VS Code,逐帧录屏):命令十几毫秒就到页面,浏览器的滚到底动画再过一帧才开始,而且在合成线程上播、页面忙着重画时照样播;不管的话页面最后停在底部(目标看不见)。只在滚动时拉回,重画期间仍有约 100 毫秒来回跳的画面;滚到原地浏览器当没滚、不打断已排上的动画,所以回位时先挪一个像素再回来(同一段脚本里连着两下,中间不画),动画在开始前就被打断,录屏里一帧都没有被滚走。
- 上下文键
- 和编辑器一样每回点都现问,回答不留到下次用(语言扩展刚启动时答的「没有」或按文字认的结果不会一直留着);只认最近一次询问的回答,迟到的丢掉。点另一个名字:回答到之前旧高亮留着,到了就换成新的;回答是空的(没装语言扩展、没答)就什么都不亮,不按文字自己找。
- 语言扩展答不上(shell 没有高亮提供者)时用语言插件认的同一文件里的名字(
LanguageAdapter.symbols):定义处算写入、别处算读取;中文名落到哪些名字上也走这条路。 - 清掉(连同筛选栏上那句):点空白处或不是名字的字、按 Esc(输入框、下拉列表自己用掉的 Esc 不算)、文档换了版本、换了翻译。
- 不抢别的点击:⌘ / Ctrl(以及 Alt、Shift)+ 点击、双击的第二下、拖着选字都不动高亮(双击的第一下照样算一次单击,和编辑器一样);点在按钮、链接、输入框、下拉列表、筛选栏、属性名上照旧,高亮不动——折叠箭头、「编辑」、Markdown 里的链接都照旧;名字本身在按钮里(带下一层的子行的键)时照旧折叠,同时高亮。
中文名
代码几乎都是英文名字,读的人要一个个在脑子里翻。代码里给定义起了中文名(显示名)的,预览里直接写中文名:if current_user.role != UserRole.ADMIN 读成「如果 当前用户.role 不等于 UserRole.管理员」。
从哪来(语言插件认,契约里只有一种事实:定义处的名字 + 中文名,DisplayName):
| 语言 | 写法 | 能起名的定义 |
|---|---|---|
| Python | 注解 Annotated[T, …, Comment("当前用户")](typing.Annotated 也算;调用的名字最后一节是 Comment 就算,不管从哪导入、是不是本地定义的,参数是普通字符串) |
参数、变量、类字段 |
| Python | 装饰器 @Comment("…")(项目里自己定义的什么都不做的函数) |
函数、类 |
| Python | 定义那一行行尾的注释 # @Comment 当前用户(记号从 @Comment 到行尾,前面可以还有别的字) |
参数(多行参数表里每行一个)、变量、类字段、枚举成员、self.x = … 的 x |
| Python | 兜底:函数 docstring 里这个参数的说明(谷歌写法 Args: 一节的 name (type): 说明,Sphinx 写法 :param name: / :param type name:),取第一小句(到第一个 ,。,.;;:: 为止),去掉首尾空白后不超过 12 个字才算,否则没有中文名 |
参数 |
| TypeScript / JavaScript | 装饰器 @Comment("…")(名字最后一节是 Comment) |
类(@d export class 也算)、方法、字段、方法和构造函数的参数 |
| TypeScript / JavaScript | 定义那一行行尾的 // @Comment 当前用户(块注释不算) |
参数、变量、枚举成员、类字段、接口属性 |
| TypeScript / JavaScript | 兜底:函数的 JSDoc 里 @param [{类型}] 名字 [-] 说明([名字]、[名字=默认] 也认,opts.x 这种属性不认),同上取第一小句、不超过 12 个字 |
参数(函数声明、方法、值是函数的变量和类字段) |
| Shell | 赋值、local / export / readonly 声明那一行行尾的 # @Comment 安装目录 |
变量 |
| Dockerfile | 紧挨在指令上面一行、只写了 # @Comment 部署 ID 的注释(Dockerfile 的注释只能单独成行) |
ARG、ENV(只有一个变量的)、FROM … AS 的阶段名 |
- 行尾记号给那一行第一个能带行尾记号的定义(上表第三行、第六行那些;函数、类用装饰器),单独成行的注释、那一行没有这种定义的不算。
- 同一个定义几处都写了:写在代码里的记号(注解、装饰器)优先,其次行尾记号,最后才是说明文字。
- 行尾记号不是说明文字:语言插件把记号的源码范围也交出来(
DisplayNameFacts.markers,不管这个定义最后取没取它),宿主放进SourceDocument.markers,翻译插件翻注释、说明时把它去掉(# @Comment 管理员这样只有记号的注释就没有说明,枚举成员表里没有这一列)。docstring、JSDoc 照原样显示。
落到哪些名字上(用到它的各处由语言扩展按语义认,同名的别的东西不算;没有语言扩展时一个都不换,和高亮、跳到定义一样只走这一条路):
- 本文件定义的:每个起了中文名的定义在定义处问一次
vscode.executeDocumentHighlights,回答里落在视图数据里带下标的名字(段的下标、长度都对得上)上的都换成中文名。 - 别的文件定义的(
UserRole.ADMIN,ADMIN 在 models.py):宿主给每种语言建一份工作区中文名索引(同库规则的WorkspaceIndex,找文件、排除、嵌套 checkout、忽略文件、盯改动、第一次查才建都是同一套;文件是语言插件给的DisplayNameReader.files,预筛是mentions),键是原名;这份文档里原名在索引里(别的文件)有的名字才问vscode.executeDefinitionProvider,定义处正好是索引里那个定义(同一文件、同一行列)才换。 - 按文档版本查一次,不挡第一次画:文档先发,查到本文件的先发一次,别的文件的查完再发(
displayNames消息);页面重新加载(标签页切回来)时随文档带上已经查到的。语言扩展刚启动时答不上来(定义处自己都查不到出现,或要问的定义一个都没答),隔 1、2、4、8、16 秒再查,还答不上就算了。 - 改文档时不闪:已经查到的中文名跟着改动挪位置(改动在前面的往后挪,碰到的丢掉),随新版本的文档先带过去;新版本查完一起换(手上有挪过来的就不先发只有本文件的那一半)。查到一半文档又改了,查到的丢掉。
怎么显示(webview 不认识任何语言,只按下标和长度换字):
- 预览里所有照抄源码名字的地方(一行译文、属性表、表格、枚举和库规则的卡片、推导式的框):名字写成中文名,悬停提示「原名 current_user」;下标不变,⌘ / Ctrl + 点击跳到定义、单击高亮各处出现、⌘↓ 跳都照旧,筛选栏上的名字写中文名。
- 定义处本身(输入参数表格的名称格子、名称行、枚举成员格子这些):中文名后面淡淡写原名(
descriptionForeground、字小一号,.cr-original),例如「当前用户 current_user」。 - 只换名字那一段:
UserRole.ADMIN读成「UserRole.管理员」,前面的类名照写。 - 「编辑」状态的源码、Markdown 说明里的名字不换。
问 AI
原型画板「问 AI」、组件 Reader / AskPanel(State=history | asking)。读代码时对一块有疑问,直接在块下面问 Claude Code,答完存起来,下次打开还在,理解了就删。
- 入口:块右上角那排图标按钮里的「问 AI」(每种块都有:普通块、属性表块、库规则的卡片)。点一下在块下面展开问答区,再点(或在输入框里按 Esc)收起。文件不在任何工作区文件夹里时问答没地方存,不给这个按钮。
- 问答区紧跟在块下面、子块前面,和块同宽,左边一条 2px 的链接色竖线(
--vscode-textLink-foreground50%)。从上到下:- 灰字的历史:这块以前问过的,每条一行问题(
descriptionForeground,一行放不下截断)、右边时间(「刚刚」「5 分钟前」「3 小时前」「昨天」「3 天前」,一个月以前写日期,悬停看完整时间)跟着用哪个 profile 答的(「3 天前 · sonnet」;选「默认」答的、这功能以前存的只写时间)和垃圾桶「已理解,删除」;问的时候贴了图的,问题下面一排缩略图(见「贴图」,灰字历史里淡一点、悬停变实);下面是回答的前两行(去掉 Markdown 记号的纯文字,更淡)。点问题或这两行展开整段回答,照样灰字,按 Markdown 排(同注释的 Markdown 渲染),再点收起。 - 当前这一轮(展开以来问的,和历史之间一条细分隔线):问题照常颜色、加粗,贴了图的问题下面一排缩略图,回答照常颜色、按 Markdown 排,逐字长出来,末尾一个闪的光标;下面一行灰字写正在做的事(「读取 exceptions.py」「搜索 get_user」,还没开口时「正在想…」)。出错时这一问下面一个红框写原因(找不到 claude、退出码非零带它的原话、没登录时提示在终端里运行 claude 登录、超时);叫停的写「已停止,这次没存。」
- 输入框:没问过时提示「问这块代码… Enter 发送,Esc 收起」,问过了「继续问… Enter 发送,Esc 收起」;Enter 发送,Shift+Enter 换行,输入法选字时的 Enter 不算;Esc 收起(不清掉名字高亮);展开时光标就在输入框里。右边先是 profile 下拉,再是发送按钮(没字时不能点),正在答时换成「停止」。
- 贴图:在输入框里粘贴(⌘V / Ctrl+V)剪贴板里的图片(截图、复制的图片),图片加到输入框上面一排缩略图里,随下一个问题一起发;剪贴板里同时有的文字照常粘进输入框。图片文件拖到输入框上也一样(VS Code 里要按住 Shift 拖,不然它自己接走打开文件)。认 PNG、JPEG、GIF、WebP,每张最多 5 MB,一个问题最多 6 张;不认的、太大的、多出来的不加,缩略图下面一行提示原因(「只能贴 PNG、JPEG、GIF、WebP 图片;每张图片不能超过 5 MB;一个问题最多带 6 张图片,有 N 个没加上。」,警告色 11.5px),下次贴图或发送时清掉。问题还是要有字才能发(只贴图不写字发不出去);发出去后缩略图清空。
- 缩略图:40px 见方、圆角 4px、1px 边框,图按比例裁满;历史和当前这一轮的和回答一样往右缩到问题下面,贴了还没发的和输入框左边对齐、每张右上角一个小圆「✕」(「去掉这张图片」,去掉后光标回到输入框)。点缩略图看大图:盖住整个预览的深色底,图按比例放到放得下为止,右上角「关闭」;打开时焦点在「关闭」上,点哪儿或按 Esc 关上(只关大图,不收起问答区、不清名字高亮),焦点回到点开它的缩略图。缩略图用
data:地址(还没发的、当前这一轮的)或webview.asWebviewUri给的地址(存下的),页面安全策略本来就允许这两种图片,不用改。 - profile 下拉(同筛选栏的下拉
SelectMenu,ghost样子):只写选中的 profile 名和一个小箭头,灰字 12px、高 20px,最宽 140px,长名字截断,输入框还是主角;悬停--vscode-toolbar-hoverBackground,打开时--vscode-toolbar-activeBackground,键盘聚焦描 1px--vscode-focusBorder。点开(或聚焦后按 ↓ / ↑ / Enter / 空格)列出能用的 profile,↑ ↓ 挑、Enter 选、Esc 关;profile 多了弹层顶上有搜索框。悬停提示「用哪个 Claude Code 配置(profile)回答,下一个问题起用它」。 - 选项是插件的 profile 目录(同看板、会话:
cc/profiles的listClaudeProfiles,名字是文件名去掉.json)。插件的默认 profile(getDefaultProfilePath,offical.json)在里面时,没选过就选它;不在(一个 profile 都没有,或没有offical.json)时最前面多一项「默认」:不带--settings,claude 用~/.claude的登录,和以前一样照问,没选过就选它。 - 选了从下一个问题起用它(正在答的不受影响)。所有预览、所有块共用一份,记在扩展的
globalState(superpowers.codeReader.askProfile,记 profile 名,「默认」记'')里:换个预览、重启 VS Code 都还是它;记着的那个没了(删了、目录换了)就回到默认那项。
- 灰字的历史:这块以前问过的,每条一行问题(
- 没展开、但问过的块:块下面一行灰字「问过 N 次」,下面默认摊开灰字历史(能展开、能删),读的时候一眼看到这里问过什么;点这一行收起历史、再点摊开,箭头跟着换;没有输入框。
- 收起就结束这一轮:这一轮答完的都回到灰字历史里(再问照样接着会话,见「追问」);还没答完的接着答,答完照样存;出错、被叫停没存的那一轮扩展端也不再留着。同一块同时只答一个问题(正在答时不能再发),不同块可以同时问,全插件同时最多 3 个,再多就在这一问下面说「同时最多问 3 个问题,等前面的答完再问」。
- 追问:这块存过问答,再问就接着最近一次(按问的时间)存下的那条的会话(
--resume <会话 id>,会话 id 取 stream-json 的 init 消息,和问答一起存在code-qa.json里),只发问题和「还是第几行那块」;收起再展开、预览重新加载、重启 VS Code 都一样接得上,前面问过的照旧灰字显示。换了 profile 也照样接着那次会话问(--resume和这次选的--settings一起带,本机 claude 2.1 试过:换 profile 后会话 id 不变、记得前面说过的),线程里不另外提示。那条没记会话 id、或会话找不到了(过期、被清理)就按第一次那样带上代码上下文重新开会话。删掉最近那条,就接着剩下里最近的;一块下面删光了就是没问过。 - 删除:点垃圾桶马上删,不用确认;一块下面删光了这块就不挂了,一份文件删光了文件里就没有这份文件。那条带的图片文件一起删。
- 找不到原来位置的问答:记号(见下)对不上这份文件里任何一块的问答不显示,也不动它,留在
code-qa.json里;代码改回来、记号又对上了,就又挂回那块下面。 - 预览重新加载(标签页切走再切回来)后,扩展端记着的当前一轮接上(展开它那块):还在答的接着长、答完照样显示,出错、被叫停留着的照原样画出来(收起这块前一直留着)。同一份文件开在几处,问答、正在答的都一样。
怎么问(扩展端 qa/):
- 用 Claude Code 命令行:和插件别处的
claude -p一样找 claude(resolveClaudeBin)、清环境变量(ANTHROPIC_*、嵌套会话标记)、带问的时候选的 profile(--settings <它的路径>,选「默认」不带;每次问都现读 profile 目录,见上面的 profile 下拉),在这份文件所在的工作区文件夹里跑,它能自己读项目里别的文件。 - 只读,靠三层:
--tools Read,Grep,Glob(可用的内置工具只剩读、搜,编辑、写文件、跑命令的工具根本没有),--allowedTools Read,Grep,Glob(这几个不用再批),--permission-mode dontAsk(别的一律拒、不等人批,盖过 profile 里的 bypassPermissions),外加--strict-mcp-config且不给--mcp-config(不加载任何 MCP 服务,免得 MCP 工具能改东西)。不带--dangerously-skip-permissions。 - 问题从 stdin 发:
-p --input-format stream-json,写一条用户消息(一行 JSON,同cc/spawnClaude.ts带图时的buildStreamJsonLine:先是问题的文字,后面按贴的先后跟着各张图片,{ type: 'image', source: { type: 'base64', media_type, data } })后关上 stdin。没贴图也走这一条路(只有文字),问题不上命令行。追问(--resume)和会话找不到时重新问都带上这一问的图片。本机 claude 2.1 试过:带图第一次问、接着会话再带一张图追问,都认得出图、记得上一张;约 6 MB 的 base64 图片照样能答。 - 输出
--output-format stream-json --verbose --include-partial-messages:init 消息给会话 id;逐字的text_delta拼成回答;工具调用换成「读取 X」「搜索 Y」;新的一条回复(读完文件后开口)一开口就换掉前面那句(「我先看看 X」);答完以 result 行的最终回答为准,is_error当失败。超时 5 分钟;「停止」给进程发 SIGTERM,这次不存。 - 第一次问的话:要求用中文、大白话、简洁,只读,提到文件写相对项目根目录的路径(带行号);带上文件的相对路径和语言、外面套着的各层块的开头(每层最多 6 行,带行号,例如类和它里面的函数)、这块代码(块的完整范围,同「编辑」,带行号,超过 120 行只贴开头并说剩下的在哪几行)、问题。追问只发问题和「还是 路径 第 a–b 行那块代码,代码可能刚改过,需要时重新读文件」。这些都只用视图数据的行号、记号和树形,不认识任何语言。
块的位置记号(BlockView.anchor,qa/anchor.ts):
- 扩展端生成视图数据时按源码先后逐块给:有名字的块(
defines的来源:函数名、类名、枚举名、单个变量名,库规则卡片取规则给的定义范围)是外层各个有名字的块加自己的「种类 名字」,例如class ReferralRelation > function display;没名字的块(条件、语句、返回、文件开头的说明、导入、多变量声明……)是外层最近的有名字的块加「种类 @源码哈希」,哈希是这块头部那几行(库规则认领的块是整条语句)去掉所有空白后的 SHA-1 前 8 位,改它的子块不影响它的记号,例如function display > if @3fa1c2d0。同一层里记号相同的(同名的两个函数、一模一样的两行语句)按源码先后在后面补[1]、[2]。 - 所以:上面插几行、改缩进空白、改别的函数的名字,记号都不变;有名字的块改了主体、签名还认得;没名字的块自己的源码改了,记号就变了,旧问答成了「找不到原来位置的」(不显示)。种类用视图数据的种类(库规则的卡片是规则的种类 id)。
- webview 只拿记号比相等,不拆开看。
存法:
- 每个工作区文件夹一份
<工作区文件夹>/.spx/code-qa.json,和.spx/code-notes.json同一套写法(pathMap.ts、folderStore.ts两者共用):{ "相对路径": [{ "anchor": 记号, "entries": [{ "id", "question", "answer", "createdAt", "sessionId", "profile"?, "images"? }] }] },回答是 Markdown 原文,createdAt是 ISO 时间,profile是用哪个 profile 答的(profile 名;选「默认」答的没有这一项),images是问题带的图片的文件名(见下;没带图的没有这一项);文件缺失或解析失败当作没有,认不出的块、问答跳过;写时按需建.spx,两格缩进、文件按路径排、块按记号排、问答按时间排、末尾换行;存第一条前不生成文件,删光后留{}。 - 问题带的图片(
qa/qaImages.ts):答成了才写,每张一个文件,放在同一个工作区文件夹的.spx/code-qa/里,名字是<问答 id>-<第几张>.<扩展名>(png/jpg/gif/webp),内容是原图;这个目录第一张图存下时才建。没答成(失败、叫停)不留文件;图片写不进去时这一问算失败(「回答没存上:图片写不进 .spx/code-qa(原因)」),不存这条。images只认这个样子的名字,手改成别的路径(../x)的跳过,删的时候也碰不到别处。 - 图片跟着条目走(
PathMapFile.attachments,folderStore.ts每次改表写好后删掉不再有条目带着的文件):删一次问答、删源文件(问答跟着删)时图片一起删;改名、挪位置按问答 id 起名,文件不用动;另一条还带着的不删;外部改了code-qa.json(git pull、手改)不删文件。挪到另一个工作区文件夹时问答跟过去,图片留在原来那个文件夹、跟着删掉,过去的那条缩略图就没了。 - 预览要能读到
.spx/code-qa/:打开预览时把这份文件所在工作区文件夹的这个目录加进 webview 的localResourceRoots,发给预览的问答里图片换成webview.asWebviewUri的地址。 - 在 VS Code 里改名、挪位置、删除文件 / 目录时问答跟着走(同文件注释);外部改了
code-qa.json(git pull、手改)文件监听重读,开着的预览跟着变。
重写注释
按写注释的技能(/ecc:writing-code-comments)给一块代码重写它的注释。没有说明、但块前面有普通注释时换掉那段;都没有就按这门语言的说明新写一段。像问 AI 一样留会话、能接着说:块下面记着重写过的每一轮,在输入框里说修改意见,模型接着那次会话改,交回新正文就写回文件。
- 按钮(
MessageSquareText图标,只有图标,悬停 / 读屏「按注释规范重写」,块还没有说明、也没有前面的普通注释时是「按注释规范写注释」):每块代码都有,不管卡片有没有「注释」一行都排在右上角那排按钮的最前面,依次是它、「问 AI」、「编辑」;文件开头的说明卡片也一样。定义卡片浮窗里、删掉的块没有;嵌着的代码里的块(Dockerfile RUN 里的 shell)、语言既没有说明的写法(LanguageAdapter.freshDoc)也没给行注释记号(LanguageAdapter.lineComment)、又还没有可换的注释的块没有(BlockView.commentTarget没有);文件不在工作区文件夹里时重写记录没地方存,也没有。点了按技能命令重写(开新会话)、展开这块的重写记录;展开着又正在写时不能再点。重写过、记录收着时它叫「展开重写记录」,点了只展开(好接着说),展开着再点才重写。 - 改哪儿(
BlockView.commentTarget,扩展端blockView.ts算):块已经有说明(Python 函数 / 类主体里第一条语句的 docstring、紧贴声明的 JSDoc、Rust 的//////!,以及别的语言记在块上的说明)时换掉这段说明的那几行,正文填回原来的定界符("""、/** */、///),不在声明上面再插一行行注释;说明和块前面的普通注释都有时只换说明,普通注释留着。没有说明、块前面挂着普通注释时换掉它们(从第一段开头到最后一段结尾,几段中间隔着的空行一起换掉,写成一段);文件开头的说明卡片换掉它自己。都没有时新写:Python 的函数或类在主体里插一段 docstring(缩进同主体,作为第一条语句;声明和主体写在同一行时从主体那一列拆开);JavaScript / TypeScript 的函数、类、枚举、常量、变量在声明紧上面插/** */;Rust 的项在紧上面插///;别的(语句、Python 的变量,只有行注释的 shell、Dockerfile)在块的第一行上面插一行行注释。挂在块后面、子块交上来的注释不动。技能照这条规矩从范围本身判断写哪种:写说明时带@param等标签,前端组件的说明画原型图、嵌进正文。 - 问模型:同「问 AI」用 Claude Code 命令行、在这份文件所在的工作区文件夹里跑、用输入框旁边选的 profile,按 JSON Schema 交结果(
--json-schema '{"type":"object","properties":{"body":{"type":"string"}},"required":["body"],"additionalProperties":false}':模型调StructuredOutput交,result 行的structured_output.body是不带注释符号的正文;只回了一段话、body是空的都算没交回)。文件有没保存的修改时不问(模型读的是磁盘上的文件),直接失败「先保存再重写注释」。- 第一轮(点按钮)从 stdin 发的只有一句
/ecc:writing-code-comments <路径>:<起>-<止>:路径相对 claude 的工作目录、用/分隔,行号 1 起,是这块代码的完整范围(连同挂在它上面的注释;文件开头的说明卡片是它自己那几行),开新会话(会话 id 由 Claude Code 起,结果里带回来)。 - 输入框里说的话:
--resume这块最近一次重写的会话(和问答的会话分开,各接各的),发「块现在在哪(<路径>:<起>-<止>,写回注释后行号会变,按记号从最新的块树里找)、空一行、用户的话」,带上贴的图。会话找不到了(过期、被清理)就开新会话,发技能命令后面空一行跟着这句话。 - 能做的事(
claudeAsk.ts的rewriteArgs):--restricted不读用户、项目、本地的设置文件(那里放行的命令进不来,比如用户设置里的git -C),文件工具只认工作目录;写注释的插件单独加载(--plugin-dir,目录按claude plugin list --json里启用着的 ecc 找),它的目录加进工作目录(--add-dir),技能读得到它的参考文档。原型照技能的约定存在仓库里:docs/prototype/<功能>/openpencil/下的build.js、<功能>.op,截图在它的exports/里,注释链到截图。工具只给读、搜、Bash、写文件(--tools Read,Grep,Glob,Bash,Edit,Write),不用再批的只有读搜、OpenPencil 的op(Bash(/Applications/OpenPencil.app/Contents/MacOS/op *))、建原型的截图目录(Bash(mkdir -p docs/prototype/*/openpencil/exports))、改原型目录里的文件(Edit(/docs/prototype/*/openpencil/**),开头的/是 claude 的工作目录,*只配一层目录;Edit 规则也管 Write)。Bash 规则里的*能跨空格,建目录时后面多跟一个路径(mkdir -p docs/prototype/x/openpencil/exports src/y)也对得上,所以另外拒掉Bash(mkdir -p docs/prototype/* *)(--disallowedTools)。别的一律拒绝、不等人批(--permission-mode dontAsk),不加载 MCP(--strict-mcp-config):改不了源码、跑不了别的命令,写回归扩展。本机 claude 2.1.296 用这组参数试过:写、改docs/prototype/x/openpencil/build.js,建它的exports/,op --version/op status都行;写、改源码,写docs/prototype之外和docs/prototype/x/下openpencil/之外的文件,在别处建目录(docs/other/exports、src/openpencil/exports、仓库外的绝对路径、带..的路径、合法路径后面多跟一个),rm、git -C、node -e、重定向写文件都被拒。 - 画图比读文件慢,超时 15 分钟(问 AI 仍是 5 分钟)。
- 第一轮(点按钮)从 stdin 发的只有一句
- 过程:说的话(第一轮是那句技能命令,排上队或开始后扩展端报上来)、正文逐字长出来(
--include-partial-messages时StructuredOutput的输入逐段来,按写到一半的 JSON 读出已经写出来的正文)、正在做什么(「读取 a.py」)。块下面同问答区的样子(AskThread换成重写的称呼):灰字历史、当前这一轮、输入框(提示「说说这段注释要怎么改…」,旁边 profile 下拉,能贴图),正在写时发送换成停止。 - 排队(
asks.ts的REWRITE_POLICY):整个扩展(所有预览、所有文件,第一轮和接着说都算)同时只跑一段重写,因为 OpenPencil 同一时间只能开一个画板,两段同时画,一边op stop另一边就跟着断。后来的排在后面,块下面这一轮写「排队中,前面的写完就开始」(沙漏图标、不转圈),输入框旁边的按钮是「取消排队」,点了这一轮按被叫停结束(「已停止,这次没存。」)、不再开始;前一段结束(写成、出错、被叫停都算)后按先后自动开始下一段,开始了照常显示进展。同一块同时只有一轮(在写、排着都算),这时块右上角的按钮不能点,再从输入框说也回「这段注释已经在重写了」。问 AI 不排队,照旧最多同时 3 个。 - 写回(扩展端,
WorkspaceEdit,能撤销):换掉时套回原来那段的定界符和缩进(Python 三引号、#//////////!行注释、/* */块注释,refillComment;几段隔着空行的照第一段的写法);新写时用这块说明的壳(docstring、/** */、///,或行注释#///)一行一行写,缩进同插入的那一行,空行写成只有记号的一行(docstring 的空行仍是空行)。写之前核对这块代码的那几行和开始时一样,变了就不写(「这块代码在重写期间变了」);没交回正文、定界符认不出、编辑器没接受也不写。 - 记录:每个工作区文件夹一份
.spx/comment-rewrites.json,和code-qa.json同一套写法(同一份qaMap.ts,改名、删文件跟着走),条目{ "id", "question", "body"?, "error"?, "createdAt", "sessionId", "profile"?, "images"? }:说的话(第一轮是技能命令)、交回的正文、没写进文件的原因(写进去了没有)。模型答完就记,写回失败也记下原因,好接着让它改;Claude Code 自己报错、被叫停的不记,文件不动。带的图片和问答的放在同一个.spx/code-qa/里,删一条跟着删。没展开时块下面一行「重写过 N 次」带灰字历史(没写进文件的那条下面写着原因),能收起、能删(「删除这条记录」)。 - 预览重新加载(标签页切走再切回来)后照样接上:排着队的、进行中的、出错被叫停留着的那一轮记在扩展端(同问 AI,
QaAsks),随qa消息的rewriteCurrent回来,展开它那块、排队中照样显示、进展接着长、停止和取消排队能用;收起这块时留着的那一轮去掉。切走期间扩展端照样写完、写回、记下。
通用契约
放在 vscode/src/codeReader/contract.ts,语法树类型用 @vscode/tree-sitter-wasm 自带的 web-tree-sitter 类型。
export type BlockKind =
| 'import' | 'function' | 'class' | 'enum' | 'exception' | 'type' | 'field' | 'const' | 'variable'
| 'trait' | 'impl' | 'module' | 'attribute' // Rust:特质、实现、模块、编译属性(属性表)
| 'if' | 'else' | 'loop' | 'try' | 'catch' | 'finally' | 'return' | 'statement' | 'comment'
| 'interpreter' | 'options' | 'case' | 'command' | 'pipeline' // shell:解释器、选项(属性表),分情况、命令、管道(一行译文)
| 'directive' | 'stage' | 'run' | 'copy' | 'setting' | 'label' | 'health' | 'start' // Dockerfile:解析指令、阶段、健康检查、启动(属性表),执行、复制、设置、标签(一行译文)
/** 源码里一段的下标(源码字符串下标),end 为开区间。 */
export interface SourceRange { startIndex: number, endIndex: number }
/** 源码里的一段,带起止行号(从 0 开始)。 */
export interface SourceSpan extends SourceRange { 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
positional?: PositionalParameter[] // 不写参数表、按位置取参数的语言(shell 的 $1):用到的第几个参数,照位置排;填了 parameters 就是空的
prints?: Node[] // 函数打印到输出的那几句(shell 的 echo / printf,没重定向到别处)
captured?: boolean // 函数的输出被调用处拿去用了(shell 同一文件里有 $(函数名 …))
returns?: Node[] // 只有成不成功的语言(shell):写了的 return 语句;空数组是「最后一句成不成功」。别的语言不填
}
/** 按位置取的一个参数:第几个(1 起),函数里给它起的名字(`local url="$1"`)。 */
export interface PositionalParameter { position: number, name?: Node }
/** 第一行 `#!` 写的解释器:程序名(bash),交给 env 按 PATH 找还是写死的路径,`#!` 后面的整句。 */
export interface InterpreterFacts { program: string, lookup: 'env' | 'path', command: string }
/** 打开 / 关掉解释器的一个开关(`set -e`、`set -o pipefail`):id 是语言自己的叫法(bash 的长名字 errexit……),flag 是源码里的写法。 */
export interface SwitchFacts { id: string, on: boolean, flag: string }
export interface OptionsFacts { switches: SwitchFacts[] }
/** 文件开头给构建工具看的解析指令(`# syntax=docker/dockerfile:1.7`),一行一条。 */
export interface ParserDirective { name: string, value: string }
export interface DirectiveFacts { directives: ParserDirective[] }
/** 指令后面的一个选项(`--from=deps`):值在源码里从哪开始(值里的阶段名、变量能跳到定义)。 */
export interface InstructionFlag { node: Node, name: string, value: string, valueStart: number }
/** 一个构建阶段(FROM):阶段名、第几个(没起名的按它引用)、紧贴在上面的注释、镜像名、是不是前面的阶段、标签 / 摘要(带 : / @)、选项。 */
export interface StageFacts { name?: Node, index: number, doc?: Node[], image: Node, stage: boolean, tag?: Node, digest?: Node, flags: InstructionFlag[] }
/** 要执行的命令的两种写法:数组(不经过 shell,node 是整个数组)、交给 shell 的一段嵌着的代码。 */
export type CommandForm = { form: 'exec', node: Node } | { form: 'shell', code: EmbeddedCode }
/** 健康检查:none 是 `HEALTHCHECK NONE`,否则选项和检查命令。 */
export interface HealthFacts { none: boolean, flags: InstructionFlag[], command?: CommandForm }
/** 容器启动时执行的命令:CMD(默认命令)或 ENTRYPOINT(入口)。 */
export interface StartFacts { instruction: 'cmd' | 'entrypoint', command: CommandForm }
/** 写在这门语言里的另一种语言的代码:外层语法树里装着它的节点、语言、源码里属于它的几段(去掉了外层的记号,拼起来是一份那种语言的代码)。 */
export interface EmbeddedCode { node: Node, languageId: string, ranges: SourceRange[] }
/** 嵌着的代码被当成一串步骤执行时的分块:一步一块,chained 表示前一步失败就不往下走(shell 的 &&)。 */
export interface StepBlocks { blocks: CodeBlock[], chained: boolean }
/** 宿主读嵌着的代码读出来的:各步读成的一行(注释不算一步)、是不是一步失败就停、整段是不是就一步(这一步的子块归嵌着它的块)。 */
export interface EmbeddedReading { steps: { text: Inline[], table?: PreviewTable }[], chained: boolean, whole: boolean }
/** 父类的一项:TS `extends Base<T>` 的 Base(表达式)和它后面的泛型实参 <T>;Python 类参数列表的一项(含 metaclass=…、*bases)。 */
export interface BaseFacts { value: Node, typeArguments?: Node }
export interface ClassFacts {
name?: Node // 匿名类没有;赋给变量、类字段的类表达式取变量名、字段名
doc?: Node // 同 FunctionFacts.doc
access?: Access // 嵌套在语句块里的类没有
abstract?: boolean // 只有 TypeScript 填(JavaScript 不填)
decorators: Node[]
typeParameters: Node[] // 每个泛型参数一项
bases: BaseFacts[]
implements: Node[] // TypeScript 的 implements,每个接口一项
}
/** 枚举的一个成员。 */
export interface EnumMemberFacts {
name: Node
value?: Node // 源码里写的值;没写(TS 自增)、交给库自动取(Python auto())时没有
computed?: number // 没写值、语言算得出来时的值(TS 数值枚举自增 0、1、2…)
docs: Node[] // 说明节点,照源码顺序:紧挨在上面单独成行的注释 / JSDoc、这一行行尾的注释、Python 的属性 docstring
}
/** 枚举:TS 的 enum 由语言插件报;Python 自带 enum 库的类由库规则认成这种块(RuleFacts)。 */
export interface EnumFacts {
name: Node
doc?: Node // 同 ClassFacts.doc
access?: Access // 同 ClassFacts.access
decorators: Node[]
bases: BaseFacts[] // 照写的父类(Python 的 str, enum.Enum);TS 没有
members: EnumMemberFacts[]
}
/** 异常类的一个类属性:Python 类主体里给一个名字赋值的语句,TS 的类字段(含 static)。 */
export interface FieldFacts {
name: Node
type?: Node
value?: Node
docs: Node[] // 同 EnumMemberFacts.docs
}
/** 异常类:Python、TS 都由语言插件报(见「异常」)。 */
export interface ExceptionFacts {
name?: Node // 同 ClassFacts.name
doc?: Node // 同 ClassFacts.doc
access?: Access // 同 ClassFacts.access
decorators: Node[]
bases: BaseFacts[] // 同 ClassFacts.bases
fields: FieldFacts[] // 类属性,照源码顺序
parameters?: ParameterFacts[] // 构造函数的参数(Python 的 __init__ 去掉 self,TS 的 constructor);没写构造函数时没有
}
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 填(JavaScript 不填)
items: ImportItem[]
}
/** 字典 / 对象字面量里的一项,照源码顺序;注释不算。 */
export type EntryFacts =
| { kind: 'pair', key: Node, value: Node } // 'a': 1、a: 1、[k]: v;TS 简写 { a } 键和值是同一个节点
| { kind: 'spread', value: Node } // **other、...other:value 是展开的表达式
| { kind: 'method', name: Node, node: Node, header: SourceSpan } // 对象里的方法、getter、setter:header 同 BlockShape.header,只到主体开始之前
/** 列表 / 元组 / 集合 / 数组字面量里的一项,照源码顺序;注释不算。 */
export type ElementFacts =
| { kind: 'item', value: Node } // 写出来的一项
| { kind: 'spread', value: Node } // *other、...other:value 是展开的表达式
/** 写出各项的字面量(LanguageAdapter.collection):map 是字典 / 对象,sequence 是列表 / 元组 / 集合 / 数组;空字面量的各项是空数组,项的值里还能再套。 */
export type CollectionFacts =
| { kind: 'map', entries: EntryFacts[] }
| { kind: 'sequence', elements: ElementFacts[] }
/** 写在表达式里的遍历(Python 推导式、生成器表达式)得到什么。 */
export type IterationResult = 'generator' | 'list' | 'set' | 'dict'
/** 遍历的一节,照源码顺序:从来源里逐个取出变量(async:异步地取),或只留下满足条件的。 */
export type IterationClause =
| { kind: 'for', variables: Node, source: Node, async: boolean }
| { kind: 'if', condition: Node }
/** 写在表达式里的遍历的结构:语言插件读出,翻译插件按自己的语序重排;各节、取的值里还能再套遍历。 */
export interface IterationFacts {
result: IterationResult
clauses: IterationClause[] // 至少一节,第一节是 for
value: Node // 每一项取的值
key?: Node // 结果是字典时每一项的键
sharedParentheses?: { open: Node, close: Node } // 遍历的括号同时是外面调用的括号(Python f(x for x in y)):写在遍历外面;只翻这个遍历(它是实参表里的值)时不写
}
/** 调用里的一个实参,照源码顺序;注释不算。 */
export type CallArgument =
| { kind: 'positional', value: Node } // 按位置传的
| { kind: 'keyword', name: Node, value: Node } // 按名字传的:Python 的 name=value
| { kind: 'spread', value: Node } // *args、...args
| { kind: 'keywordSpread', value: Node } // Python 的 **kwargs
/** 一次调用(含新建对象)的结构:语言插件读出,翻译插件把跨几行写的(拆开的列表里整项是调用的,写成一行也算)排成「新建 / 调用 名字 · N 个参数」一行、实参摊成表(见「调用与构造」)。 */
export interface CallFacts {
callee: Node // 被调用的表达式(TypeScript 的 new 不算在里面)
name?: Node // callee 里最后那个名字(点分名字的最后一节):着色、查定义用它;callee 不是名字时没有
construct?: boolean // 写法本身说明了是不是新建对象时给(TypeScript:new 是,别的调用不是);Python 不给,翻译插件问 SourceDocument.calleeKind
argumentList: Node // 括号连同里面的实参:它跨了几行,这次调用就是跨几行写的
arguments: CallArgument[]
}
export interface DeclaratorFacts {
name: Node
type?: Node
value?: Node
}
export interface VariableFacts {
access?: Access
doc?: Node // 同 FunctionFacts.doc:Python 模块、类主体里紧跟在这条赋值后面的字符串节点(属性 docstring)
declarators: DeclaratorFacts[] // 几条连着写的声明并成一块时(Dockerfile 连着的 ENV)是它们的全部
scope?: 'build' | 'environment' // 语言分这个才填:build 是构建参数(ARG),environment 是环境变量(ENV)
}
/**
* 筛选属性的通用词表,筛选栏按这个顺序排:kind(BlockKind)、access(public / private / protected,语言可加值)、
* async / abstract / typeOnly('true' / 'false')。宿主从 kind 和 facts 推出,facts 里没填的块就没有这个属性。
*/
export const COMMON_ATTRIBUTES = ['kind', 'access', 'async', 'abstract', 'typeOnly'] as const
export type CommonAttribute = (typeof COMMON_ATTRIBUTES)[number]
/** 块的筛选属性:属性 id → 值 id,都是稳定的英文 id,不放显示文字。 */
export type BlockAttributes = Record<string, string>
export interface BlockShape {
/** 这一块对应的语法树节点(整条语句)。 */
node: Node
/** 块自己显示 / 编辑的那一段:复合语句只到主体开始之前,其余是整条语句;块自己的说明(facts.doc、TextBlock.doc)也算在里面。 */
header: SourceSpan
children: CodeBlock[]
/** 讲这块的普通注释(不是说明),照源码顺序;由 attachComments(languages/shared.ts)挂上,预览排成卡片的「注释」一行,块的编辑范围连同它们。见「注释挂到它讲的块上」。 */
comments?: CommentBlock[]
/** 语言自己报的筛选属性:通用词表以外的(Rust unsafe: 'true'),或 facts 表达不了的通用属性值(Kotlin access: 'internal',盖过推出来的);kind 填了不算。 */
attributes?: BlockAttributes
}
/** 预览成属性表的块:种类和 facts 绑在一起,语言插件漏给 facts 编译不过。 */
export interface FunctionBlock extends BlockShape { kind: 'function', facts: FunctionFacts }
export interface ClassBlock extends BlockShape { kind: 'class', facts: ClassFacts }
export interface EnumBlock extends BlockShape { kind: 'enum', facts: EnumFacts } // TS:头部是整条声明,没有子块;库规则认出来的:整个类,留下的方法等是子块
export interface ExceptionBlock extends BlockShape { kind: 'exception', facts: ExceptionFacts } // 头部是整个类,留下的方法(含构造函数)等是子块
export interface ImportBlock extends BlockShape { kind: 'import', facts: ImportFacts }
export interface VariableBlock extends BlockShape { kind: 'const' | 'variable', facts: VariableFacts }
/** Rust 的内部属性 `#![…]`:生效的各个属性(cfg_attr 拆开)、cfg_attr 的生效条件(CfgPredicate:not / all / any / 配置项 / 照原文)、写在哪一层(文件顶层、mod、impl)。 */
export interface AttributeBlock extends BlockShape { kind: 'attribute', facts: AttributeFacts }
export type FactBlock
= | FunctionBlock | ClassBlock | EnumBlock | ExceptionBlock | ImportBlock | VariableBlock | AttributeBlock | InterpreterBlock | OptionsBlock
| DirectiveBlock | StageBlock | HealthBlock | StartBlock // 各带自己的 facts:InterpreterFacts、OptionsFacts、DirectiveFacts、StageFacts、HealthFacts、StartFacts
/** 带说明(facts.doc)的块。 */
export type DocumentedBlock = FunctionBlock | ClassBlock | EnumBlock | ExceptionBlock | VariableBlock
/** 属性表块的种类和 facts,不带块的形状:库规则把一块认成现成的一种属性表块时给它。 */
export type BlockFacts = FactBlock extends infer B ? B extends FactBlock ? Pick<B, 'kind' | 'facts'> : never : never
export type TextKind = Exclude<BlockKind, FactBlock['kind']>
/** 预览成一行译文的块(文件开头的说明是 Markdown)。 */
export interface TextBlock extends BlockShape {
kind: TextKind
doc?: Node // 同 VariableFacts.doc:Python 字段后面紧跟的属性 docstring;预览在译文下面按 Markdown 显示(Translator.doc)
group?: Node[] // 几条连着写的同种语句并成的一块(Dockerfile 连着的 COPY、LABEL):各条,node 是第一条;翻译插件排成一行字加一张表
embedded?: EmbeddedCode // 这块执行的是嵌着的另一种语言的代码(RUN):宿主分步,步是它的子块(只有一步时是那一步的子块)
}
export type CodeBlock = TextBlock | FactBlock
/** 一段注释:相邻行同一写法的行注释合成的一段(node 是第一条,头部盖住全部)、一条块注释或 Python 当注释用的字符串。文件开头的说明单独成块,别的挂在讲的块上(BlockShape.comments)。 */
export interface CommentBlock extends TextBlock { kind: 'comment' }
/** 着色用的词性,源码着色和译文着色共用。 */
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 个 默认导出 …
| 'constant' // 按习惯全大写、当常量用的名字(shell 的 PY_VERSION、HOME)
| 'variable' | 'function' | 'type' | 'string' | 'number' | 'literal' | 'comment' | 'plain'
/**
* offset:这段照抄自源码里的名字(标识符、属性名、类型名、点分模块名的一节)或字符串时,它在源码里的起始下标,
* 预览拿它向 VS Code 查定义、跳过去;译词、提示词没有。由从语法树叶子生成这段的一方填(模板直译引擎的译文和属性表);
* 第三方翻译插件可以不填,不填就不能跳。
* reference:只有库规则给的段有,这段讲的东西由规则自己查定义(LibraryRule.resolve),不问语言扩展;这样的段必须带 offset。
* color:这段自己的颜色(暗色、亮色主题各一个),盖过词性的颜色:库规则按库自己的配色上色(FastAPI 的 HTTP 方法用 Swagger UI 的颜色)。
* refers:这段讲的是源码里别处的一个名字(那个名字的范围),语言扩展不认得这段自己的位置:写在字符串里的名字(FastAPI 路径里的参数名)。
* 预览把这段当那个名字:⌘ / Ctrl + 点击跳到它的定义,单击高亮它的各处出现,它的某处出现盖住 refers 时这段也高亮;offset 照旧是照抄的位置。
* tooltip:悬停这段时看的字(说明里写了、参数表里没有的参数名:「没有这个参数」)。
*/
export interface Segment { text: string, role: TokenRole, offset?: number, reference?: RuleReference, color?: ThemeColor, refers?: SourceRange, tooltip?: string }
/**
* 框的样式,决定框头图标和框的颜色:iteration 写在表达式里的遍历(渲染大纲的「对 … 的每一项」也是),图标、颜色同「循环」块;
* condition 按条件(「当 … 时」「如果 …」),图标、颜色同「条件」块;branch 条件的一支(三元的「是」「否」),同「条件」块的颜色、不带图标;
* tag 小标签(「具名导出」「回调」),只有框头的字、颜色是 color;
* classes 收起来的一串类名(「样式 N 个类」),中性色,点开、悬停看 detail。
*/
export type GroupStyle = 'iteration' | 'condition' | 'branch' | 'tag' | 'classes'
/**
* 框起来的一组词:预览画成嵌在前后文字里的小框,框头是图标和 label(翻译插件、库规则给;只有图标时是空的);框里的词照样带下标,还能再套框。
* color 盖过样式的颜色(tag 按标签的意思上色),detail 是点开、悬停时看的全文(classes 的全部类名,一行一条)。
*/
export interface SegmentGroup { style: GroupStyle, label: string, segments: Inline[], color?: ThemeColor, detail?: string }
/** 译文里的一段:着色的词,或框起来的一组词。 */
export type Inline = Segment | SegmentGroup
/**
* 一段 Markdown 说明文字:注释、JSDoc、docstring 去掉注释符号、引号和共同缩进后的原文,不做别的转换。
* 预览按 Markdown 渲染成注释色的段落、列表、代码块(见「注释的 Markdown 排版」);第三方翻译插件给注释也填它。
*/
export interface Markdown { markdown: string }
/** 说明里讲的一个参数(`@param 名字 说明`):名字照写(`*args`、`...rest` 带着前缀),说明是 Markdown。 */
export interface DocParam { name: string, text: Markdown }
/** 说明里讲的一种异常(`@throws 异常 什么时候抛`):只写了名字的(`ValueError`、JSDoc 的 `{ValueError}`)写成 `[ValueError]`,`[Foo]`、`[说法](路径#Foo)` 照写。 */
export interface DocThrows { exception: Markdown, when: Markdown }
/**
* 一段说明(Translator.doc 读出来的):原文按 KDoc 拆成正文和块标签(规则见「注释的 Markdown 排版」的块标签)。
* 只写了标签、没有正文时没有 body。
*/
export interface Doc { body?: Markdown, params: DocParam[], returns?: Markdown, throws: DocThrows[] }
/** 预览里显示的一段文字:着色的词(一行译文、属性值),或整段 Markdown 说明(注释、函数和类的说明)。 */
export type PreviewText = Inline[] | Markdown
/**
* 属性表里的一行或一个子行讲的是哪个定义:定义出来的那个名字的节点(取自 facts)在源码里的范围——
* 参数名(连 `*` 前缀、整个解构模式)、导入项(名称到别名)、声明的变量名、类的泛型参数(整串)、字典 / 对象一项的键。跳到的定义落在里面就只闪这一行;
* 第三方翻译插件可以不填,不填就闪整块。
*/
export interface Defines { defines?: SourceRange }
/**
* 属性表里展开的一个子行:几列对齐的格子,格子是着色的词或一段 Markdown(参数的说明、异常的触发条件);
* items 是最后一格的值拆开的下一层子行(字典 / 对象、跨几行写的列表的各项):排成列表的子行展开在它下面、能折叠,
* 表格的行排在最后一格里、文字下面、总是展开。一层层可以一直往下套。
*/
export interface PropertyItem extends Defines {
cells: PreviewText[]
items?: PropertyItem[]
table?: PreviewTable // 最后一格的值里那次调用的实参表:表格的行排在最后一格里、文字下面,排成列表的子行排在它下面;总是展开,表里的行又能带 items、table
}
/**
* 属性表的一行:属性名、着色的值或 Markdown 说明(函数、类的「说明」)、展开在下面的子行(输入参数、导入项、字典 / 对象、列表的各项)。
* columns 是子行各列的列名(输入参数的名称 / 类型 / 默认值):给了的子行排成带表头的表格,没给的排成列表;两种子行都能再带下一层。
* note 是跟在值后面的一段说明(Markdown,注释色):输出参数那一行,返回类型后面是说明里的 `@return`。
*/
export interface PropertyRow extends Defines { label: string, value: PreviewText, note?: Markdown, items?: PropertyItem[], columns?: string[] }
/** 一张带表头和边框的表:列名,每行几列对齐的格子,同 PropertyRow 给了 columns 的子行。 */
export interface PreviewTable { columns: string[], items: PropertyItem[] }
/** 一行译文的块翻出来的:一行译文(注释块——文件开头的说明、挂在块上的注释——是 Markdown 原文);预览排在这一行下面的,跨几行写的字面量是拆开的各项(items),跨几行写的调用是摊开各个实参的表(table)。 */
export interface TextTranslation { text: PreviewText, items?: PropertyItem[], table?: PreviewTable }
/** 语言插件:语法树 → 代码块树,外加只有它懂、翻译插件翻表达式时要用的信息(叶子的词性、写在表达式里的遍历的结构)。 */
export interface LanguageAdapter {
id: string
/** 覆盖的 VS Code languageId,例如 ['typescript', 'typescriptreact', 'javascript', 'javascriptreact']。 */
languageIds: string[]
/** 该 languageId 用的语法包 .wasm 的绝对路径(宿主负责加载,保证和解析器运行时是同一份)。 */
grammarPath: (languageId: string) => string
/** languageId:一个插件覆盖几种方言时按它区分(JavaScript 没有 TypeScript 的抽象类、仅类型导入)。 */
toBlocks: (root: Node, source: string, languageId: string) => CodeBlock[]
/** 给叶子节点定词性:源码着色用它,译文里照抄的词也按它着色、按它认出能查定义的名字。 */
tokenRole: (leaf: Node) => TokenRole
/** 行注释的记号(`#`、`//`):还没有说明、而这门语言的说明就是行注释时(shell、Dockerfile),「重写注释」用它新写一段;不填、又没有 freshDoc,就不能新写。 */
lineComment?: string
/** 还没有说明时新写的说明插在哪、用什么壳(正文占位 `_`)。不填就用 lineComment 写在块上面。 */
freshDoc?: (block: CodeBlock) => { line: number, shell: string, breakAt?: number } | undefined
/** node 是写在表达式里的遍历(推导式、生成器表达式)时给出它的结构,别的节点没有;没有这种写法的语言不填。 */
iteration?: (node: Node) => IterationFacts | undefined
/** node 是一次调用(含新建对象)时给出它的结构,别的节点没有;没给的语言不展开调用,照常一行译文。 */
call?: (node: Node) => CallFacts | undefined
/** node 是写出各项的字面量(字典、列表……)时给出它的各项,别的节点没有;没给的语言不拆字面量,照常一行译文。 */
collection?: (node: Node) => CollectionFacts | undefined
/** node 是执行一条命令(shell)时给出它的结构,交给命令规则读;没有命令的语言不填。 */
command?: (node: Node) => CommandFacts | undefined
/** 一份源码里自己认得出的名字(LocalSymbol);VS Code 有这种语言的定义、高亮提供者时用不上,不填。 */
symbols?: (root: Node, source: string, languageId: string) => LocalSymbol[]
/** 这份源码里嵌着的别的语言的代码(块里的、属性表里的都算);没有的语言不填。 */
embedded?: (root: Node, source: string, languageId: string) => EmbeddedCode[]
/** 这门语言的代码被别的语言嵌着、当一串步骤执行时怎么分块;不填就按 toBlocks 分、不算一步失败就停。 */
steps?: (root: Node, source: string, languageId: string) => StepBlocks
/** 认代码里给定义起的中文名(见「中文名」);不填就没有中文名。 */
displayNames?: DisplayNameReader
/** 这块除了自己的种类,还认得出的概念(异步、抽象)。认到才返回;不填就只有种类那一个。种类由宿主补在最前面,这里不要再报。 */
concepts?: (block: CodeBlock) => BlockConcept[]
}
/** 代码里给一个定义起的中文名:name 是定义处那个名字,text 是中文名。 */
export interface DisplayName { name: SourceRange, text: string }
export interface DisplayNameFacts {
names: DisplayName[] // 起了中文名的定义,照源码顺序;一个定义几处都写了时语言插件定取哪个
markers: SourceRange[] // 写在注释里给代码阅读看的记号(`@Comment 当前用户`):翻注释、说明时去掉
}
export interface DisplayNameReader {
files: Record<string, string> // languageId → 工作区里这种语言的文件(glob):宿主建中文名索引
mentions: (source: string) => boolean // 便宜的文字预筛
find: (root: Node, source: string, languageId: string) => DisplayNameFacts
}
/** 翻译插件翻一块时拿到的这份源码:源码字符串,解析它的语言插件,和要从注释、说明里去掉的记号。 */
export interface SourceDocument {
source: string
language: LanguageAdapter
/** 文件路径(TextDocument.fileName);宿主没给(嵌着的代码、只读一段源码)时没有,按文件名定的说法写成判断不了的那种(Rust 编译属性的作用范围)。 */
fileName?: string
markers: SourceRange[]
/**
* 调用的名字(CallFacts.name)的定义是哪种块:宿主问语言扩展定义在哪,用那个文件的语言插件分块认出来。
* 同步地只交出已经查到的;还没查到、查不到、宿主不查都是 undefined,查到了宿主重排、重发(见「调用与构造」)。
*/
calleeKind?: (call: CallFacts) => BlockKind | undefined
/** 这个命令名的命令规则(这种语言注册的,先注册的算);没有时命令照抄。规则出错由宿主记下、当它没读。 */
commandRule?: (name: string) => CommandRule | undefined
/** 嵌着的代码(EmbeddedCode.node 是 node 的那段)读出来的样子:宿主用那种语言的插件读好了交出来;那种语言没注册时没有。 */
embedded?: (node: Node) => EmbeddedReading | undefined
}
/** 一份源码里语言插件自己认得出的一个名字:定义处,和用到它的各处(含定义处)。没有定义、高亮提供者的语言(shell)靠它跳转、高亮。 */
export interface LocalSymbol { definitions: SourceRange[], occurrences: SourceRange[] }
/** 筛选属性和值的显示名;没给名字的显示 id。 */
export interface AttributeNames {
attributes: Record<string, string> // access → 访问级别
values: Record<string, Record<string, string>> // access → public → 公共、async → true → 是、kind → function → 函数
}
/** 翻译插件:代码块 → 一行译文或属性表。 */
export interface Translator {
id: string // 'typescript.zh-template'
label: string // '中文直译'
languageIds: string[]
displayLanguage: string // 译文用的语言('zh'):库规则按它挑自己的词表
translate: (block: TextBlock, document: SourceDocument) => TextTranslation // 只翻 header 那一段;注释块(文件开头的说明、挂在块上的注释)给 Markdown 原文;跨几行写的字面量带拆开的各项、调用带实参表
describe: (block: FactBlock, document: SourceDocument) => PropertyRow[] // 函数、类、枚举、异常、导入、常量、变量块 → 属性表
expression: (node: Node, document: SourceDocument) => Inline[] // 一个表达式 / 类型节点整个翻成一行译文,译法同属性表的值;库规则借它
verbatim: (node: Node, document: SourceDocument, role?: TokenRole) => Inline[] // 一个类型 / 签名节点照抄成一行原文,不翻关键字:叶子按源码着色(给了 role 就都按它),名字各自带下标,跨几行写的压成一行、连着的空白收成一个空格;库规则借它
doc: (node: Node, document: SourceDocument) => Doc | undefined // 说明文字(注释、docstring、当说明用的字符串)→ 正文和块标签(Doc);正文同属性表的「说明」;连着几行的行文档注释(Rust 的 `///`)node 是第一条,翻译插件自己往下连
attributeNames?: AttributeNames // 筛选属性和值叫什么,和属性表的叫法一致;筛选栏、种类图标的悬停提示用它
}
// ── 库规则(见「库规则」一节)──
/** 一种颜色在暗色、亮色主题下各用哪个;高对比主题跟着暗 / 亮走。 */
export interface ThemeColor { dark: string, light: string }
/** 种类图标:插件自己定义的一组 lucide 图标(kebab-case 名字);前 26 个给内置种类,后面的给库规则。规则要新图标就加进这组,没有按任意名字找图标的路。 */
export const KIND_ICON_NAMES = ['import', 'square-function', /* …26 个内置种类的… */ 'database', 'package', 'server-cog', 'calendar-clock', 'globe', 'network', 'atom', 'zap', 'mouse-pointer-click', 'toggle-right', 'anchor', 'download', 'sigma', 'layout-template'] as const
export type KindIconName = (typeof KIND_ICON_NAMES)[number]
/** 内置种类 → 图标名。宿主写进概念列表,webview 不自己查。 */
export const KIND_ICONS: Record<BlockKind, KindIconName>
/** 一块上认出的一个概念。颜色不填的是语言的种类(按 id 用 CSS 的种类色);规则的概念填颜色,只给这个图标上色。 */
export interface BlockConcept { id: string, label: string, icon: KindIconName, color?: ThemeColor }
/** 库规则的种类:筛选栏里叫 label,卡片边框用 color,icon 进概念列表;icon 只能从 KindIconName 里挑。 */
export interface KindBadge { label: string, color: ThemeColor, icon: KindIconName }
/** 库规则认领一块后给出的替身,下面挂着原块留下的子块:规则自己的卡片,或认成的现成属性表块;这块讲在了别的卡片里时是略去它。 */
export type RuleBlock = RuleCard | RuleFacts | RuleOmit
/** 这块讲在了别的卡片里,不再单独成块(React 的 "use client" 讲成组件卡片的「客户端组件」标签);不能有子块,也不能挂着注释(注释没地方放)。 */
export interface RuleOmit { omit: true }
/** 规则自己的卡片:一张属性表。种类(badge)决定筛选和边框色;卡片左边的图标是语言的概念再加这个种类、以及 concepts。原块的「注释」一行由宿主插进 rows,规则不管。 */
export interface RuleCard {
kind: string // 种类 id,「规则 id.名字」(sqlalchemy.table):筛选的 kind 值,要稳定
badge: KindBadge // 种类的名字、颜色、图标;筛选栏里这个种类也叫这个名字,卡片边框用这个颜色
concepts?: BlockConcept[] // 额外认出的概念,排在种类后面;语言的概念仍在更前面,同一个 id 只留第一个
rows: PropertyRow[]
defines?: SourceRange // 块自己的名字(类名):跳到的定义是它时闪整块
attributes?: BlockAttributes // 规则自己的筛选属性,叠在宿主从原块推出的上面;kind 填了不算;名字由 LibraryRule.attributeNames 给
absorbs: CodeBlock[] // 并进来的子块(原块 children 里的对象本身,讲它的注释在它的 comments 里,跟着并进来),不再单独成块;没并的照源码顺序留作子块,编辑范围同别的块,是原块整条语句
slots?: RuleSlot[] // 挂在卡片某一行的子行上、点子行才展开的子块,一块只能放一处
}
/**
* 卡片里的一处子块:block(没并进来的子块)不排在卡片后面,挂在第 row 行的第 item 个子行上,照常转成块(翻译、库规则、编辑、问 AI、差异都照常)。
* 它默认收着,点那个子行的第一格才在这一行下面展开当子卡片、同一行一次只展开一个(Rust 实现块的方法表点方法名)。
*/
export interface RuleSlot { block: CodeBlock, row: number, item: number }
/** 把这块认成契约里现成的一种属性表块(enum 库的类 → 枚举块):宿主当语言插件报的这种块处理,翻译插件排属性表;头部是原块整条语句。 */
export interface RuleFacts {
as: BlockFacts
absorbs: CodeBlock[] // 同 RuleCard.absorbs
}
/** 规则看一块时宿主给的。 */
export interface RuleContext {
document: SourceDocument
imports: ImportFacts[] // 这份源码里各处导入,照源码顺序:认别名用
displayLanguage?: string // 当前翻译插件的 displayLanguage;没有翻译插件时没有
translate: (node: Node) => Inline[] // 借当前翻译插件的 expression;没有翻译插件时照抄源码
verbatim: (node: Node, role?: TokenRole) => Inline[] // 借当前翻译插件的 verbatim;没有翻译插件时照抄源码
doc: (node: Node) => Doc | undefined // 借当前翻译插件的 doc:正文和块标签,规则按自己的卡片把标签放到对应的行
lookup: (index: WorkspaceIndex, name: Node) => IndexEntry | undefined // name 指的定义在 index 里认出的那一项(见「规则认领时查别的文件里的定义」);还没查到是 undefined
}
// ── 规则自己查定义(见「规则自己查定义」)──
export interface RuleReference { rule: string, key: string } // 哪个规则、规则自己认的键(Celery 的任务名)
export interface RulePosition { line: number, character: number } // 0 起,列按 UTF-16
export interface RuleLocation { uri: string, start: RulePosition, end: RulePosition } // 定义处那个名字
export interface IndexedFile {
path: string // 相对所在工作区文件夹,/ 分隔
source: string
exists: (path: string) => boolean // 同一个工作区文件夹里有没有这个文件(只认 include 找到的)
}
export interface IndexEntry { key: string, name: SourceRange, value?: string } // value:规则从定义处读出、认领别的文件时要用的值(FastAPI:异常类的 status_code)
/** 规则在整个工作区里建的索引:规则只说看哪些文件、认出什么;找文件、预筛、解析、缓存、跟着改动更新归宿主。 */
export interface WorkspaceIndex {
id: string // 'celery.tasks',同一个 id 只建一份
languageId: string // 用这个语言插件解析
include: string // '**/*.py'
mentions: (source: string) => boolean // 便宜的文字预筛
entries: (root: Node, file: IndexedFile) => IndexEntry[]
}
export interface RuleWorkspace {
lookup: (index: WorkspaceIndex, key: string) => Promise<RuleLocation[]> // 第一次查时建,照文件、行排好
}
export interface LibraryRule {
id: string // 'sqlalchemy'
label: string // 'SQLAlchemy 数据表'
languageIds: string[]
libraries: string[] // 关心的库:导入来源等于它或是它的子模块才启用;空数组是不看导入,languageIds 里这几种语言都启用(React)
claim: (block: CodeBlock, context: RuleContext) => RuleBlock | undefined
resolve?: (key: string, workspace: RuleWorkspace) => Promise<RuleLocation[]> // 查自己标的引用;没给就不能标引用
attributeNames?: (displayLanguage: string | undefined) => AttributeNames // 自己报的筛选属性和值叫什么(同 Translator.attributeNames,用词归规则);翻译插件给了名字的以翻译插件为准
}
export interface CodeReaderApi {
registerLanguage: (adapter: LanguageAdapter) => Disposable
registerTranslator: (translator: Translator) => Disposable
registerLibraryRule: (rule: LibraryRule) => Disposable
registerCommandRule: (rule: CommandRule) => Disposable
}
// ── 命令规则(见「命令规则」一节)──
/** 重定向:输出(1)、错误输出(2)或两样写到 / 追加到文件,并进另一路(`>&2`、`2>&1`),从文件读。 */
export type Redirect =
| { kind: 'write' | 'append', stream: 'output' | 'error' | 'both', target: Node }
| { kind: 'merge', stream: 'output' | 'error', into: 'output' | 'error' }
| { kind: 'read', source: Node }
/** 命令的一个词:节点,和它写死的文字(去掉引号、转义;有展开的没有)。 */
export interface CommandWord { node: Node, literal?: string }
/** 一条命令:命令名、后面的各个词(选项、选项的值、要处理的东西,规则自己认)、重定向。 */
export interface CommandFacts { name: CommandWord, arguments: CommandWord[], redirects: Redirect[] }
/** 规则读出来的一行;value:读出来的是一个东西(`dirname "$0"` 是脚本所在目录),不是一个动作。 */
export interface CommandReading { text: Inline[], value?: boolean, table?: PreviewTable } // table:选项写得多的命令的「选项 | 意思」表
export interface CommandContext {
displayLanguage: string // 当前翻译插件的译文语言,规则按它挑词表
translate: (word: Node) => Inline[] // 一个词照常翻(名字带源码下标)
command: (name: CommandWord, args: CommandWord[]) => CommandReading | undefined // 读写在实参里的另一条命令(find -exec)
}
export interface CommandRule {
id: string // 'shell.rm'
label: string // 'rm'
languageIds: string[]
commands: string[] // 管哪些命令名;几条规则管同一个名字时先注册的算;'*' 管没有自己的规则的命令
read: (command: CommandFacts, context: CommandContext) => CommandReading | undefined // 读不了返回 undefined,命令照抄
}
activate()返回{ codeReader: CodeReaderApi };其他扩展声明extensionDependencies后用extensions.getExtension('clurdra.superpowers-vscode-clurdra')!.exports.codeReader注册。- 内置的 TypeScript、Python、Shell、Dockerfile 语言插件、四个中文直译翻译插件、shell / 装系统 / 装软件包的命令规则和 SQLAlchemy、Celery、enum、dataclasses、FastAPI 库规则在
activate()里通过同一个 API 注册(规则按这个顺序),没有旁路。 - 注册表变化时更新
superpowers.codeReader.languageIds上下文,并让已打开的预览刷新。 - 分工:语言插件只从语法树读出和语言无关的事实(facts,引用源码节点,不做正则匹配文字);翻译插件决定属性名、枚举词(公共 / 是 / 默认导出)和值怎么译,也给筛选属性和值起名(
attributeNames)。筛选属性的通用部分由宿主从 facts 推出(attributes.ts的blockAttributes),语言插件只在BlockShape.attributes里补自己语言特有的。第三方语言插件给函数、类、枚举、异常、导入、常量、变量块填 facts 就能用上任何翻译插件的属性表;自己语言的字典 / 映射、列表 / 数组这类字面量由collection给各项(节点 →CollectionFacts)就能展开成子行(中文模板直译按字面量的节点类型查叫法,见ZhTable.collections)。写在表达式里的遍历(推导式这类)由语言插件的iteration给结构(节点 →IterationFacts),翻译插件排成一个SegmentGroup(中文直译是样式iteration、框头是结果类型的译名);webview 只认框的样式 id,不认语言。调用由语言插件的call给结构(节点 →CallFacts),跨几行写的由翻译插件排成一行字加一张表(PreviewTable,见「调用与构造」);webview 只画表。 - 语言插件的信息只经契约到翻译插件:宿主翻每一块时把
{ source, language }(SourceDocument,language 是解析这份源码的语言插件)交给translate/describe,翻译插件要词性、遍历结构都从language取,不 import 任何语言插件的模块(lint 规则挡住translators/下的非测试文件 importlanguages/)。所以第三方翻译插件(英文、AI 翻译)拿得到和内置翻译插件一样的信息,第三方语言插件也能配上任何翻译插件。 - 第三方语言插件要给的:
id、languageIds、grammarPath、toBlocks(块的种类、头部、子块;函数、类、枚举、异常、导入、常量、变量块带 facts)、tokenRole。可以给的:facts 里语言有才填的项(async、abstract、typeOnly、access、doc)、BlockShape.attributes(通用词表以外的筛选属性)、iteration(有写在表达式里的遍历的语言)、call(调用的结构)、collection(字面量的各项)、displayNames(这门语言里给定义起中文名的写法)。没给的就少那一样:没有collection的字面量不展开,没有iteration的遍历照源码结构逐词翻,没有call的调用不摊成表,没有displayNames就没有中文名。 - 嵌着的代码的分工:外层的语言插件只报是哪几段(
LanguageAdapter.embedded、TextBlock.embedded、CommandForm),宿主按语言找那种语言的语言插件、翻译插件、命令规则解析和分步(LanguageAdapter.steps),外层的翻译插件只问读出来的样子(SourceDocument.embedded),连词、条数这些叫法归它;两种语言的插件互不 import。 - 命令的分工:语言插件认出一条命令的结构(
LanguageAdapter.command),命令规则按命令名读成译文(SourceDocument.commandRule,宿主按语言、注册顺序给),翻译插件读命令时问它、接上重定向;规则只从契约拿东西,不认识语言插件、翻译插件。 - 中文名的分工:语言插件只认写法(
DisplayNameReader.find:定义处的名字、中文名、注释里的记号);宿主解析一次源码时顺带问它,记号放进SourceDocument.markers交给翻译插件(翻注释、说明时去掉,中文模板直译在ZhTable.commentText之前去掉),定义交给预览找用到它的各处(nameResolution.ts);翻译插件不认识@Comment,webview 不认识任何语言。 - 翻译插件拿到的:代码块(kind、node、header、children,属性表块还有 facts)和
SourceDocument。按语法节点类型写的叫法(关键字表、按语境换的译法、节点模板、注解位置这类只关乎措辞的判断)属于翻译插件自己,写在它的表里。
库规则
语言插件讲语法结构,翻译插件定用词,它们都不认识某个库。库规则是第四种插件:认出某个库的一种写法(SQLAlchemy 的模型类),把那块整个换成一张专门的卡片;库的写法对应契约里现成的一种块时(Python 自带 enum 库的类就是枚举,TypeScript 的 enum 由语言插件报),也可以把那块认成这种块,交给翻译插件照常排。原型:docs/prototype/code-reader/figma-plugin/code.js 的「库规则:SQLAlchemy 数据表」一节(SQLA_MODEL、tableCard),画板「库规则 · SQLAlchemy 数据表」。
规则要给什么、可以给什么
- 注册:
CodeReaderApi.registerLibraryRule(rule),返回Disposable;内置规则走同一个 API。 - 必须给:
id、label、languageIds、libraries(关心的库)、claim(看一块,认领就返回替身RuleBlock,不认返回undefined)。 - 替身(
RuleBlock)三种,按有没有omit、as分:- 略去(
RuleOmit,{ omit: true }):这块讲在了别的卡片里(React 文件开头的"use client"讲成组件卡片的「客户端组件」标签),预览不画它、不占块的位置(后面的块 id 接着往下数)。它不能有子块、不能挂着注释(注释没地方放),有时宿主当替身不合约定。 - 认成现成的属性表块(
RuleFacts):as是种类和 facts(BlockFacts,例如{ kind: 'enum', facts: EnumFacts }),absorbs同卡片。宿主把原块换成这种块(节点不变,头部是原块整条语句,子块是留下的),此后和语言插件报的这种块完全一样:翻译插件的describe排属性表、种类名取翻译插件的、颜色取这个种类的、筛选属性从 facts 推、块自己的名字从 facts 取。规则不给用词,也不给标签和颜色。只要检查并进来的是不是这块的子块。 - 规则自己的卡片(
RuleCard),下面几条都说的是它。
- 略去(
- 卡片必须给:
kind(写成「规则 id.名字」,宿主核对前缀;筛选记住的就是它,要稳定)、badge(种类的名字、暗 / 亮两种颜色和 lucide 图标名)、rows(用现成的PropertyRow:属性名 + 着色的值或 Markdown,表格就是带columns的子行)、absorbs(并进来的子块)。可以给:defines(块自己的名字,跳到定义是它时闪整块)、attributes(自己的筛选属性)、concepts(额外认出的概念,排在种类图标后面)、slots(挂在卡片某一行的子行上、点子行才展开的子块,见下);表格子行填PropertyItem.defines(字段名),跳到这个定义时只闪那一行。 - 挂在卡片子行上的子块(
RuleSlot):留下的子块(没并进来的)可以不排在卡片后面,挂在卡片第row行的第item个子行上(讲它的那一个子行);它默认收着,点那个子行的第一格才在这一行下面展开当子卡片、同一行里别的收起(Rust 实现块的方法表点方法名)。它照常转成块,照样问各规则,编辑、问 AI、差异、筛选都照常。一块只能放一处,不能同时并进来;row要有这一行、item要有这个子行。只有这一种放法:子块要么挂在讲它的子行上,要么照常排在卡片后面。 - 只经契约:规则拿到的是块(kind、node、header、children、facts)和
RuleContext;源码里的名字照抄时自己带下标(Segment.offset),表达式、类型交给context.translate(当前翻译插件的expression,名字带下标,⌘ + 点击、高亮各处出现照常),要照原文显示的类型、签名交给context.verbatim(当前翻译插件的verbatim:不翻关键字,压成一行,名字各自带下标),说明文字交给context.doc(当前翻译插件的doc,给正文和块标签:正文和别的「说明」一样按 Markdown 显示,标签放到规则自己卡片对应的行,共用的几行在同目录的docRows.ts),别的文件里定义的名字交给context.lookup(见「规则认领时查别的文件里的定义」);库自己的配色(HTTP 方法)写在段的color上。规则可以读自己那门语言的语法树(规则声明了languageIds),但不 import 语言插件、翻译插件和宿主模块(rules/contractOnly.test.ts检查rules/下非测试文件只 import../contract和同目录的文件)。 - 用词归规则:属性名、表头、种类名、类型译名都在规则自己的词表里,按
context.displayLanguage(翻译插件的displayLanguage)挑,没有这种语言的词表就用规则默认的那份;加一种显示语言就是给规则加一份词表。webview 只画结构,不认识「数据表」这些字。
启用、认领、优先
- 启用:宿主收集这份源码里各层导入块的
ImportFacts.sources(去掉引号),有一个等于规则的某个库、或以「库.」「库/」「库::」开头(子模块;Rust 的路径用::,use axum::routing::get的来源是axum::routing),规则才对这份源码启用。规则不用自己读导入语句。libraries是空数组的规则不看导入,languageIds里这几种语言的源码都启用(React:新的 JSX 写法不用导入 react。Pydantic:子类文件常常只导入父模型、不再导入 pydantic,见「内置:Pydantic 数据模型」)。 - 认领:宿主从顶层往下每块问一遍启用的规则,按注册顺序,第一个认领的算(先注册的优先);被认领的块留下的子块照样问。
- 出错:规则抛错、或替身不合约定(种类 id 没带规则 id 前缀、并进来的不是这块的子块、属性表里的引用标的不是自己 / 规则没给
resolve/ 引用的段没带下标、挂在子行上的块不是留下的子块 / 放了几处 / 没有那一行或子行、要略去的块还有子块)时,宿主记一条警告日志(输出面板「Superpowers」),当它没认、接着问下一个规则;都没认这块照常显示。预览不会因为规则出错而失败。 - 并进来的子块:不再单独成块,由规则挑(
absorbs),宿主不替规则多并。没并进来的子块(方法、别的成员)不管写在哪(字段中间也算),都照源码顺序是替身的子块,预览里照常看得见。替身的编辑范围同别的块,是原块整条语句(从头部开始,到原块连同各子块的最后一行,类就是整个类),盖住留下的子块的行:替身编辑时预览不画这些子块,同一行不会同时在两处能改(见「预览(代码块树)」)。讲成员的注释挂在成员上(BlockShape.comments),成员并进来时跟着并进来,内置规则拿它当成员的说明;讲方法的注释挂在方法上,照常是方法卡片的「注释」一行。 - 卡片在预览里:左边的图标是语言的概念(原来的类、函数……)再加
badge这一个、以及concepts(同一个 id 只留第一个),悬停各自的名字;卡片边框色取badge.color(暗色、亮色主题各取一个),内容是rows,挂在子行上的子块展开时画在它那一行下面(收起这一行时它们跟着收起;画成一层块,同块树),不算卡片的子块(卡片的折叠箭头只管后面留下的子块);筛选的kind是替身的种类 id,其余属性从原块推出(原来的类是公共的,数据表也是公共的),叠上规则给的(规则自己的属性叫LibraryRule.attributeNames给的名字,规则起名时抛错记日志、照写 id);筛选栏里这个种类叫badge.label;和类块一样默认收起子块。多了图标不改筛选、不改边框色。
规则自己查定义
有些名字语言扩展查不到定义,只有懂这个库的规则知道它指向哪儿(Celery 任务表里的 "commerce.cancel_timeout_pending_orders" 是个字符串,指向工作区里某个 @celery_app.task(name=…) 装饰的函数)。规则可以把自己给的这种名字标成引用,自己查。别的库(路由名 → 处理函数、配置键 → 定义处……)照同样的办法用。
- 标:规则在属性表的段上填
reference: { rule: 自己的 id, key: 规则自己认的键 },段照常带offset(名字在源码里的位置,高亮各处出现、webview 认名字都用它);规则要给resolve(key, workspace),异步返回各处定义(RuleLocation:文件 uri 和定义处那个名字的起止行列)。不合这几条时宿主当替身不合约定(见上面「出错」)。 - 查:webview 不认引用,⌘ / Ctrl 悬停、点击、悬停看定义卡片照旧只报名字的下标(
probeDefinition/goToDefinition/peekDefinition)。扩展端记着最近发出去的那份文档里哪些下标是引用(只对那个文档版本有效),是引用的交给标它的规则的resolve,其余照旧问语言扩展(vscode.executeDefinitionProvider);两边查到的位置走同一条路:链接样子、一处原地滚 / 在预览里打开 / 普通编辑器打开、几处弹挑选框、闪「就是这处定义」的那块或那一行,都和「跳到定义」一节一样。规则注销了、resolve出错时记日志,当作没有定义。 - 在整个工作区里找:规则不能碰 VS Code,只描述一个
WorkspaceIndex(看哪些文件、便宜的文字预筛、从一份语法树里认出「键 → 定义处名字」),resolve里调workspace.lookup(索引, 键)。宿主(workspaceIndex.ts,VS Code 那一层在workspaceFiles.ts):- 第一次查时才建,同一个 id 只建一份,建的时候不挡预览(查的那一下等它建好);
- 按
include在各工作区文件夹里找文件,files.exclude、search.exclude里开着的和.venv、node_modules、__pycache__不看;自己是一份 checkout 的子目录(带.git文件或目录:子仓库、git worktree,例如.claude/worktrees/…里的副本,不然同一个任务会查到两处)整个不看,工作区文件夹自己的.git不算;search.useIgnoreFiles开着时(默认开,和 VS Code 搜索一样)路上各层目录的.gitignore、.ignore忽略的不看——每层的规则相对那一层判断,哪一层忽略了就不看,下层不能把上层忽略的加回来;工作区文件夹上面的、全局的忽略文件和.git/info/exclude不读(VS Code 的search.useParentIgnoreFiles、search.useGlobalIgnoreFiles默认也不读)。插件声明的引擎版本(^1.97)的正式 API 里findFiles不能按忽略文件筛,所以忽略文件由宿主自己读(workspaceScope.ts);文件改动时新建、改了的文件照同样的规则筛,改了忽略文件不会重扫已经建好的索引。每次读 32 份,预筛不过的不解析,用languageId的语言插件解析,语法树用完释放; - 盯着文件改动:改了只重看那一份;新建的加进来;删掉的(目录连里面的)去掉;规则认的时候问过在不在(
IndexedFile.exists)的文件新建、删掉时,问过它的那几份跟着重看(Python 包路径看__init__.py);建索引期间的改动建好后补上; - 读不了、规则认的时候出错的文件记日志、跳过,别的照常。
规则认领时查别的文件里的定义
有的卡片要用到别的文件里的东西:FastAPI 接口里 raise Forbidden(...) 的状态码写在 core/exceptions/schemas.py 的 class Forbidden 上。规则照「规则自己查定义」描述一个 WorkspaceIndex,entries 认出的项带着规则要的值(IndexEntry.value,FastAPI:"403");认领时对源码里的名字(节点)调 context.lookup(索引, 名字),拿到那个名字的定义在索引里的那一项。和「中文名」查别的文件里的定义是同一个办法:
- 怎么算:索引里有同名(键等于名字原文)的项,才问语言扩展这个名字的定义(
vscode.executeDefinitionProvider,同 ⌘ + 点击);定义处(文件、起始行列)正是索引里某一项的名字才算,同名的别处的类不算。索引里没有同名的项就是没有,不问语言扩展。同一份源码里同名的名字(几处raise Forbidden)按第一处查、共用一个结果。 - 什么时候查(宿主,
ruleLookups.ts的DocumentLookups,不碰 VS Code):认领是同步的,排视图数据时lookup只交出眼下知道的(没查到是 undefined),记下这一遍问了哪些名字;发出预览后再查,同一个版本同一个名字只查一次;查到的(键、值)和已知的不一样就按新结果重排一遍、重发文档。文档改了先用上一版查到的(不闪),新版本再查一遍,没变不重发。索引里有同名的项、语言扩展却没答(还没起来)时已知的不动,照「中文名」的间隔(1、2、4、8、16 秒)再查。 - 没有工作区时(单元测试、没传
lookup)总是 undefined;规则要能照常出卡片,只是少了查不到的那部分。
内置:SQLAlchemy 数据表(Python)
vscode/src/codeReader/rules/sqlalchemy.ts,libraries: ['sqlalchemy', 'flask_sqlalchemy']。类块主体里有 __tablename__,或有赋值是 mapped_column(...) / Column(...)(sa.Column、db.Column 这类取最后一节)时认领,种类 sqlalchemy.table,标签「数据表」(暗 #e8a860 / 亮 #a3590d)。导入时起的别名按 context.imports 换回原名(from sqlalchemy import Enum as SQLEnum、UUID as PgUUID)。
属性表(有才显示):
| 行 | 内容 |
|---|---|
| 名称 | 类名(type 色,带下标) |
| 表名 | __tablename__ 的字符串(string 色,不带引号) |
| 说明 | __table_args__ 里选项字典的 comment,没有就用类的 docstring;Markdown |
| 继承 | 父类照写法(翻译插件翻) |
| 装饰器 | 有才显示 |
| 字段 N 个 | 表格:字段 | 类型 | 约束 | 说明,每个字段一行,讲的定义是字段名 |
| 关系 N 个 | 表格:名称 | 目标 | 说明,每个 relationship(...) 一行 |
| 索引 N 个 | 表格:名称 | 列,来自 __table_args__ 里的 Index(...);唯一索引名称后写「(唯一)」 |
| 唯一约束 N 个 | 表格:名称 | 列,来自 __table_args__ 里的 UniqueConstraint(...)(单独一张表,名称取 name=,没给写「未命名」) |
字段:mapped_column(...) / Column(...) 的赋值,以及只写 Mapped[...] 注解的声明。第一个位置参数是字符串时是库里的列名,不当类型。
类型(第一个不是外键、序列这类的位置参数;没写就用 Mapped[...] 里去掉 None 的类型,照常翻,例如 Mapped[bool] → 布尔;都没有写「未标注」):
| SQLAlchemy | 译名 |
|---|---|
| Integer / INTEGER / INT | 整数 |
| BigInteger / BIGINT | 长整数 |
| SmallInteger / SMALLINT | 短整数 |
| String(n) / Unicode / VARCHAR / NVARCHAR / CHAR | 字符串(n) |
| Text / UnicodeText / TEXT | 长文本 |
| Boolean / BOOLEAN | 布尔 |
| DateTime / DATETIME / TIMESTAMP | 日期时间;timezone=True 加「(带时区)」 |
| Date / DATE、Time / TIME、Interval | 日期、时间、时间间隔 |
| Numeric(p, s) / NUMERIC / DECIMAL | 定点数(p,s) |
| Float / FLOAT / REAL / Double / DOUBLE_PRECISION | 浮点数 |
| JSON / JSONB | JSON |
| UUID / Uuid | UUID |
| LargeBinary / BYTEA | 二进制 |
| ARRAY(T) | 数组(T) |
Enum(X)、SQLEnum(X)、名字里带 enum 的辅助函数(_enum(X, "…")) |
枚举 X(X 是类名时 type 色、带下标);只列了取值的写「枚举 "a", "b"」 |
| 别的 | 照写法(翻译插件翻) |
约束(按下面的顺序,用 · 隔开;非空是常态,不写):
| 写法 | 显示 |
|---|---|
primary_key=True |
主键 |
unique=True |
唯一 |
index=True |
索引 |
nullable=True;没写 nullable 时:Column 和没注解的 mapped_column 默认可空,有注解的按 Mapped[X | None] / Optional[X];主键不可空 |
可空 |
default=表达式 |
默认 表达式(翻译插件翻,True → 真,None → 空) |
server_default=表达式 |
数据库默认 表达式 |
ForeignKey("t.c", ondelete=…, onupdate=…) |
外键 t.c(删除时禁止);ondelete:RESTRICT 删除时禁止、CASCADE 级联删除、SET NULL 删除时置空、SET DEFAULT 删除时置默认值、NO ACTION 删除时不处理,onupdate 同理「更新时…」「级联更新」,别的写「删除时 X」;两个都有用「,」隔开 |
说明:comment= 的字符串(comment 色);没写 comment= 时用字段的属性 docstring(context.doc 给的原文,comment 色;它随字段并进卡片)。关系的目标取第一个位置参数(字符串去掉引号),没写就用 Mapped[...] 里的类型;说明写「对方字段 X」(back_populates / backref)、「单个对象」(uselist=False)、「中间表 X」(secondary),用 · 隔开。
并进卡片的子块:__tablename__、__table_args__、每个字段和关系的赋值(讲它们的注释跟着并进来);__mapper_args__、普通属性、方法(写在两个字段中间也算)照常是子块,讲它们的注释是它们卡片的「注释」一行。
样例(luffy referral/models.py 的 ReferralRelation,测试里逐字核对):
名称: ReferralRelation
表名: referral_relations
说明: 邀请关系
继承: IntPK, TimestampMixin, Base
字段: 6 个
inviter_user_id | 整数 | 索引 · 外键 sys_user.id(删除时禁止) | 邀请人用户ID
invitee_user_id | 整数 | 唯一 · 外键 sys_user.id(删除时禁止) | 被邀请人用户ID(唯一, 一人只绑一个邀请人)
invite_code | 字符串(16) | | 绑定时使用的邀请码
bind_time | 日期时间(带时区) | | 绑定时间
expire_time | 日期时间(带时区) | | 归属过期时间
status | 枚举 RelationStatus | 默认 RelationStatus.ACTIVE | 关系状态
索引: 1 个
ix_referral_relations_inviter_bind_time | inviter_user_id, bind_time
配色:字段名 variable、类型译名 type、约束的词(主键、外键、默认……)storage、外键目标 string、删除规则和 · hint、说明 comment、索引名 string、索引列 variable。
内置:Celery(Python)
vscode/src/codeReader/rules/celery.ts,libraries: ['celery']。原型:code.js 的「库规则:Celery」一节(CELERY_APP、beatCard),画板「库规则 · Celery」;样例是 luffy 的 backend/src/luffy_agent/celery_app.py(节选在 fixtures/celeryApp.py)。两张卡片,都不并子块(语句本身没有子块);改配置(conf.update(...) 不带 beat_schedule)、autodiscover_tasks、信号处理函数照常显示。导入时起的别名按 context.imports 换回原名(from celery import Celery as App、crontab、timedelta)。
Celery 应用:常量、变量块的唯一一个赋值的值是 Celery(...)(celery.Celery(...) 也算),种类 celery.app,标签「Celery 应用」(暗 #98c379 / 亮 #4d7f17);块自己的名字是赋值目标(跳到 celery_app 的定义闪整张卡片)。属性表(有才显示):
| 行 | 内容 |
|---|---|
| 名称 | 赋值目标(variable 色,带下标;不是单个名字时照写法翻) |
| 说明 | 赋值后面的属性 docstring;Markdown |
| 应用名 | main(第一个位置参数,或 main=),翻译插件翻("luffy_agent" 带引号) |
| 消息队列 | broker |
| 结果存储 | backend |
| 别的参数 | 照写的顺序各一行,行名是参数名(include、task_cls……),值翻译插件翻;位置参数按 Celery.__init__ 的参数顺序认名字(第三个位置参数是 backend);*x、**x 行名「展开」 |
定时任务:X.conf.beat_schedule = {...}(值是字典字面量),或 X.conf.update(beat_schedule={...})(后者其余的配置各一行,行名是配置名,排在「应用」后面),种类 celery.beat,标签「定时任务」(暗 #56b6c2 / 亮 #127f8c)。属性表:
| 行 | 内容 |
|---|---|
| 应用 | .conf 前面的表达式(翻译插件翻,名字带下标) |
| 说明 | 赋值后面的属性 docstring;Markdown |
| 任务 | N 条,有展开时 N 条,另有展开 M 处(hint);表格:任务 | 频率 |(选项)| 说明,每条一行,照源码顺序 |
每一条:
| 条目写法 | 任务 | 频率 | 选项 | 说明 |
|---|---|---|---|---|
"键": {"task": "名", "schedule": …, …} |
"task" 的字符串内容(string 色,不带引号,带下标,是规则的引用 { rule: 'celery', key: 名 });不是字符串照写法翻(不是引用);没写写「没写任务名」;键和任务名不一样时后面淡色写「(条目名 键)」 |
见下表 | 除 task、schedule 以外的项,· 隔开:args 位置参数、kwargs 关键字参数、options 执行选项,别的写键本身,值翻译插件翻 |
见下 |
"键": 别的表达式 |
键(string 色,带下标) | 值照写法翻 | 空 | 见下 |
**表达式 |
「展开」(hint) | 展开的表达式照写法翻 | 空 | 见下 |
- 「选项」一列只在有条目带别的项时才有(
celery_app.py的主表没有,只有三列)。 - 说明:写在这条里面的
#注释(调度那一行行尾的、单独成行的)、写在这条最后一行行尾的、紧挨在它上面单独成行的,按源码顺序,取翻译插件给的原文(context.doc)用空格连起来,comment 色;最后一条后面单独成行的注释不算。
频率(keyword 色;换算不了时照下表最后两行):
| 调度写法 | 频率 |
|---|---|
只由数字和 + - * / // % ** 组成的算式(秒数):5 * 60、60 * 60、24 * 60 * 60、15、10⏎ * 60 |
换成最大的整单位:每 5 分钟、每小时、每天、每 15 秒;1 个单位写「每分钟 / 每小时 / 每天 / 每秒」,天数不再往上换(7 * 24 * 3600 → 每 7 天),不是整数的秒照写(每 0.5 秒) |
timedelta(...)(位置参数按 days, seconds, microseconds, milliseconds, minutes, hours, weeks,关键字同名),每个参数都是上面那种算式 |
同上:timedelta(minutes=5) → 每 5 分钟 |
schedule(run_every=X) / schedule(X) |
按 X 换算 |
crontab(...)(位置参数按 minute, hour, day_of_week, day_of_month, month_of_year;每个字段是整数、字符串 * / */n / 逗号隔开的数或 a-b,星期还能写 mon…sun,或整数的列表 / 集合 / 元组) |
时间:分、时都是 * → 每分钟;分 */n、时 * → 每 n 分钟;分是几个数、时 * → 每小时整点 / 每小时的第 0、30 分;时 */n → 每 n 小时整点 / 每 n 小时的第 m 分;时是几个数 → 几点几分(04:00,几个时刻「、」隔开,按先后排)。哪些天:都是 * → 每天;只有星期 → 每周一、每周一至周五、每周六、周日(0 和 7 是周日);只有日 → 每月 1 日、每月 1 至 7 日;日和月 → 每年 1 月 1 日。连起来:每天 04:00、每周一至周五 07:30、每月 1 日 03:00;一天里不止一次的写「哪些天 + 时间」(每周一 每小时整点),每天的只写时间(每 15 分钟、每 2 小时整点) |
名字、属性、下标、带名字的算式(get_settings().STORAGE_USAGE_POLL_INTERVAL_SEC、INTERVAL) |
当作秒数:每 〈翻译插件翻的表达式〉 秒,表达式里的名字带下标 |
别的(换算不了的 crontab,如 crontab(hour=4) 分钟是 *、带 1-10/2 这种步长;别的调用、timedelta(minutes=settings.X)) |
照写法(翻译插件翻) |
样例(fixtures/celeryApp.py,测试里逐字核对;子行各列用 | 隔开):
名称: celery_app
应用名: "luffy_agent"
消息队列: settings.CELERY_BROKER_URL
结果存储: settings.CELERY_RESULT_BACKEND
应用: celery_app
任务: 12 条,另有展开 1 处
commerce.cancel_timeout_pending_orders | 每 5 分钟 |
commerce.check_channel_pending_manual_slices | 每 10 分钟 | 手动退款切片对账: 微信回调丢失兜底
deployment.notify_expiring_deployments | 每天 | 每天一轮到期提醒扫描 (#274)
deployment.sweep_terminal_garbage | 每 10 分钟 | 终态垃圾兜底: failed 行 teardown 重试 + OSS 败方补清 (#320)
session.prune_langgraph_checkpoints | 每 5 分钟 | 高频小批量 sweep: … 防磁盘吃满 与清理 IO 风暴 (#316); …
referral.settle_due_commissions | 每小时 | 每小时扫一次到期佣金
storage.poll_app_data_usage | 每 get_settings().STORAGE_USAGE_POLL_INTERVAL_SEC 秒 |
展开 | _ops_beat_entries(settings.OPS_DASHBOARD_ENABLED) |
(其余几条同理)
任务名 → 定义(规则的 resolve,索引 celery.tasks):
- 看工作区里所有
**/*.py(排除的见「规则自己查定义」),预筛:源码里有.task(后面不接字母数字下划线)、shared_task或@task才解析。不要求文件导入 celery(luffy 的任务文件只from luffy_agent.celery_app import celery_app)。 - 认:装饰器是
@<任何东西>.task、@shared_task、@task(x.shared_task也算),带不带括号都算,装饰的是函数(含async def)。 - 任务名:装饰器的
name="…";name=写的不是字符串(常量、表达式)就认不出来,不收。没写name=时是 Celery 默认的「模块路径.函数名」,只对写在模块里(不在函数、类里面)的函数算。 - 模块路径:从文件所在目录往上,有
__init__.py的目录都是包;包外面(上面)还有src目录时从src下面算起(src布局下的命名空间包)。backend/src/luffy_agent/commerce/tasks.py→luffy_agent.commerce.tasks;worker/jobs.py(没有包)→jobs;__init__.py本身是包名。 - 定义处:
def那一行的函数名,所以目标预览闪整个函数块。几处定义(同名任务写在几个文件里)弹挑选框。
配色:Celery 应用的名称 variable、别的值按翻译插件;任务名 string,「(条目名 …)」「N 条…」「展开」hint,频率的换算词 keyword,选项名 storage、· hint,说明 comment。
内置:enum(Python)
vscode/src/codeReader/rules/enum.ts,libraries: ['enum'](Python 自带的库)。原型:code.js 的「枚举」一节(ENUM_SAMPLE、enumCard),画板「Prototype / 枚举」;样例是 luffy 的 referral/models.py、vault/models.py 里的枚举(节选在 fixtures/enumModels.py)。不出卡片,把类块认成枚举块(RuleFacts,as: { kind: 'enum', facts: EnumFacts }),属性表、种类名、颜色和 TypeScript 的 enum 一样,见「属性表」的「枚举」一行。
- 认哪些类:父类里有 enum 的枚举类型(
Enum、IntEnum、StrEnum、Flag、IntFlag、ReprEnum),写成enum.X(import enum、import enum as e的e.X)或从 enum 导入的名字(from enum import Enum as E的E,别名按context.imports换回),可以和别的父类混着写(class Status(str, enum.Enum))。同名的别处的东西不算(from sqlalchemy import Enum的Enum);父类是同一文件里别的枚举类(class Sub(Base))的认不出来(规则只看这一块)。 - facts:名称、说明(docstring)、访问级别、装饰器(
@enum.unique)、父类照原来的类;成员照源码顺序。 - 成员照 Python 的规则认:类主体里给一个名字赋值的语句(
A = 1、A: int = 1),名字不是__x__、_x_(enum 保留的,如_ignore_、_order_)、__x(私有),不在_ignore_列的名字里(空格隔开的字符串,或字符串的列表 / 元组),值不是lambda、property(...)/classmethod(...)/staticmethod(...)、nonmember(...)、连着赋值(A = B = 1);解构赋值(A, B = 1, 2)不认。只写注解的(_value_: int)不是成员。 - 值:
auto()(enum.auto()也算)交给库自动取,没有value(表里写「自动」);member(x)的值是x;别的是写的值。 - 说明(
docs):讲它的注释(挂在成员上的BlockShape.comments:它上面的,类主体最后一个子块是它时后面的也算;几条行注释连成的一段是几条)、它这一行行尾的注释、紧跟在它后面的属性 docstring,照源码顺序。 - 并进来的子块:各个成员(讲它的注释跟着并进来);方法、嵌套类、不是成员的属性照常是子块(不管写在哪),讲它们的注释是它们卡片的「注释」一行,枚举块默认收起它们。
样例(fixtures/enumModels.py 的 CommissionStatus,测试里逐字核对):
名称: CommissionStatus
访问级别: 公共
继承: str, enum.Enum
成员: 5 个
PENDING | "pending" | 待结算
AVAILABLE | "available" | 可提现
WITHDRAWING | "withdrawing" | 提现中
WITHDRAWN | "withdrawn" | 已提现
INVALID | "invalid" | 已失效
内置:dataclasses 数据类(Python)
vscode/src/codeReader/rules/dataclass.ts,libraries: ['dataclasses'](Python 自带的库)。样例是 luffy 的 user/repositories.py、commerce/payments/_driver.py、core/sms/services.py、sandbox/runtime.py 里的数据类(节选在 fixtures/dataclassModels.py)。种类 dataclasses.dataclass,标签「数据类」(暗 #ef7fb0 / 亮 #b3306c),和类块一样默认收起子块。卡片左边的图标是「类」再加「数据类」。frozen=True 再加一把锁(概念 id dataclasses.frozen,悬停「不可修改」,颜色同常量 #9cdcfe / #001080)。slots 不加图标。
- 认哪些类:装饰器是
@dataclass、@dataclass(...)、@dataclasses.dataclass(...)(import dataclasses as dc的dc.dataclass、from dataclasses import dataclass as data的data也算,别名按context.imports换回)。同名的别处的东西不算(from pydantic.dataclasses import dataclass、@other.dataclass)。 - 字段照 dataclasses 的规则认:类主体里带注解的名字(
x: int、x: int = 1),照源码顺序。ClassVar[...](typing.ClassVar、只写ClassVar也算)是类变量,不是字段,照常是子块;InitVar[...]是字段,类型写方括号里的,说明前面写「仅初始化时使用」;_: KW_ONLY是记号,不是字段,它后面的字段(没单独写kw_only=的)在默认值一格写「只能按名字传参」。 - 并进卡片的子块:各个字段(讲它的注释跟着并进来)、
KW_ONLY记号;方法(写在字段中间也算)、ClassVar、没注解的类属性照常是子块,讲它们的注释是它们卡片的「注释」一行。
属性表(有才显示;字段总有,一个都没有时写「0 个」、不画表格):
| 行 | 内容 |
|---|---|
| 名称 | 类名(type 色,带下标) |
| 说明 | 类的 docstring;Markdown |
| 访问级别 | 公共 / 私有 / 受保护(keyword 色) |
| 继承 | 父类照写法(翻译插件翻) |
| 装饰器 | dataclass 以外的装饰器(@final),有才显示 |
| 选项 | dataclass 装饰器的参数,只讲写了、又不是默认值的,照写的顺序用 · 隔开(storage 色):frozen=True 不可修改、slots=True 省内存(固定属性)、order=True 可比较大小、kw_only=True 只能按名字传参、eq=False 不按字段比较相等、unsafe_hash=True 可哈希、init=False 不生成构造函数、repr=False 不生成文字表示、match_args=False match 语句里不能按位置匹配、weakref_slot=True 可弱引用;写成默认值的(frozen=False)不讲;别的参数、值不是 True / False 的照写 名=值(值翻译插件翻),**x 照写 |
| 字段 N 个 | 表格:字段 | 类型 | 默认值 | 说明,每个字段一行,讲的定义是字段名 |
字段表的格子:
- 字段:字段名(variable 色,带下标)。类型:注解照写法翻(翻译插件翻,
str→ 字符串),InitVar[X]写 X。 - 默认值:
= x写 x(翻译插件翻,None→ 空);field(default=x)写 x;field(default_factory=f)写「每次新建 f()」(list→ 每次新建 list();不带参数的lambda写它返回的东西:lambda: {"a": 1}→ 每次新建 {"a": 1});没写默认值时空着。后面接field(...)的别的参数,照写的顺序:init=False不进构造函数、repr=False不显示、compare=False不参与比较、hash=True参与哈希、hash=False不参与哈希、kw_only=True只能按名字传参、kw_only=False可按位置传参,写成默认值的不讲,别的照写名=值(metadata={"unit": "px"});最后是KW_ONLY带来的「只能按名字传参」。各项用·隔开。 - 说明:
field(doc="…")的字符串;没写时同枚举成员的说明取法(讲它的注释、这一行行尾的注释、紧跟在后面的属性 docstring,原文用空格连起来),comment 色;都没有时是类说明里讲它的@param(字段就是构造参数,Markdown)。类说明里@param的名字不是哪个字段时,在卡片的「说明」下面列成「对不上的参数」;@throws是卡片最后的「异常」表。
样例(fixtures/dataclassModels.py 的 SandboxAccess,测试里逐字核对;说明的 Markdown 从略):
名称: SandboxAccess
说明: sandbox 类工具的最小资源集 (任何沙箱工具都需要). …
访问级别: 公共
选项: 不可修改 · 省内存(固定属性)
字段: 5 个
sandbox | CodeSandbox | |
sandbox_id | 字符串 | |
app_id | 字符串 | 空 | 空 |
session_id | 字符串 | 空 | 空 |
binding | SandboxBinding | 不进构造函数 · 不参与比较 · 不显示 |
__post_init__ 照常是卡片的子块。
内置:Pydantic 数据模型(Python)
vscode/src/codeReader/rules/pydantic.ts,libraries: [](子类文件常常只 from … import 父模型,不再导入 pydantic,不能靠导入来决定看不看)。类主体里带注解的赋值和数据类共用 rules/children.ts 的 annotatedAssignments。种类 pydantic.model,标签「数据模型」(暗 #c678dd / 亮 #8b3db5),图标 braces(和类型块同一个,颜色不同;悬停提示是 badge.label,走 data-tooltip),和类块一样默认收起子块。
- 认哪些类:模块顶层的类,父类链上有 pydantic 的
BaseModel。from pydantic import BaseModel、BaseModel as Model、import pydantic的pydantic.BaseModel、from pydantic.main import BaseModel都算,别名按context.imports换回。同一文件里继承另一个已经是模型的类也算(class Child(AIGCMetadata),父类写在后面也认)。不认TypedDict、NamedTuple、普通类、没导入 pydantic 时同名的BaseModel,也不认 pydantic 的@dataclass(那不是这张卡片,数据类规则也不认它)。 - 别的文件里的父类:父类是一个名字、又不是本文件里的类时,问索引
pydantic.models(context.lookup,见「规则认领时查别的文件里的定义」)。索引只扫源码里写了BaseModel的**/*.py,记下本文件里能确定的模型(父类是 BaseModel,或本文件里另一个模型),值是"model"。查到是模型就换成这张卡片;还没查到(或确定不是)就先按普通类画,查到后再重排。再往下跨一层、中间那个类自己的文件里写不出BaseModel的,索引不收,认不出来。 - 字段:类主体里带注解的赋值(
x: str、x: str | None = None、x: str = Field(...)),照源码顺序。ClassVar[...]不是字段。model_config = ConfigDict(...)(写成带注解的赋值也算)不是字段,是「选项」。 - 并进卡片的子块:各个字段(讲它的注释跟着并进来)、
model_config那一行;方法、@field_validator、ClassVar、没注解的类属性(model_config除外)照常是子块,讲它们的注释是它们卡片的「注释」一行。字段并进来之后不再单独画成变量块。
属性表(有才显示;一个字段都没有时不画「字段」这一行):
| 行 | 内容 |
|---|---|
| 名称 | 类名(type 色,带下标) |
| 说明 | 类的 docstring;Markdown |
| 访问级别 | 公共 / 私有 / 受保护(keyword 色) |
| 继承 | 父类照写法(翻译插件翻) |
| 选项 | model_config = ConfigDict(...) 里写了的项,照写的顺序用 · 隔开(storage 色)。没写的不讲 |
| 字段 N 个 | 表格:字段 | 类型 | 默认值 | 说明,和数据类同一套列,每个字段一行,讲的定义是字段名 |
选项(ConfigDict 的参数;不是这个调用就整段照写法翻):
| 写法 | 显示 |
|---|---|
strict=True / strict=False |
严格 / 不严格 |
extra="forbid" / "allow" / "ignore" |
禁止多余字段 / 允许多余字段 / 忽略多余字段 |
from_attributes=True / False |
可从对象读取 / 不从对象读取 |
frozen=True / False |
不可修改 / 可修改 |
populate_by_name=True |
可用字段名填 |
validate_assignment=True |
赋值时也校验 |
arbitrary_types_allowed=True |
允许任意类型 |
use_enum_values=True |
用枚举的值 |
str_strip_whitespace=True |
去掉首尾空白 |
validate_default=True |
也校验默认值 |
| 别的 | 照写 名=值(值翻译插件翻),**x 照写 |
字段表的格子:
- 字段:字段名(variable 色,带下标)。类型:注解照写法翻(翻译插件翻,
str→ 字符串,str \| None→ 字符串 | 空);类型里的名字带源码下标,能点。 - 默认值:
= x写 x(None→ 空);Field(default=x)或第一个位置参数写 x(Field(...)的...是必填,不算默认值);Field(default_factory=f)写「每次新建 f()」,和数据类同一个说法。后面接Field(...)的约束,照写的顺序用·隔开:min_length=1至少 1 个字符、max_length=255最长 255、ge=0≥ 0、le≤、gt>、lt<、multiple_of=33 的倍数、pattern匹配、min_items/max_items至少 / 最多 N 项、max_digits最多 N 位数字、decimal_places小数 N 位。default、default_factory、description不在这里重复。不认得的照写名=值,不丢掉。 - 说明:
Field(description="…")的字符串优先(不是字符串就照写法翻);没有就用字段上方的注释、这一行行尾的注释、紧跟在后面的属性 docstring,原文用空格连起来,comment 色;都没有时是类说明里讲它的@param(同数据类)。对不上的@param、@throws同数据类。
样例(fixtures/aigcMetadata.py 的 AIGCMetadata,从 luffy model/aigc.py 摘的,测试里逐字核对):
名称: AIGCMetadata
说明: 生成内容来源标识。
访问级别: 公共
继承: BaseModel
选项: 严格 · 禁止多余字段
字段: 7 个
Label | 字符串 | |
ContentProducer | 字符串 | |
ProduceID | 字符串 | |
ReservedCode1 | 字符串 | |
ContentPropagator | 字符串 | |
PropagateID | 字符串 | |
ReservedCode2 | 字符串 | |
TextTrace 里 reasoning_content: str | None = None 的默认值是「空」。fixtures/createShareRequest.py(luffy share/schemas.py 的 CreateShareRequest)的 session_id 是「至少 1 个字符」,说明是 Field(description=…) 的原文;@field_validator 的方法仍是子块。
内置:FastAPI 接口(Python)
vscode/src/codeReader/rules/fastapi.ts,libraries: ['fastapi']。原型:code.js 的「库规则:FastAPI 接口」一节(ROUTER_SAMPLE、routerCard、ROUTE_SAMPLE、routeCard),画板「库规则 · FastAPI 接口」;样例是 luffy 的 user/routes.py(fixtures/routes.py)、skill_hub/routes.py(fixtures/skillHubRoutes.py)、user/console/routes.py(节选在 fixtures/consoleRoutes.py)。路由器和挂在它上面的接口是两回事,各一张卡片:
- 接口:种类
fastapi.route,名字「接口」,图标globe,颜色暗 #61afef / 亮 #1a6fb5,和函数块一样默认收起子块。见下面「接口卡片」。 - 路由器:种类
fastapi.router,名字「路由器」,图标network,颜色暗 #7aa2f7 / 亮 #34548a(Tokyo Night 的蓝,亮色取它 Day 主题的)。见下面「路由器卡片」。
路由器卡片
模块顶层 X = APIRouter(...) / X = FastAPI(...) 的赋值(常量、变量块,只声明它一个)整块换成「路由器」卡片,没有子块可并。属性表(有才显示):
| 行 | 内容 |
|---|---|
| 名称 | 赋值目标(variable 色,带下标;跳到路由器的定义闪整张卡片) |
| 说明 | 赋值后面的属性 docstring;Markdown |
| 前缀 | prefix=,着色同接口卡片的 URL;FastAPI(...) 没有 |
| 标签 | tags=,同接口卡片 |
| 依赖 | dependencies=[Depends(x)] 写 x,写了才有 |
| 默认响应 | responses={...},写了才有;值和表格同接口卡片的「响应」 |
| 接口 | N 个(hint),下面一张表格「方法 | 路径 | 函数 | 说明」,列这份文件模块顶层挂在它上面的全部接口,照源码顺序;一个都没有时写「0 个」、不画表格 |
接口表的格子:方法同接口卡片(Swagger UI 的颜色);路径是完整的(前缀 + 装饰器里的路径),着色同接口卡片的 URL,{参数} 里的名字讲的是那个函数的同名参数(refers);函数是函数名(function 色,带下标:⌘ + 点击跳到它的定义,就是那张接口卡片,闪整张卡片);说明是函数 docstring 的第一行(comment 色),没有 docstring 留空。
样例(fixtures/routes.py 的 router,测试里逐字核对):
名称: router
前缀: /users
标签: 用户管理
接口: 3 个
GET | /users/{user_id} | get_user | 获取用户详情。
PUT | /users/{user_id} | update_user | 更新用户信息。
POST | /users/me/avatar | upload_current_user_avatar | 上传当前登录用户头像。
接口卡片
- 认哪些函数:装饰器是
@<路由对象>.<方法>(...),方法是 get / post / put / patch / delete / head / options / trace,路由对象是同一文件模块顶层X = APIRouter(...)/X = FastAPI(...)赋值出来的(APIRouter、FastAPI从 fastapi 导入,别名、fastapi.APIRouter也算)。路由对象从别的文件导入的、@router.get不带括号的、@router.websocket不认。几个路由装饰器时取第一个。 - 整个函数块换成卡片,不并子块:函数体照常是子块;编辑范围同别的卡片,是整条语句(连装饰器的整个函数)。
- 挂到应用上时再叠的前缀(别的文件里的
include_router(prefix=…)、settings.API_V1_PREFIX)URL 不算。 - 筛选:接口卡片多一个筛选属性
httpMethod,值是大写的方法(GET、PUT……,照写);属性名「请求方法」在规则的词表里,经LibraryRule.attributeNames给筛选栏。
属性表(有才显示):
| 行 | 内容 |
|---|---|
| 名称 | 函数名(function 色,带下标) |
| 说明 | 函数的 docstring 的正文,同函数块;Markdown |
| 对不上的参数 | 说明里 @param 的名字不是这个函数的参数时才有,同函数块(依赖、FastAPI 自己填的参数是函数的参数,不算对不上) |
| 路由器 | 挂在哪个路由对象上:它的名字(variable 色,带下标,⌘ + 点击跳到路由器卡片) |
| 标签 | 路由对象的 tags= 加装饰器的 tags=,字符串不带引号(string 色,带下标),「、」隔开 |
| 异步 | 是 / 否(storage 色) |
| 装饰器 | 路由装饰器以外的装饰器(@cache(60)),有才显示 |
| 依赖 | 路由对象、装饰器的 dependencies=[Depends(x)] 写 x,参数上的 Depends(x) / Security(x) 写 x(参数名),参数名是 _ 的只写 x;Depends() 不带参数时 x 是参数的类型;照这个顺序,「、」隔开。x 是一个名字时按 function 色、带下标,别的写法交给翻译插件翻 |
| 请求 | 值是 方法 URL(hint),下面一张表格「项 | 值」,一项一行,没有的项不列:URL、方法、路径参数、查询参数、请求头、Cookie、请求体。说明里有请求参数对上了 @param 时多一列「说明」(Markdown),这几种参数改成一个参数一行、各带它的说明,URL、方法和没写说明的格子留空 |
| 响应 | 值是各状态码用 / 连起来(hint),下面一张表格「状态码 | 内容 | 说明」,一个状态码一行,按状态码从小到大,不是三位数字的("5XX")排在最后;说明里写了 @return 时跟在值后面 |
| 异常 | 说明里写了 @throws 才有,同函数块的「异常」表 |
请求表:
- URL:路由对象的
prefix加装饰器的路径;{参数}按 variable 色(拆成{、名字、}三段),其余按 string 色,都带源码下标;花括号里的名字是函数的同名参数时标refers(参数名的范围):单击参数user_id时 URL 里的user_id跟着亮,⌘ + 点击 URL 里的user_id跳到参数;写的不是字符串(prefix=settings.X)交给翻译插件翻。 - 方法:大写,颜色用 Swagger UI 的配色(段的
color):GET #61affe、POST #49cc90、PUT #fca130、PATCH #50e3c2、DELETE #f93e3e(暗色);白底上照原色看不清,亮色主题取同色相深一些的 #1b6ac9、#18834f、#b35f00、#0f8a70、#cf2020;HEAD #b36bff / #9012fe、OPTIONS #4b92e0 / #0d5aa7(Swagger UI 的 #9012fe、#0d5aa7 在暗底上看不清,暗色调亮了);TRACE 没有配色,按 keyword 色。 - 路径参数、查询参数、请求头、Cookie:
名称: 类型,逗号隔开,名称 variable 色带下标,类型交给翻译插件翻(Annotated[T, …]写 T);没标注的只写名称。 - 请求体:只有一个时写它的类型(模型名),几个时(表单的几个字段)写
名称: 类型一串。
参数放在哪儿,照 FastAPI 的规则,按下面的先后,第一条对上的算:
- 写了
Depends(...)/Security(...)(默认值= Depends(x),或Annotated[T, Depends(x)]):依赖,不是请求带的。 - 名字在路径里(
{user_id}、{file_path:path},前缀和路径都算):路径参数。 - 写了
Path/Query/Header/Cookie/Body/Form/File(同上两种写法):照写的,Header进「请求头」、Cookie进「Cookie」,Body/Form/File进「请求体」。 - 类型是别处写好的类型别名(
Int4Query = Annotated[int \| None, Query(...)]、FilterQuery = Annotated[FilterList, Depends(...)]):经工作区索引查到它的标记(见下),照 1、3 放。 Request、Response、WebSocket、HTTPConnection、BackgroundTasks、SecurityScopes:FastAPI 自己填的,不列。- 上传文件(
UploadFile、list[UploadFile]):请求体。 - 简单类型:查询参数。简单类型是 int、float、str、bool、bytes、complex、UUID、datetime、date、time、timedelta、Decimal、Any、EmailStr、HttpUrl、AnyUrl、SecretStr、
None、Literal[...]、枚举类(别处定义的经索引查),和只包着它们的X | None、Optional/Union/list/set/tuple/Sequence这些;没标注类型的也是。 - 别的(Pydantic 模型、字典……):请求体。
*args、**kwargs 不算。
响应表(同一个状态码的几处写在一行里:内容「、」隔开、去重,说明「;」隔开、去重):
- 成功那一行:装饰器的
status_code(整数,或status.HTTP_201_CREATED这种常量),没写是 200;内容是response_model,没写就用返回类型标注(response_model=None、-> None没有内容);说明是response_description,没写是「成功」(comment 色)。 - 路由对象和装饰器的
responses={...}:每一项一行,键是状态码(整数、"404"、常量;"5XX"、"default"照写);值是字典时"model"是内容、"description"是说明,值不是字典时整个是内容;**x不列。 - 函数体里直接抛的异常(嵌套的函数、类、lambda 里的不算,只写
raise的不算,raise X from e算,调别的函数间接抛的不算)按状态码归:HTTPException(从 fastapi 或 starlette 导入)的状态码是status_code=或第一个位置参数,消息是detail=或第二个位置参数;别的类的状态码是类上的status_code = N,经工作区索引查(下面),消息是第一个位置参数(或message=、detail=)。内容是异常类名(type 色,带下标,⌘ + 点击到类的定义),说明是消息:字符串照写(comment 色),别的写法交给翻译插件翻。查不到状态码的异常(没查完、不是 HTTP 异常)不列。 - 有参数或请求体(路径 / 查询 / 请求头 / Cookie / 请求体里有一项)时,422 那一行加上「参数校验失败 | FastAPI 自动返回」(hint 色,淡)。
- 状态码的颜色:小于 400 的绿色(暗 #2ea043 / 亮 #1a7f37,段的
color),别的 string 色;不是三位数字的照写、string 色。
别的文件里的定义(索引 fastapi.names,见「规则认领时查别的文件里的定义」):看工作区里所有 **/*.py,预筛有 status_code、Annotated 或 Enum 才解析;从模块顶层认出:类上写了 status_code = N(带不带注解、status.HTTP_403_FORBIDDEN 这种常量也算)的类 → 值是状态码;父类是 enum 的枚举类型(Enum、IntEnum、StrEnum、Flag、IntFlag)的类 → Enum;X = Annotated[T, Query(...)] 这种类型别名 → 标记名(Query、Path、Depends……)。父类继承下来的 status_code(class AccountFrozen(Forbidden) 没自己写)不认。
样例(fixtures/routes.py 的 update_user,Forbidden 定义在 luffy 的 core/exceptions/schemas.py(fixtures/appExceptions.py),status_code = 403;测试里逐字核对):
名称: update_user
说明: 更新用户信息。…
路由器: router
标签: 用户管理
异步: 是
依赖: get_db(db)、get_current_user(current_user)
请求: PUT /users/{user_id}
URL | /users/{user_id}
方法 | PUT
路径参数 | user_id: Int4Path
请求体 | UserUpdateRequest
响应: 200 / 403 / 422
200 | UserDetailResponse | 成功
403 | Forbidden | 无权更新此用户信息;无权修改角色;无权修改启用状态;无权修改密码
422 | 参数校验失败 | FastAPI 自动返回
skill_hub/routes.py 的 list_skills:查询参数 | query: 字符串 | 空, tag: 字符串 | 空, skip: 整数, limit: 整数,依赖 get_db(db)、get_current_user(参数名是 _);user/console/routes.py 的 create_user:成功那一行 201 | UserDetailResponse | 成功,请求体 UserCreateRequest。
配色:名称 function、标签 string、依赖名 function、「(参数名)」hint、表格第一列(项)hint、URL 同上、参数名 variable、类型按翻译插件、异常类名 type、消息和说明 comment、自动带的 422 hint。
内置:React 组件(TypeScript / JavaScript)
vscode/src/codeReader/rules/react.ts(读语法树的办法在 rules/reactTree.ts,都是纯函数),languageIds: ['typescriptreact', 'javascriptreact'],libraries: [](新的 JSX 写法不用导入 react,TSX / JSX 文件都启用)。原型:docs/prototype/code-reader/openpencil/react.js 的三张画板「编辑器全貌」「卡片种类」「对照」(导出图 exports/react-main.png、react-kinds.png、react-compare.png);样例是 luffy 的 chat/code/bubbles/QueuedBubble.tsx(fixtures/queuedBubble.tsx.txt)和 chat/code/PlanChecklist.tsx 第 91–226 行(fixtures/planChecklist.tsx.txt,前面带上文件开头的导入),以前的 DesktopTitlebar、LegalDocPage、DeleteAppDialog、DesktopSettingsDialog、AppVaultSummary、ShareButton、PosterDialog、OverviewSection 也在 fixtures/。
TSX 和 Python 一样,从上往下一块语法一张卡片,嵌套的块排在父卡片下面,缩进、竖线、折叠同函数体。规则只换掉 React 的几种写法,别的块(导入、接口、类型、常量、普通变量、if、尝试……)照语言默认的卡片,注释照常挂在它讲的卡片上。组件卡片不把组件体并进来:组件体的语句照源码顺序是它的子块,照常交给各规则看。表格的格子里只写名字和位置,函数体只出现在子卡片里。
卡片种类(颜色写暗 / 亮):
| 种类 id | 名字 | 图标 | 颜色 | 认哪一块 | 行 |
|---|---|---|---|---|---|
react.component |
React 组件 | atom |
#61dafb / #0a7ea4 | 组件的声明 | 名称 / 说明 / 构造参数 |
react.state |
状态 | toggle-right |
#9cdcfe / #001080 | const [v, setV] = useState(…)、useReducer(…) |
名称 / 初始值 / 更新位置 |
react.ref |
引用 | anchor |
#9cdcfe / #001080 | useRef(…) |
名称 / 初始值 / 使用位置 |
react.source |
数据源 | download |
#9cdcfe / #001080 | 别的钩子赋给变量(useQuery、usePathname、useRouter、自定义 useXxx) |
变量 / 钩子 / 说明 |
react.derived |
派生值 | sigma |
#9cdcfe / #001080 | useMemo、useCallback |
名称 / 计算表达式 / 依赖项 |
react.effect |
副作用 | zap |
#e5c07b / #9a6a00 | useEffect、useLayoutEffect、useInsertionEffect |
触发时机 / 清理函数 |
react.handler |
事件处理函数 | mouse-pointer-click |
#dcdcaa / #795e26 | 组件体里定义的函数(函数声明、值是函数的常量) | 名称 / 说明 / 参数 / 绑定事件(说明里的块标签另有几行,见下) |
react.render |
渲染输出 | layout-template |
#89d185 / #388a34 | 组件函数自己返回 JSX 的 return | JSX 结构大纲 |
组件色同时用在渲染大纲里的组件、「客户端组件」标签上;状态、引用、数据源、派生值都是数据,共用变量块的浅蓝,靠图标区分;副作用同「选项」块的黄,事件处理函数同「函数」块,渲染输出同「返回」块的绿。筛选栏的「种类」按这几个 id 筛,叫上表的名字。
认哪些是组件:名字大写开头;是函数声明、值是函数的变量(箭头函数、函数表达式,直接返回 JSX 的也算),或被 memo(…) / forwardRef(…)(React.memo 也算,可以套几层)包着的函数;函数自己的 return(嵌套函数里的不算)里有一处返回 JSX(元素、片段,三元、&& 的某一支是 JSX 也算;和 null 混着返回也算)。小写开头的工具函数、不返回 JSX 的大写函数照常是函数块。组件里又定义的组件自己也是一张组件卡片,不算事件处理函数。
认领(LibraryRule.claim 一块一块看,组件卡片的 absorbs 是空的):
- 组件块(函数块、只声明一个变量的常量 / 变量块)是组件时换成组件卡片。文件开头的
"use client"(文件里有组件时)略去(RuleOmit),讲成卡片上的「客户端组件」标签;它挂着注释时不略去(注释没地方放),照常是一张语句卡片。组件上面的 JSDoc 是组件块自己的说明(TypeScript 的 JSDoc 能当常量、变量块的说明,memo(…)、直接返回 JSX 的常量块也是),讲成卡片的「说明」。 - 组件体里的块:语法树往上找,块的语句直接是某个组件函数主体(语句块)的子语句才认(
enclosingComponent:所在的函数经componentOfFunction找回它的声明,再用认组件的componentAt核对),按组件体的归类(bodyOf)认成状态、引用、数据源、派生值、副作用、事件处理函数。 return块:所在的最内层函数(enclosingFunction)是组件函数、返回的是 JSX 时认成渲染输出;写在if里的提前返回 JSX 也算,它是如果卡片下面的子块。返回null这类的照常是返回块。- 别的照语言默认:组件体里普通的
const x = 表达式是变量卡片,if (…) return null是如果卡片、下面是返回卡片,注释是它讲的那张卡片的「注释」一行。 - 回调、函数里的语句来自 TypeScript 语言插件本来给的子块:调用的实参是带语句块的函数时(任何调用都一样),那句语句、那个变量的子块就是函数里的语句,所以
useEffect(() => {…})、useCallback(() => {…})、useMemo(() => {…})的语句排在副作用、派生值卡片下面;事件处理函数是函数块,函数体照常是子块。
组件卡片:
| 行 | 内容 |
|---|---|
| 名称 | 组件名(组件色,带下标);后面跟小标签(tag 框):「具名导出」「默认导出」(同 import / export 关键字的颜色;export default X、export { X } 写在文件别处也算)、「客户端组件」(组件色)、「记忆化」「转发 ref」(同 const 关键字的颜色,memo / forwardRef 包着) |
| 说明 | 组件上面的 JSDoc 的正文,Markdown(块标签拆开放,见下) |
| 对不上的参数 | 说明里 @param 的名字不是哪个属性时才有,同函数块 |
| 构造参数 | N 个(hint);属性类型写的是一个名字(接口、类型别名、导入的类型)时后面写「 · 类型 XxxProps」,名字带下标(⌘ + 点击跳到类型)。下面一张表格「名称 | 类型 | 默认值 | 必填」,有属性带说明时多一列「说明」。没写参数时写「无」。说明里讲整个属性参数的 @param props 跟在这一行的值后面 |
| 异常 | 说明里写了 @throws 才有,同函数块 |
组件说明里的 @param:属性名对上的进「说明」一格(字段自己有注释的照旧用注释,没注释时才用它,Markdown);写成 props.title(属性参数名.属性名)的也算,属性参数是解构写的时点号前面写的那个名字当它的名字。
事件处理函数卡片:名称 / 说明(函数上面 JSDoc 的正文)/ 对不上的参数 / 参数(逗号隔开写在一行;说明里有参数对上了 @param 时排成「名称 | 类型 | 默认值 | 说明」表)/ 返回(写了 @return 才有,值空着、后面跟说明)/ 异常 / 绑定事件。
直接返回 JSX 的箭头函数组件(const Tag = ({ text }) => <span>{text}</span>)没有子块可放,它渲染的大纲作为第四行「JSX 结构大纲」写在组件卡片里。
构造参数:属性类型的字段照类型里的顺序一行一个(类型字面量、同文件的接口 / 类型别名,交叉类型各部分接起来,Readonly<…>、PropsWithChildren<…> 看里面;没标注参数类型时 const X: FC<Props> 的泛型实参、forwardRef<R, Props> 的第二个泛型实参),解构里有、类型里没有的接在后面,...rest 最后(类型写「其余的属性」)。名称是属性名(定义处带下标:解构出来的是解构那处,没解构的是字段;跳到定义只闪这一行),children 后面标「子元素」、函数类型的属性((x) => R、方法签名)标「回调」(组件色的小标签),解构时改了名写「(组件内名为 x)」;类型照翻译插件翻,方法签名写成函数签名;默认值取解构的默认值,没有写「—」;必填看类型的 ?(是 / 否,storage 色),没类型可看时写了默认值的「否」、别的「—」;说明是字段上面的 JSDoc / 注释、这一行行尾的注释(comment 色,几行接成一行)。类型是导入的名字时经工作区索引查它的字段(索引 react.propTypes:看工作区里所有 **/*.{ts,tsx},预筛有 interface X / type X 才解析,按 typescriptreact 解析;从文件顶层认出接口、类型别名,键是名字、值是各字段的名字、类型写法、可省略、回调、说明的 JSON),查法同「规则认领时查别的文件里的定义」:先照解构的出行、类型「未标注」,查到了按字段重排、重发;别的文件里的类型照写文字(type 色)。
状态、引用、数据源、派生值(名称、变量带下标,解构模式照常翻;跳到定义闪这张卡片):
- 状态的初始值:useState 的实参、useReducer 的第二个实参(后面括号里写 reducer)。更新位置:调用改状态函数的各处,只写名字和位置,「、」隔开,写出来一样的只留一处,没有时「—」:「副作用」(几个时「副作用 2」,副作用色)、事件处理函数名 / 派生值名 / 变量名 / 数据源的变量名(带下标,⌘ + 点击跳到那张卡片)、「(排队消息内容) 的 onChange」(绑在 JSX 属性上;元素写成 <code><Tag></code>:组件用组件色、能跳到定义(<code>refers</code> 到标签名),原生标签灰色,带 <code>aria-label</code> / <code>title</code> 字符串时后面写上它;在列表里一项项渲染的前面加「每个」)、「渲染期间,当 … 时」(渲染时按条件重置状态的 <code>if (…) { setX(…) }</code>,这句 if 照常是如果卡片)、「渲染输出」(JSX 里别处用到)。</li> <li>引用的使用位置:「<textarea>(排队消息内容) 的 ref 属性」,别处用到的「;」接在后面、几处「、」隔开:「<main> 的 ref 属性;副作用 1、副作用 2、handleSectionClick」。</li> <li>数据源的钩子是这次调用,照常翻;说明:库自带的钩子照词表(next/navigation 的 <code>usePathname</code> 当前路径、<code>useRouter</code> 路由器,用于页面跳转、<code>useSearchParams</code> URL 查询参数、<code>useParams</code> 路由动态参数、<code>useSelectedLayoutSegment(s)</code> 当前激活的下一级 / 各级布局段,react 的 <code>useId</code>、<code>useTransition</code>、<code>useSyncExternalStore</code>、<code>useOptimistic</code>、<code>useActionState</code>、<code>useDeferredValue</code>,react-dom 的 <code>useFormStatus</code>);<code>useContext(X)</code> 是「X 上下文的值」;传选择器的 <code>useXxx((s) => s.a.b)</code> 是「xxx store 中的 a.b」(store 名:导入路径最后一节是 store 时取上一节,<code>@/lib/desktop/store</code> 是 desktop;否则去掉 store、use 字样,再不行取钩子名),选择器不是直接取属性的写「基于 xxx store 计算 …」;自己项目里的(相对路径、<code>@/</code> 这类别名、同文件)是「自定义钩子」,别的库的是「xxx 提供的钩子」(hint)。</li> <li>派生值的计算表达式:useMemo 是算值的那个表达式(照常翻),主体是语句块时写「N 条语句」;useCallback 记住的函数主体是语句块时写「函数(N 条语句)」,直接返回表达式的照常翻整个函数。依赖项:依赖数组的各项「、」隔开,后面写「(useMemo)」「(useCallback)」;空数组写「无」,没写依赖数组写「未指定」。主体是语句块时里面的语句是这张卡片的子块。</li> </ul> <p dir="auto">副作用:触发时机按依赖数组读,<code>[a, b]</code> 是「a、b 变化时」,<code>[]</code> 是「仅挂载时」,没写是「每次渲染后」(keyword 色),依赖写成别的表达式时「deps 变化时」;useLayoutEffect 后面加「,浏览器绘制前」。最后返回清理函数时那句 return 并进来,多一行「清理函数」:<code>() => …</code> 翻它的主体,语句块一句句翻、「;」隔开。副作用函数里别的语句是子块。</p> <p dir="auto">事件处理函数:名称是函数名(function 色,带下标);参数照常翻(<code>e: KeyboardEvent<HTMLDivElement></code>),几个「, 」隔开,没有写「无」;绑定事件从 JSX 属性反查:「<div>(修改排队消息) 的 onKeyDown」,在属性值里调用时写传入的实参「(传入 h.id)」,在列表里一项项渲染的写「每个 <button>」,几处「、」隔开;没绑在 JSX 上时写由谁调用「由 download、share 调用」,都没有「—」。函数里的语句是子块(如果、返回、调用、变量、尝试……),注释挂在它讲的那块上。</p> <p dir="auto">渲染输出(「JSX 结构大纲」一行下面的子行,一行一个元素,下一层缩进;带下一层的行有折叠箭头):</p> <ul dir="auto"> <li>元素:标签名,原生标签灰色(暗 #808080 / 亮 #6f6f6f),组件用组件色、带下标能跳到定义(<code>a.b</code> 一节节都能跳);<code><></></code> 写「<> 片段,不产生元素」。</li> <li><code>className</code>(<code>class</code>)收成「样式 N 个类」小标(<code>classes</code> 框):N 是写死的类;按条件切换的(模板字符串里的三元、<code>cn(…)</code> / <code>clsx(…)</code> 的实参、<code>cond && "x"</code>、<code>{ "x": cond }</code>)、算出来的表达式在后面写「 · 取决于 doc」(比较的左边、取反的那个,算出来的写它自己);点开在下面弹出全部类名(写死的一行,按条件的「doc 全等 "terms" 时:…」「否则:…」,算出来的「另加 …」),盖住后面的字,再点收起;悬停也能看。</li> <li>别的属性(<code>key</code> 不写,列表那一行写)写成「名字=值」,两个空格隔开:字符串照写,表达式照常翻,跨几行写的对象只写有哪些键「对象:h2、a」,只写了名字的(<code>aria-hidden</code>)就是名字,<code>{...x}</code> 照写。属性值是元素、条件或列表时不写进元素那一行:属性名单独成行,值照同一套大纲缩进在下面。</li> <li>子节点只有文字、插值时写在同一行:只有一段文字时带引号,插值写成 <code>{…}</code>(花括号 hint 色),和文字混着时文字不带引号。</li> <li><code>cond && <X/></code> 前面加「当 … 时」小框(<code>condition</code>,条件照常翻);三元一行「如果 …」,下面第一行各带「是」「否」小框(<code>branch</code>),一支是 null 时写「不渲染」;<code>items.map((x) => …)</code> 一行「对 items 的每一项 x」(<code>iteration</code> 小框)后面「key …」,回调里先算的常量写「其中 isActive = …」,再下面是每一项渲染的;JSX 注释写成一行「// …」(comment 色)。</li> </ul> <p dir="auto">每张卡片「编辑」是它那条语句的完整代码,组件卡片是整个组件(盖住它的子块);组件体里的卡片各自能编辑、问 AI。代码差异在视图数据这一层比:组件卡片的属性表逐行(属性表按名称配对,加的属性标新加的一行),组件体里的卡片是块,照常配对、逐行标(改了依赖,「触发时机」那一行逐截标)。</p> <p dir="auto">样例(<code>fixtures/planChecklist.tsx.txt</code> 的 <code>PlanChecklist</code>,测试里逐字核对;<code>⟦…⟧</code> 是小标签、小框,子块多缩进两格,说明只写第一行):</p> <div data-attr-class="89b22fd727645604:code-block-container code-overflow-scroll"><pre data-attr-class="89b22fd727645604:code-block"><code data-attr-class="89b22fd727645604:chroma language-text" data-code-language="text">react.component 23 名称: PlanChecklist ⟦具名导出⟧ ⟦客户端组件⟧ 说明: 执行期"计划清单"实时勾选 (TodoWrite 式): code-agent 批准计划后边执行边刷新各步 构造参数: 2 个 · 类型 PlanChecklistProps todos | PlanTodo[] | — | 是 | 执行期计划清单 (覆盖式刷新); 空数组时不渲染. active | 布尔 | — | 是 | 本轮是否仍在流式执行中. ``in_progress`` 仅在 ``active`` 时转圈; … react.state 35 名称: collapsed 初始值: 假 更新位置: 副作用、toggle react.effect 37 触发时机: 仅挂载时 try 38: 尝试 if 39: 如果 (localStorage.getItem(PLAN_CHECKLIST_COLLAPSED_KEY) 全等 "1") statement 40: queueMicrotask(() => setCollapsed(真)); catch 42: 捕获 注释: localStorage 不可用 (隐私模式等) 时静默用默认 react.derived 47 名称: toggle 计算表达式: 函数(3 条语句) 依赖项: collapsed(useCallback) variable 48 名称: next 初始值: collapsed 不成立 statement 49: setCollapsed(next); try 50: 尝试 statement 51: localStorage.setItem(PLAN_CHECKLIST_COLLAPSED_KEY, 如果 next 则 "1" 否则 "0"); catch 52: 捕获 注释: 忽略写入失败 if 57: 如果 (todos.length 全等 0) return 57: 返回 空; variable 58 名称: whole 初始值: wholeStatusOf(todos, active) …(doneCount、counter、current、headline 四张变量卡片,headline 那张带「注释」一行) react.render 65 JSX 结构大纲: Collapsible ⟦样式 1 个类 · 取决于 CARD_SURFACE⟧ open=collapsed 不成立 onOpenChange=toggle CollapsibleTrigger ⟦样式 3 个类 · 取决于 FLUSH_ROW_INTERACTIVE⟧ data-testid="plan-checklist-toggle" div ⟦样式 4 个类⟧ data-testid="plan-progress" … </code></pre></div><p dir="auto">和原型不一样的地方:接口 <code>PlanChecklistProps</code> 照 TypeScript 的接口块(一行译文,字段是子块,注释挂在讲的字段上),原型「对照」里画成的字段表(名称 / 类型 / 必填 / 说明)这次没做;渲染输出的大纲挂在「JSX 结构大纲」这一行下面,不是直接画在卡片里;更新位置、使用位置里的「副作用」是副作用色的字,不能跳(副作用没有名字可查定义),原生标签(<code><textarea></code>)也不能跳,组件标签能跳到组件定义。</p> <h3>内置:Rust 的库</h3> <p dir="auto">文件导入了对应 crate 才启用(来源等于 crate 名,或以 <code>crate::</code> 开头)。tokio 不单独做卡片,异步已经写在函数上。样例 <code>fixtures/rustLibs.rs</code>。</p> <ul dir="auto"> <li>serde(<code>rules/serde.ts</code>,<code>libraries: ['serde']</code>):<code>derive</code> 了 <code>Serialize</code> 或 <code>Deserialize</code> 的结构体。种类 <code>serde.struct</code>,标签「可序列化」,图标 <code>braces</code>,颜色同结构体(暗 #4ec9b0 / 亮 #267f99)。名称后面再标一个「可序列化」。<code>rename_all = "camelCase"</code> 收成选项「字段名改驼峰」。字段表多一列「序列化」:<code>rename</code> 写「改名 xxx」,<code>skip</code> / <code>skip_serializing</code> 写「不输出」,没标是「—」。说明里的 <code>@param</code> 同结构体对到字段表的「说明」列。</li> <li>thiserror(<code>rules/thiserror.ts</code>,<code>libraries: ['thiserror']</code>):<code>derive</code> 了 <code>Error</code> 的枚举。种类 <code>thiserror.error</code>,标签「错误」,图标 <code>triangle-alert</code>,颜色同异常(暗 #f14c4c / 亮 #cd3131)。变体表「变体 | 何时 | 展示」:何时是变体上面的文档注释,展示是 <code>#[error("…")]</code> 的文案。变体上的 <code>#[from]</code> 或 <code>#[source]</code> 在展示后面标「透传来源」。</li> <li>axum(<code>rules/axum.ts</code>,<code>libraries: ['axum']</code>):处理函数上的 <code>#[get("/path")]</code>、<code>#[axum::post(…)]</code> 等(get / post / put / delete / patch / head / options / trace),或顶层语句里 <code>Router</code> 的 <code>.route(路径, get(函数))</code>。函数已经标了路由属性时,这句 <code>route</code> 不再单独成卡。种类 <code>axum.route</code>,标签「接口」,图标 <code>globe</code>,颜色同 FastAPI 接口(暗 #61afef / 亮 #1a6fb5)。属性表:名称、说明、异步(是)、请求(一项「方法」、一项「路径」、路径里的 <code>{名字}</code> 对上参数写成「路径参数」,<code>Path(id): Path<Uuid></code> 写 <code>id: Uuid</code>)、参数(说明里写了 <code>@param</code> 才有:参数是 <code>Path(id)</code> 这类模式,不对名字,照写成「名称 | 说明」表)、返回(<code>@return</code> 跟在后面)、异常(<code>@throws</code>)。</li> <li>sqlx(<code>rules/sqlx.ts</code>,<code>libraries: ['sqlx']</code>):<code>query!</code> / <code>query_as!</code> / <code>query_scalar!</code>。种类 <code>sqlx.query</code>,标签「查询」,图标 <code>database</code>,颜色暗 #ce9178 / 亮 #a31515。前面的注释同别的块,是卡片的「注释」一行(宿主插在第一行 SQL 后面)。SQL 原文等宽,参数 <code>$1</code> 对上传入的表达式,<code>query_as!</code> / <code>query_scalar!</code> 的第一个实参是返回类型,<code>query!</code> 的返回类型是「—」。</li> <li>tauri(<code>rules/tauri.ts</code>,<code>libraries: ['tauri']</code>):<code>#[tauri::command]</code> 的函数。种类 <code>tauri.command</code>,标签「桌面命令」,图标 <code>square-function</code>,颜色暗 #e5c07b / 亮 #9a6a00。名称后面再标「桌面命令」。参数表「名称 | 类型 | 来源」:<code>State<…></code> 是「窗口状态」,其余是「调用方传入」;说明里的 <code>@param</code> 对上时多一列「说明」,<code>@return</code> 跟在「返回」后面,<code>@throws</code> 成「异常」表。</li> </ul> <h2>命令规则</h2> <p dir="auto">shell 脚本大半是一条条命令,一条命令读成什么取决于命令本身(<code>rm -rf</code> 是「删除(连同里面的东西、不提示)」,<code>tar -xzf</code> 是「解压 tar.gz」)。这些既不是语法(语言插件不管),也不是某种语言的措辞(翻译插件不该认识几十个命令)。命令规则是第五种插件:按命令名注册,把一条命令连同它的选项读成一句译文,像库规则按库认写法一样。</p> <ul dir="auto"> <li>契约:<code>CommandRule</code>(<code>id</code>、<code>label</code>、<code>languageIds</code>、<code>commands</code>、<code>read</code>,见「通用契约」)。语言插件认出一条命令(<code>LanguageAdapter.command</code> → <code>CommandFacts</code>:命令名、后面的各个词连写死的文字、重定向),翻译插件读到命令时按命令名问 <code>SourceDocument.commandRule</code>,交给规则的 <code>read</code>,拿回一行译文(<code>CommandReading</code>)。规则读选项、选项的值用词的 <code>literal</code>(去掉引号、转义的文字),要翻的词(变量、字符串、展开)交回 <code>CommandContext.translate</code>,名字照样带源码下标;<code>find … -exec rm -rf {} +</code> 里的命令用 <code>CommandContext.command</code> 读。</li> <li>注册:<code>CodeReaderApi.registerCommandRule(rule)</code>,返回 <code>Disposable</code>;内置规则走同一个 API。宿主按语言取注册着的规则(<code>registry.commandRulesFor</code>),按命令名排好(<code>commandRules.ts</code> 的 <code>commandRuleLookup</code>):几条规则管同一个名字时先注册的算。</li> <li>读不了:规则返回 undefined(不认识的写法、不是这种译文语言)或读的时候出错(宿主记下「命令规则 X 读「rm」时出错,照抄命令:…」、当它没读),命令照抄命令名、后面的词照常翻。没有规则的命令一样(<code>foo -x "$y"</code> → <code>foo -x y</code>)。</li> <li>规则不认识的选项照抄在译文后面(<code>rm -rf --one-file-system x</code> → 删除 x(连同里面的东西、不提示) --one-file-system)。</li> <li>重定向不归规则:翻译插件读完命令再统一接上(见「Shell 脚本」),所以每条规则都不用管 <code>>&2</code>、<code>2>/dev/null</code>。</li> <li>译文语言:内置规则只有中文词表,<code>CommandContext.displayLanguage</code> 不是 <code>zh</code> 时不读(照抄)。</li> <li>管哪些语言:内置规则的 <code>languageIds</code> 是 shellscript 和 dockerfile(Dockerfile 数组写法的命令不经过 shell,执行的还是同一些程序)。</li> <li>没有自己的规则的命令交给管 <code>'*'</code> 的规则:内置的只认只写了一个 <code>--version</code>(<code>-V</code>、<code>version</code>)的命令,读成「打印 pg_repack 的版本」,别的照抄。</li> <li>选项多的:规则把认出的选项连同意思交给 <code>commands/reading.ts</code> 的 <code>explained</code>(选项在源码里的写法、写了几遍都由 <code>parseOptions</code> 记下),4 个以上(同一个写几遍算一个)或有写了几遍的,这一行后面写「· N 个选项」,意思排成「选项 | 意思」表(<code>CommandReading.table</code>,翻译插件排在这一行下面,同调用的实参表),写了几遍的并成一行「--no-install-package × 11 | 不装这些包:…」;别的写在括号里。</li> <li>内置规则(文件、下载、打包、文本处理和 shell 自带的在 <code>commands/shellCommands.ts</code>,建用户、密钥、装文件、改属主和 <code>--version</code> 在 <code>commands/systemCommands.ts</code>,装软件包、装依赖、跑程序的在 <code>commands/packageCommands.ts</code>;认选项的共用办法在 <code>commands/reading.ts</code>:<code>-rf</code> 连写拆开,<code>-o 值</code> / <code>-o值</code> / <code>--retry 3</code> / <code>--retry=3</code> 都认,<code>--</code> 之后都不是选项):</li> </ul> <table> <thead> <tr> <th>命令</th> <th>读成</th> </tr> </thead> <tbody> <tr> <td><code>rm -rf dest</code></td> <td>删除 dest(连同里面的东西、不提示)</td> </tr> <tr> <td><code>mkdir -p d</code></td> <td>建目录 d(连同上级目录)</td> </tr> <tr> <td><code>curl -fL --retry 3 -o out url</code>、<code>curl -fsSL url</code></td> <td>下载 url 到 out(失败就报错、跟着跳转走、最多重试 3 次)、请求 url(不打印进度、失败就报错、跟着跳转走;<code>-sS</code> 一起写只写「不打印进度」)</td> </tr> <tr> <td><code>tar -xzf f -C d</code>、<code>unzip -q f -d d</code></td> <td>解压 tar.gz f 到 d、解压 zip f 到 d(不打印过程)</td> </tr> <tr> <td><code>find d -mindepth 1 -maxdepth 1 -type d ! -name m -exec rm -rf {} +</code></td> <td>找出 d 下一层的目录,除了 m,逐个 删除(连同里面的东西、不提示);<code>find d -maxdepth 1 -name '*.dll' -delete</code> → 找出 d 这一层的 *.dll,逐个 删除;<code>find d -type l</code> → 找出 d 下面所有符号链接</td> </tr> <tr> <td><code>cp -L a b</code>、<code>mv a b</code>、<code>mv a b d</code></td> <td>复制 a 指向的实体文件 到 b、改名 a → b、移动 a、b 到 d</td> </tr> <tr> <td><code>echo "下载 $url"</code>、<code>echo "$out"</code>、<code>printf '%s\n' "$x"</code></td> <td>打印 "下载 url"(有写死的文字时连引号)、打印 out、按格式打印 "%s\n",填入 x</td> </tr> <tr> <td><code>exit 1</code>、<code>return 1</code></td> <td>退出,退出码 1、返回 1</td> </tr> <tr> <td><code>cd d</code>、<code>pwd</code></td> <td>进入 d、打印当前目录</td> </tr> <tr> <td><code>basename x</code>、<code>dirname x</code>、<code>dirname "$0"</code></td> <td>取文件名 x、取所在目录 x、脚本所在目录(读出来是一个东西,<code>$(…)</code> 直接写它)</td> </tr> <tr> <td><code>readlink x</code>、<code>readlink -f x</code></td> <td>读出 x 指向的路径、取 x 的真实路径</td> </tr> <tr> <td><code>shasum -a 256 f</code>、<code>sha256sum f</code>、<code>md5sum f</code></td> <td>算 f 的 sha256、算 f 的 md5</td> </tr> <tr> <td><code>cut -d' ' -f1</code>、<code>cut -c1-3 f</code></td> <td>按空格切,取第 1 段、取 f 每一行的第 1 到 3 个字符</td> </tr> <tr> <td><code>du -sh a b</code></td> <td>统计占用空间 a、b(按人读的单位、只看总数)</td> </tr> <tr> <td><code>cat f</code>、<code>grep -qi error log</code>、<code>sed -i 's/a/b/g' conf</code></td> <td>打印 f 的内容、在 log 里找含 error 的行(只看找没找到、不分大小写)、把 conf 里的 a 全换成 b(直接改文件)</td> </tr> <tr> <td><code>chmod +x f</code>、<code>ln -sf t link</code>、<code>touch f</code></td> <td>给 f 加上可执行权限、建符号链接 link → t(已有就替换)、更新修改时间 f(没有就建空文件)</td> </tr> <tr> <td><code>source f</code> / <code>. f</code>、<code>read -r x</code>、<code>sleep 1</code></td> <td>读入 f 里的变量和函数、读一行进 x(不认转义)、等 1 秒</td> </tr> <tr> <td><code>continue</code>、<code>break</code>、<code>true</code> / <code>:</code>、<code>false</code></td> <td>跳过这一个、跳出循环、什么也不做、失败</td> </tr> <tr> <td><code>apt-get update</code>、<code>apt-get install -y --no-install-recommends a b</code>、<code>apt-get purge -y x</code>、<code>apt-get clean</code></td> <td>更新软件包列表、安装 a、b(不装推荐的包、不用确认)、卸载 x(连配置一起删、不用确认)、清掉下载的安装包</td> </tr> <tr> <td><code>apk add --no-cache --virtual .deps gcc</code>、<code>apk del .deps</code></td> <td>安装 gcc(不留索引缓存、记成一组 .deps)、卸载 .deps</td> </tr> <tr> <td><code>pip install --no-cache-dir -r requirements.txt</code>、<code>pip3 install -U pip</code></td> <td>pip 安装 requirements.txt 里列的包(不留缓存)、pip 安装 pip(升级到最新)</td> </tr> <tr> <td><code>npm ci --omit=dev</code>、<code>npm run build</code>、<code>npm run migrate</code></td> <td>npm 按 lock 文件装依赖(不装开发依赖)、npm 构建、npm 运行脚本 migrate(build / test / dev / start / lint 这些常见的脚本名写成要做的事)</td> </tr> <tr> <td><code>pnpm install --frozen-lockfile --ignore-scripts</code>、<code>pnpm build</code>、<code>yarn install --immutable</code></td> <td>pnpm 装依赖(严格按 lock 文件、不跑安装脚本)、pnpm 构建、yarn 装依赖(严格按 lock 文件)</td> </tr> <tr> <td><code>corepack enable</code>、<code>corepack prepare pnpm@latest --activate</code></td> <td>开启 corepack、准备 pnpm latest(并设为当前用的版本)</td> </tr> <tr> <td><code>uv sync --frozen --no-dev</code>、<code>uv pip install --system -r req.txt</code>、<code>uv run --no-sync uvicorn app:app --host 0.0.0.0 --port 8000</code></td> <td>uv 按 lock 同步依赖(严格按 lock 文件,不改它、不装开发依赖)、uv 安装 req.txt 里列的包(装进系统的 Python)、用 uv 运行(不先同步依赖) uvicorn app:app,监听 0.0.0.0:8000(<code>uv run</code> 后面的命令照它自己的规则读)</td> </tr> <tr> <td><code>useradd -m -s /bin/false appuser</code>、<code>adduser --system --uid 1001 nextjs</code>、<code>addgroup --system --gid 1001 nodejs</code>、<code>adduser web www</code></td> <td>新建用户 appuser(建主目录、不能登录)、新建系统用户 nextjs(uid 1001)、新建系统用户组 nodejs(gid 1001)、把 web 加进组 www(Debian 和 Alpine / BusyBox 的写法都认)</td> </tr> <tr> <td><code>gpg --dearmor -o k.gpg</code>、<code>install -d /d</code>、<code>install -m 644 a /etc/a</code>、<code>chown -R u:g /x</code></td> <td>转成二进制密钥 写到 k.gpg、建目录 /d、安装 a 到 /etc/a(权限 644)、把 /x 的属主改成 u:g(连同里面的东西)</td> </tr> <tr> <td><code>pg_repack --version</code></td> <td>打印 pg_repack 的版本(管 <code>'*'</code> 的规则)</td> </tr> </tbody> </table> <h2>解析运行时</h2> <ul dir="auto"> <li>依赖 <code>@vscode/tree-sitter-wasm</code>(VS Code 团队维护,包含 web-tree-sitter 运行时和 TypeScript / TSX / JavaScript / Python / Bash 语法包,版本配套)。它没有的语法包(Dockerfile)取自开发依赖 <code>tree-sitter-wasm</code>(Crysthamus/tree-sitter-wasm:各家语法包在 GitHub Actions 里编译,上游提交钉死,每个 wasm 都用 cosign 签名、发 npm 带来源证明;tree-sitter-dockerfile 是 ABI 15,和 <code>@vscode/tree-sitter-wasm</code> 的运行时配得上),<code>esbuild.mjs</code> 用它的 <code>getWasmPath</code> 取出来,和别的一起复制进 <code>dist/tree-sitter/</code>;单元测试直接用包里的(<code>testSupport.ts</code> 的 <code>DOCKERFILE_WASM_DIR</code>)。</li> <li>打包:<code>esbuild.mjs</code> 构建时把 <code>tree-sitter.wasm</code> 和用到的语法包复制到 <code>dist/tree-sitter/</code>(<code>package.json</code> 的 <code>files</code> 已包含 <code>dist/**/*</code>)。</li> <li>运行时初始化一次 <code>Parser.init({ locateFile })</code> 指向 <code>dist/tree-sitter/tree-sitter.wasm</code>;语法包按路径懒加载并缓存。</li> <li>文件扩展名到语法包:<code>.ts</code> → typescript;<code>.tsx</code> → tsx;<code>.js/.jsx/.mjs/.cjs</code> → javascript(tree-sitter-javascript 支持 JSX);<code>.py</code> → python;shellscript(<code>.sh</code> / <code>.bash</code> / <code>.zsh</code>、首行写着 shell 解释器的文件)→ bash;dockerfile(<code>Dockerfile</code>、<code>Dockerfile.*</code>、<code>*.dockerfile</code>、<code>Containerfile</code>)→ dockerfile。</li> <li>嵌着的代码只解析源码里的几段(<code>TreeSitterRuntime.parse</code> 的 <code>ranges</code>,交给 web-tree-sitter 的 <code>includedRanges</code>),节点下标、行列照旧是整份源码的。以 <code>languageId</code> 为准。</li> </ul> <h2>代码块树规则</h2> <h3>注释挂到它讲的块上</h3> <p dir="auto">注释一定是讲某块代码的,和语言无关,各语言插件建好整棵树后调一次 <code>languages/shared.ts</code> 的 <code>attachComments</code>(Python、TypeScript 在认异常类之前调,异常类并类属性时拿到的是挂好注释的块):</p> <ul dir="auto"> <li>先照各语言的规矩分好注释块(<code>listBlocks</code>:相邻行同一写法的行注释合成一段,和上一条语句同一行的是行尾注释、不成块,文档注释能当说明的进 <code>facts.doc</code>;Python 当注释用的字符串也是注释块)。</li> <li>再把每段注释挂到它讲的块上(<code>BlockShape.comments</code>,照源码顺序),不再单独成块: <ol> <li>同一层(同一个父块的子块,文件顶层也算一层)里它后面最近的一块代码,中间隔着空行也算;挂在同一块前面的几段各是一段。</li> <li>后面没有代码了(主体、文件最后的注释)挂到同一层前面最近的那块;这一层一块代码都没有(主体里只有注释)挂到父块。</li> <li>文件开头的说明单独成块(种类 comment):第一块代码之前的注释(<code>#!</code> 解释器、Dockerfile 的解析指令不算代码,Python 第一行的 <code>#!</code>、头两行写编码的 <code># -*- coding: … -*-</code> 也是文件的),和第一块代码之间隔着空行的那几段;紧挨着第一块代码(中间没空行)的那几段照第 1 条挂到它上面。本来就讲整个文件的总是文件开头的说明:Python 模块开头的 docstring、Rust 的 <code>//!</code> / <code>/*!</code>、写了 <code>@file</code> / <code>@fileoverview</code> / <code>@overview</code> 的 JSDoc;Rust 的 <code>//!</code> 写在 <code>mod</code> 身体里时交给那个模块。</li> <li>嵌在别的语言里的代码(Dockerfile 的 RUN 里的 shell)没有文件开头的说明,一块代码都没有时注释照旧单独成块(读成步骤时不算一步)。</li> </ol> </li> <li>文档注释是块的「说明」(照旧进 <code>facts.doc</code>、带块标签),普通注释是块的「注释」,一块可以两样都有。行尾注释照旧跟着那一行。</li> <li>块的完整范围(编辑范围)连同挂在它上面的注释(见「预览(代码块树)」);块的记号(问答挂的地方)只看块自己,改注释不丢问答。</li> <li>库规则拿到的块已经挂好注释:并成员时讲成员的注释跟着成员(内置规则拿它当成员的说明);规则略去的块不能挂着注释。</li> </ul> <h3>TypeScript / JavaScript</h3> <ul dir="auto"> <li>顶层与语句块内的每条语句一块;连续的行注释合成一段注释,挂到它讲的块上(见「注释挂到它讲的块上」)。</li> <li>JSDoc(以 <code>/**</code> 开头的块注释,<code>/**/</code> 不算)和下一条语句之间只有空白(空行也算,中间隔着装饰器也算)、那条语句是函数、类、枚举、常量或变量块时,它是这一块的说明:进 <code>facts.doc</code>,头部往上延到注释开头。其余情况(后面不是这几种块,中间隔着别的注释)是普通注释,挂到它讲的块上;在上一条语句的行尾的是行尾注释。<code>//</code> 行注释和普通 <code>/* */</code> 块注释是普通注释。</li> <li><code>import_statement</code> → import。</li> <li><code>export_statement</code> 包着声明 → 按声明定种类,节点取整个 <code>export_statement</code>(头部包含 <code>export</code>)。</li> <li><code>function_declaration</code> / <code>generator_function_declaration</code> / <code>method_definition</code> → function,子块来自主体语句。</li> <li>变量声明的唯一声明值是带语句块主体的箭头函数 / 函数表达式 → function(React 组件、回调常见写法),子块来自主体;是类表达式(<code>const A = class {}</code>)→ class。</li> <li><code>lexical_declaration</code> / <code>variable_declaration</code>:模块顶层的 <code>const</code> → const,其余 → variable。</li> <li><code>class_declaration</code> / <code>abstract_class_declaration</code>、<code>export default class {}</code> → class,子块:方法 → function,字段 → field(值是带语句块主体的函数 → function,值是类表达式 → class),注释挂到讲的成员上。</li> <li>类(含类表达式)的父类是异常类时(见「异常」)→ exception:头部是整个类,字段连同讲它的注释并进属性表,方法(含 constructor)、别的成员照常是子块。整棵树建好后按整份文件认(同一文件里的异常类写在后面也算)。</li> <li><code>interface_declaration</code> → type,子块:属性 / 方法签名 → field;<code>type_alias_declaration</code> → type;<code>enum_declaration</code>(含 <code>const enum</code>、<code>declare enum</code> 的 <code>ambient_declaration</code>、<code>export</code> 包着的)→ enum,头部是整条声明,成员在属性表里讲、不成子块。</li> <li><code>if_statement</code> → if,子块来自 consequence;<code>else</code> / <code>else if</code> 展平成紧跟其后的兄弟块 else(<code>else if</code> 的头部是 <code>else if (...)</code>,子块来自它的 consequence)。</li> <li><code>for</code> / <code>for…in</code> / <code>for…of</code> / <code>while</code> / <code>do</code> → loop。</li> <li><code>try_statement</code> → try;<code>catch_clause</code> → 兄弟块 catch;<code>finally_clause</code> → 兄弟块 finally。</li> <li><code>switch_statement</code> → if,每个 <code>case</code> / <code>default</code> → 子块 if。</li> <li><code>return_statement</code> → return;其余语句(含 <code>throw</code>、单独一个字符串的语句:<code>'use strict'</code> 这类指令,TypeScript / JavaScript 没有拿字符串当说明的写法)→ statement。</li> <li>表达式语句的参数里带语句块主体的函数(<code>useEffect(() => { … })</code>、<code>describe('x', () => { … })</code>)→ statement,子块来自那个函数主体,头部到主体开始为止。</li> </ul> <h3>Python</h3> <ul dir="auto"> <li><code>import_statement</code> / <code>import_from_statement</code> / <code>future_import_statement</code> → import。</li> <li><code>function_definition</code> → function;<code>decorated_definition</code> 按内部定义定种类,头部包含装饰器。</li> <li><code>class_definition</code> → class;类体里的带注解赋值 → field。</li> <li>父类是异常类的类(见「异常」)→ exception:头部是整个类,类属性连同讲它的注释并进属性表,方法(含 <code>__init__</code>)、别的语句照常是子块。整棵树建好后按整份文件认(同一文件里的异常类写在后面也算)。</li> <li>字符串语句:单独一个普通字符串字面量的表达式语句(<code>string</code>,或隐式拼接的 <code>concatenated_string</code>,每段的前缀只有 r / u 或没有;f-string、t-string、字节串不算,照旧是 statement)。</li> <li>函数、类主体的第一条语句(注释不算)是字符串语句时,它是这个函数、类的 docstring:进 <code>facts.doc</code>,头部往下延到字符串结尾(它和冒号之间的注释也在头部里),子块从它后面开始,不单独成块。</li> <li>属性 docstring:模块、类主体里(条件、函数等语句块里的不算)紧跟在一条赋值后面的字符串语句是这个常量、变量、字段的说明:常量、变量进 <code>facts.doc</code>,字段进块的 <code>doc</code>,头部往下延到字符串结尾,不单独成块。「紧跟」同 JSDoc:中间可以隔空行和赋值的行尾注释,隔着别的注释、语句就不算;只挂一个,后面再来的字符串照下一条规则。</li> <li>其余字符串语句(模块开头的 docstring、写在语句中间当注释用的字符串)和 <code>comment</code> → 注释:模块开头的 docstring、第一行的 <code>#!</code>、头两行写编码的注释是文件开头的说明,别的挂到它讲的块上(见「注释挂到它讲的块上」)。<code>#!</code> 和写编码的那两行不和后面的 <code>#</code> 注释连成一段。</li> <li>赋值:模块顶层且变量名全大写 → const,其余 → variable。</li> <li><code>if_statement</code> → if;<code>elif_clause</code> / <code>else_clause</code> → 兄弟块 else;<code>for</code> / <code>while</code> → loop(循环的 <code>else</code> → 兄弟块 else)。</li> <li><code>try_statement</code> → try;<code>except_clause</code> → catch;<code>finally_clause</code> → finally。</li> <li><code>with_statement</code> → statement,子块来自主体;<code>match_statement</code> → if,<code>case</code> → 子块 if。</li> <li><code>return_statement</code> → return;其余 → statement。</li> <li>复合语句头部到主体开始为止(包含行尾冒号,翻译时去掉)。</li> </ul> <h3>Shell</h3> <ul dir="auto"> <li>首行 <code>#!</code> → interpreter;别的 <code>comment</code> 连着的几行合成一段注释,挂到它讲的块上(见「注释挂到它讲的块上」);紧贴在函数定义上面(中间没空行)的注释是函数的说明,进 <code>facts.doc</code>、头部往上延。</li> <li><code>set</code> 后面全是开关(<code>-euo pipefail</code>、<code>+x</code>、<code>-o nounset</code>)→ options;有不是开关的词(<code>set -- a b</code>)→ command。</li> <li><code>variable_assignment</code>、<code>declaration_command</code>(local / export / readonly / declare 声明了变量的)→ variable;readonly 的、脚本顶层名字全大写且值里没有展开的 → const。一句声明几个变量是一块(<code>DeclaratorFacts</code> 各一项)。</li> <li><code>function_definition</code> → function,子块来自主体各句。</li> <li><code>if_statement</code> → if,子块是 then 后面的各句;<code>elif_clause</code> / <code>else_clause</code> 展平成紧跟的兄弟块 else。</li> <li><code>for_statement</code> / <code>c_style_for_statement</code> / <code>while_statement</code>(until 也是)→ loop,头部到 do 为止,子块是 do … done 里的各句。</li> <li><code>case_statement</code> → case:每个情况都短(最多 3 句,都是一行写完的命令、赋值、声明,没有注释)时整条一块;否则头部到 in 为止,每个 <code>case_item</code> 一块子块 case(头部是它的模式,到 <code>)</code> 为止),子块是要做的各句。</li> <li><code>pipeline</code> → pipeline;最后一段是循环时头部到 do 为止,循环主体是子块。</li> <li><code>[ … ]</code>、<code><a href="/cruldra/superwork/src/commit/95559b160b300c54a86ea39b42d5a9bfee5f5828/docs/superpowers/specs/code-reader/-%E2%80%A6-"> … </a></code>(前面可以带 <code>!</code>),和用 <code>&&</code> / <code>||</code> 连起来、全是条件或前面全是条件最后跟一句的一串 → if。</li> <li><code>return</code> 命令 → return;别的命令(带不带重定向)、<code>&&</code> / <code>||</code> 连起来的一串命令 → command;其余语句 → statement。</li> </ul> <h3>Dockerfile</h3> <ul dir="auto"> <li>文件最开头连着的 <code># syntax=</code> / <code># escape=</code> / <code># check=</code>(从第一行起、中间没空行)→ directive。</li> <li>每个 <code>from_instruction</code> 开一个 stage,头部是 FROM 那一行,紧贴在上面(中间没空行)的几行注释是它的说明、头部往上延;到下一个阶段(连同它上面的说明)之前的指令、注释是子块;第一个 FROM 之前的照常是顶层的块。</li> <li>相邻行的注释合成一段注释,挂到它讲的块上(见「注释挂到它讲的块上」:阶段里的挂到后面那条指令,阶段最后的挂到前面那条);只写了中文名记号、紧挨着下一条指令的注释算在那条指令的块里。</li> <li><code>run_instruction</code> → run(整条指令,shell 写法的带 <code>embedded</code>);<code>copy_instruction</code>、<code>add_instruction</code> → copy;<code>label_instruction</code> → label;<code>env_instruction</code>、<code>arg_instruction</code> → variable(facts 的 scope 分构建参数、环境变量);WORKDIR / USER / EXPOSE / VOLUME / STOPSIGNAL / SHELL / MAINTAINER → setting;<code>healthcheck_instruction</code> → health;<code>cmd_instruction</code>、<code>entrypoint_instruction</code> → start;<code>onbuild_instruction</code> → setting,包着的指令照这些规则是它的子块;别的(语法错误)→ statement。</li> <li>连着写的同种指令(紧挨着下一行)并成一块:ENV、ARG 并成一个变量块(declarators 是各条的全部),LABEL 和从同一个地方复制的 COPY 并成一块(<code>TextBlock.group</code>,头部盖住全部);ADD、带 heredoc 的 COPY 不并。</li> </ul> <h3>Rust</h3> <p dir="auto">语法包是 <code>@vscode/tree-sitter-wasm</code> 里的 <code>tree-sitter-rust.wasm</code>(和 python 的 wasm 同一层:包入口就在 <code>wasm/</code>)。<code>languageIds: ['rust']</code>。样例 <code>fixtures/rustLang.rs</code>、<code>fixtures/rustLibs.rs</code>、<code>fixtures/rustComments.rs</code>。原型是 <code>docs/prototype/code-reader/openpencil/exports/</code> 的 <code>rust-lang.png</code>、<code>rust-lib.png</code>、<code>rust-notes.png</code>。中文直译 <code>translators/rustZh.ts</code>:结构体不叫「类」,可见性不叫「访问级别」,<code>pub</code> 是「公开」。</p> <p dir="auto">可见性:<code>pub</code> 公开,没写是本模块,<code>pub(super)</code> 父模块,<code>pub(crate)</code> 和 <code>pub(in …)</code> 本 crate。公开写在 <code>facts.access</code>,其余写在块的 <code>access</code> 属性上(筛选值 <code>module</code> / <code>super</code> / <code>crate</code>)。说明是紧挨在前面的文档注释(<code>///</code>、<code>/**</code>)正文的第一句,句号留着;连着几行的 <code>///</code> 算一段(<code>facts.doc</code> 是第一条,中文直译的 <code>docNodes</code> 往下连),中间可以隔着 <code>#[…]</code>。卡片(含 Rust 库规则的)都经翻译插件的 <code>doc</code>(规则经 <code>context.doc</code>)读说明,块标签拆开放:函数的 <code>@param</code> 进参数表的「说明」列、<code>@return</code> 跟在「返回」后面、<code>@throws</code> 成「异常」表,结构体的 <code>@param</code> 对到字段表的「说明」列,对不上的列成「对不上的参数」。</p> <p dir="auto">卡片(含 Rust 库规则的)里照抄源码的类型、签名都经翻译插件的 <code>verbatim</code>(规则经 <code>context.verbatim</code>)照抄成一行:参数类型、「返回」、字段类型、变体携带的数据、方法表的参数和返回、导入的来源、tauri 的参数类型、axum 的路径参数、sqlx 的返回类型。名字一个个各自带源码下标、按词性着色(<code>State<'_, AppState></code> 里 <code>State</code>、<code>AppState</code> 各自能悬停、⌘ + 点击跳到定义,导入来源 <code>std::collections</code> 每一节各自能点、按类型着色),跨几行写的压成一行;没写返回类型的 <code>fn</code> 的 <code>()</code> 是普通文字。</p> <p dir="auto">注释和别的语言走同一条路(<code>listBlocks</code>,Rust 的 <code>CommentRules</code> 在 <code>languages/rust.ts</code>),文件顶层、<code>mod</code> 身体、<code>trait</code> / <code>impl</code> 身体、函数体(含条件、match、循环里的各层)都一样:</p> <ul dir="auto"> <li> <p dir="auto">连续的行注释合成一段注释:相邻行、写法相同才合,<code>//</code>、讲所在模块或 crate 的 <code>//!</code>、讲下一项的 <code>///</code> 各是一种写法,不混着合。中间空的 <code>//</code> 不打断,是 Markdown 里的空行、分段;隔着空行、隔着别的语句就断开。合好的注释挂到它讲的项上(见「注释挂到它讲的块上」);<code>//!</code> 写在文件开头是文件的说明,写在 <code>mod</code> 身体里交给那个模块。</p> </li> <li> <p dir="auto">和上一条语句同一行的 <code>//</code> 是行尾注释,不单独成块。</p> </li> <li> <p dir="auto"><code>///</code>(和 <code>/**</code> 开头的块注释)后面紧跟的是函数、结构体、枚举、特质、实现、模块、常量、<code>static</code> 时,是这一项的说明:进 <code>facts.doc</code>(第一条注释节点),头部往上延到它开头;后面是 <code>use</code>、宏调用、<code>let</code> 这类不带说明的项,或隔着别的注释时,是普通注释,挂到它讲的项上。</p> </li> <li> <p dir="auto">注释的预览(「注释」一行、文件开头的说明)是去掉 <code>//</code>(<code>///</code>、<code>//!</code> 连那个 <code>/</code>、<code>!</code>)和后面一个空格的原文,按「注释的 Markdown 排版」显示。语法包把 <code>///</code>、<code>//!</code> 行尾的换行算进注释节点,块的范围不算这个换行。</p> </li> <li> <p dir="auto"><code>use</code> → import。来源是路径去掉最后一节(<code>std::sync::Arc</code> 的来源是 <code>std::sync</code>)。导入项表「名字 | 别名」,没起别名是「—」,<code>*</code> 是「*」。</p> </li> <li> <p dir="auto"><code>struct</code> → class(中文直译把这个种类叫「结构体」)。字段不单独成块,表「字段 | 类型 | 可见性」。元组结构体没有这张表。</p> </li> <li> <p dir="auto"><code>enum</code> → enum。变体表「变体 | 携带的数据」:没带数据是「—」,<code>Failed(String)</code> 写类型,<code>Running { pid: u32 }</code> 写字段原文。</p> </li> <li> <p dir="auto"><code>fn</code> → function。有才显示:说明、异步(只有 <code>async fn</code> 写「是」)、可见性(特质里、<code>impl Trait for Type</code> 里的方法跟着特质,没有这一行)、输入参数表「名称 | 类型」、返回(没写是 <code>()</code>)。函数体各句是子块(自由函数、<code>impl</code> / <code>trait</code> 里的方法、嵌套的 <code>fn</code>、闭包都拆),同 Python / TypeScript 的函数:</p> <ul> <li><code>let</code> → variable(名称是模式,<code>let Some(x) = …</code> 就是 <code>Some(x)</code>);<code>let … else { … }</code> 的 else 照 if / else 的写法成一块 else,是这个变量块唯一的子块(节点仍是整条 <code>let</code>,头部只有 else 这个词)。值是主体写成 <code>{}</code> 的闭包时是 function(名称是变量的模式,没有可见性,写了返回类型才有「返回」),主体不是 <code>{}</code> 的闭包照旧是 variable,同 TypeScript 的箭头函数。</li> <li><code>if</code> / <code>if let</code> 和 <code>else if</code> / <code>else</code> 展平成兄弟块 if / else,同 TypeScript;<code>match</code> 一块 if、每一支一块 if,同 Python 的 match / case、TypeScript 的 switch / case,一支的头部是模式和守卫(到 <code>=></code> 之前),子块来自 <code>=></code> 后面的值(<code>{}</code> 里的各句,不是 <code>{}</code> 时这个值本身一块)。</li> <li><code>loop</code> / <code>while</code> / <code>while let</code> / <code>for</code> → loop;<code>return</code> → return;<code>break</code>、<code>continue</code>、调用、宏调用、<code>?</code>、赋值这些表达式语句 → statement。</li> <li>隐式返回值:函数体、闭包体、async 块最后那个不带分号的式子是 return,译文前面补「返回」;排在最后的 if / match / <code>{}</code> / <code>unsafe {}</code> 自己不是 return,照控制流拆,把「最后那个式子是返回值」传给各支;循环体、<code>let … else</code> 的 else 里不传。</li> <li><code>unsafe {}</code>、<code>async {}</code>、带标签的 <code>'a: {}</code> → 带子块的 statement,头部是 <code>unsafe</code> / <code>async move</code> / 标签;前面什么都没写的 <code>{}</code> 同 TypeScript 展平成各句。</li> <li>表达式语句、<code>let</code> 的值带着闭包、async 块时(值本身,或调用的实参里第一个:<code>tokio::spawn(async move { … })</code>、<code>.for_each(|x| { … })</code>,<code>.await</code>、<code>?</code> 看里面的调用),同 TypeScript 的回调,这句照常一行,子块来自那个 <code>{}</code>。</li> <li>块里写的项(嵌套的 <code>fn</code>、<code>struct</code>、<code>use</code>、<code>const</code>……)跟文件顶层同一条路;注释同上面几条。</li> <li>调用(<code>LanguageAdapter.call</code>):<code>call_expression</code> 的被调用者和实参(都是按位置传的),名字取路径、方法、泛型函数的最后一节;<code>Some(x)</code> 这类写法和调用一样,不给 construct;宏调用不算。跨几行写的调用同别的语言排成「调用 … · N 个参数」加实参表。</li> </ul> </li> <li> <p dir="auto"><code>trait</code> → trait(图标 <code>network</code>,颜色同声明关键字的蓝)。同 <code>impl</code>:身体里的各项是子块,规则 <code>rules/rustImpl.ts</code>(种类 <code>rust.trait</code>,徽章「特质」暗 #569cd6 / 亮 #0000ff)把每张方法卡挂在方法表「名称 | 参数 | 返回」讲它的那个子行上,带默认实现的方法展开后有函数体子块。</p> </li> <li> <p dir="auto"><code>impl</code> → impl(图标 <code>square-function</code>,颜色同函数)。标题是「某类型 的方法」(<code>impl Trait for Type</code> 取 Type)。方法是子块。不看导入的规则 <code>rules/rustImpl.ts</code>(<code>libraries: []</code>,种类 <code>rust.impl</code>)把每张方法卡挂在方法表讲它的那个子行上(<code>RuleSlot</code>),点方法名展开。方法卡的参数表叫「参数」。身体里的 <code>const</code>、<code>static</code> 不收成语句,跟文件顶层同一条路(见下)。</p> </li> <li> <p dir="auto"><code>mod 名字 { … }</code>(<code>mod_item</code> 有 <code>body</code>)→ module(中文「模块」,图标 <code>layers</code>,颜色同阶段)。卡片行:名称、说明(有文档注释才有)、可见性(没写也是本模块,跟结构体一样总有这一行)。身体用和文件顶层同一套规则拆成子块,排在这张卡片下面,不另做一张表把子块塞进某一行。模块自己的文档注释留在模块卡片上,身体里每项自己的文档注释留在那一项上。<code>mod 名字;</code> 没有身体,不拆,仍是一条 statement。</p> </li> <li> <p dir="auto"><code>const</code>(<code>const_item</code>)→ const,<code>static</code>(<code>static_item</code>)→ variable。属性表不另写一套译文,走中文模板里常量 / 变量那一套:名称、说明、访问级别(公开才有,模板里叫「公共」)、类型、值(<code>static</code> 的值叫「初始值」)。文件顶层、模块身体、impl 身体同一条路。</p> </li> <li> <p dir="auto"><code>extern "…" { … }</code>(<code>foreign_mod_item</code>)不单独成一块源码。里面的函数签名(<code>function_signature_item</code>,没有函数体)是函数卡片,跟普通函数一样有名称、说明、可见性、参数、返回,作为所在这一层(模块、文件顶层或 impl)的子块。</p> </li> <li> <p dir="auto">内部属性 <code>#![…]</code>(<code>inner_attribute_item</code>)→ attribute(中文「编译属性」,图标 <code>sliders-horizontal</code>,颜色同常量词性的蓝 <code>--cr-constant</code>:暗 #4fc1ff / 亮 #0070c1,和特质的声明关键字蓝、选项的黄分开)。它写给所在的 crate 或模块,例如 Tauri <code>main.rs</code> 开头的 <code>#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]</code>。语言插件把 <code>cfg_attr(条件, a, b)</code> 拆成生效条件和 a、b 两个属性(<code>AttributeFacts</code>),别的属性就是它自己一个;<code>cfg_attr</code> 只写了条件、没写属性的照一个属性算。卡片行(有才显示):</p> <ul> <li>属性:属性名连参数照原文着色(<code>windows_subsystem = "windows"</code>、<code>allow(dead_code, unused_imports)</code>)。<code>cfg_attr</code> 里写了几个属性时值是「N 个」,下面一张「属性 | 说明」表,每个属性一行(认不得的说明写「—」),不再另起「说明」一行。</li> <li>生效条件:只有 <code>cfg_attr</code> 有。<code>debug_assertions</code> 是「调试构建」,<code>test</code> 是「测试构建」,<code>target_os = "windows"</code> 是「目标系统 windows」,<code>feature = "x"</code> 是「启用特性 x」,<code>not(x)</code> 是「非 x」,<code>all(…)</code> 用「并且」、<code>any(…)</code> 用「或者」连起来,套在别的条件里、有两项以上的加括号(「unix 并且 非 (测试构建 或者 启用特性 mock)」);别的配置项、认不得的写法照原文。「非」「并且」「或者」按 operator 着色,特性名、系统名按 string。</li> <li>作用范围(按 keyword 着色):写在文件顶层时,crate 根文件是「整个 crate」,别的文件是「本模块」,宿主没给文件名(<code>SourceDocument.fileName</code>)时是「所在模块」;写在 <code>mod</code> 身体里是「本模块」,<code>impl</code> 身体里是「本 impl 块」。crate 根文件按 Cargo 默认的认法(<code>rules/rustTree.ts</code> 的 <code>isCrateRoot</code>):<code>main.rs</code>、<code>lib.rs</code>,<code>src/bin/</code> 下的 <code>.rs</code>,包根下 <code>examples/</code>、<code>tests/</code>、<code>benches/</code> 里直接放的 <code>.rs</code>(<code>src/</code> 下同名目录里的是模块)。</li> <li>说明(按 comment 着色,列出的名字按 variable):认得的属性才有。<code>windows_subsystem = "windows"</code>「在 Windows 上编译为图形界面程序(windows 子系统),启动时不弹出控制台窗口」,<code>= "console"</code>「在 Windows 上编译为控制台程序(console 子系统),启动时打开控制台窗口」;<code>allow</code> / <code>warn</code> / <code>deny</code> / <code>forbid</code>「编译检查(lint)级别设为放行 / 警告 / 报错 / 强制报错(内层不能再放宽):」后面列括号里的各项;<code>feature</code>「启用实验特性(需 nightly 编译器):」后面列名字;<code>no_std</code>「不链接标准库 std,只使用核心库 core」;<code>no_main</code>「不使用标准的 main 入口函数,程序入口另行提供」;<code>recursion_limit = "N"</code>「宏展开等编译期递归的深度上限设为 N(默认 128)」。</li> <li>挂在函数、结构体上的外部属性 <code>#[…]</code> 不变:算进那一项的头部,<code>derive</code>、库规则照旧。</li> </ul> </li> <li> <p dir="auto">文件顶层、模块身体里的其余(<code>let</code>、宏调用)→ statement。模块身体里宏调用后面多出来的那个空 <code>;</code>(<code>empty_statement</code>)不成块。</p> </li> </ul> <h3>属性表信息(facts)从哪来</h3> <ul dir="auto"> <li>TypeScript 函数:<code>function_declaration</code> / <code>generator_function_declaration</code> / <code>function_signature</code>、类里的 <code>method_definition</code> / 方法签名、值是带语句块主体函数的变量和类字段、<code>export default</code> 的函数值。名称取声明名 / 变量名 / 字段名;说明取紧贴在前面的 JSDoc 注释节点(见上面代码块树规则);参数来自 <code>formal_parameters</code>(<code>required_parameter</code> / <code>optional_parameter</code> 的 pattern、type、value,JavaScript 的 <code>assignment_pattern</code>),不带括号的单参数箭头函数取 <code>parameter</code>;返回类型取 <code>return_type</code> 里的类型;异步看有没有 <code>async</code>;类成员的装饰器是类主体里排在它前面的 <code>decorator</code> 兄弟节点。</li> <li>TypeScript 类:<code>class_declaration</code> / <code>abstract_class_declaration</code>、类表达式 <code>class</code>(<code>export default class {}</code>、变量的值、类字段的值)。名称取类名 / 变量名 / 字段名;说明同函数;访问级别同函数(类字段按类成员);抽象看节点类型是不是 <code>abstract_class_declaration</code>(只有 languageId 是 typescript / typescriptreact 时填,JavaScript 没有这个概念,不填);装饰器是 <code>export_statement</code> 和类声明自己的 <code>decorator</code> 子节点(<code>@d export class</code>、<code>export @d class</code>、<code>@d class</code> 都算),类字段的值是类表达式时还有类主体里排在它前面的 <code>decorator</code>;泛型参数是 <code>type_parameters</code> 里每个 <code>type_parameter</code>;父类、实现的接口在 <code>class_heritage</code> 里:<code>extends_clause</code> 的每个 value 一项,跟在它后面的 <code>type_arguments</code> 归它,<code>implements_clause</code> 里每个类型一项;JavaScript 的 <code>class_heritage</code> 直接是 <code>extends</code> 后面的表达式。</li> <li>TypeScript 导入:<code>import_statement</code> 的 <code>source</code>(<code>import x = require()</code> 取 <code>import_require_clause</code> 里的);<code>import_clause</code> 里的标识符是默认导入,<code>namespace_import</code> 是 <code>* as</code>,<code>named_imports</code> 里每个 <code>import_specifier</code> 的 name / alias;语句本身带 <code>type</code> 是仅类型(同抽象,只有 TypeScript 填)。</li> <li>TypeScript 变量:每个 <code>variable_declarator</code> 的 name / type / value;value 是 <code>object</code> 时 entries 是它的具名子节点(注释除外):<code>pair</code> 的 key / value(value 又是 <code>object</code> 时连它的 entries)、<code>shorthand_property_identifier</code>(键和值都是它)、<code>spread_element</code> 里的表达式、<code>method_definition</code> 的 name 和头部(到 body 之前)。</li> <li>Python 函数:<code>function_definition</code> 的 name / parameters / return_type,<code>async</code> 子节点;说明是主体开头 docstring 的字符串节点(<code>string</code> / <code>concatenated_string</code>);装饰器是 <code>decorated_definition</code> 里的 <code>decorator</code>;参数 <code>identifier</code>、<code>typed_parameter</code>(名字是第一个具名子节点)、<code>default_parameter</code>、<code>typed_default_parameter</code>、<code>*args</code> / <code>**kwargs</code>,<code>*</code> 和 <code>/</code> 分隔符不算参数。</li> <li>Python 类:<code>class_definition</code> 的 name;说明是主体开头 docstring 的字符串节点;装饰器同函数;泛型参数是 Python 3.12 <code>type_parameters</code> 里的每一项;父类是 <code>superclasses</code>(<code>argument_list</code>)里的每个具名子节点,照写的顺序,关键字参数(<code>metaclass=ABCMeta</code>)、<code>*bases</code>、<code>**kw</code> 也算;没有抽象、实现的接口。Python 没有枚举语法:enum 库的类由库规则认成枚举块(见「内置:enum(Python)」)。</li> <li>异常(TypeScript、Python 一样):名称、说明、访问级别、装饰器、父类取自原来的类(同上两条),TypeScript 的抽象、泛型参数、实现的接口不带;类属性、构造参数见「异常」。</li> <li>TypeScript 枚举:<code>enum_declaration</code> 的 name、body;说明、访问级别同类(<code>export</code> 了公共、顶层没 <code>export</code> 私有、嵌套的没有);没有装饰器、父类。成员是 body 里的 <code>property_identifier</code>(没写值)和 <code>enum_assignment</code>(name、value),照源码顺序;没写值的接着上一个数往上加(第一个 0),上一个的值是数字字面量(可带正负号,<code>0x10</code> 这类也算)或算出来的数才算得出,否则没有 <code>computed</code>。说明:从上一个成员之后起单独成行的注释(<code>//</code> 和 JSDoc),和写在它这一行行尾的注释;同一行上一个成员后面的 JSDoc 讲的是下一个成员;最后一个成员后面的注释不算。</li> <li>Python 导入:<code>import_statement</code> 每个 name 一个来源、导入项是整个模块;<code>import_from_statement</code> 的 module_name 和各 name(<code>aliased_import</code> 拆出 alias),<code>wildcard_import</code> 是全部;<code>future_import_statement</code> 的来源是 <code>__future__</code>。</li> <li>Shell:解释器取 <code>#!</code> 后面的整句,第一个词是 <code>env</code> 的是按 PATH 找、程序名是它后面那个词,否则是写死的路径、程序名是路径最后一节。选项:每个开关一项,单字母换成 bash 的长名字(e → errexit、u → nounset、x → xtrace……),<code>-o 名字</code> 照写,<code>-</code> 开、<code>+</code> 关。变量:<code>variable_assignment</code> 的 name / value,声明里没给值的只有 name。函数:name 是函数名,doc 是紧贴在上面的注释;<code>positional</code> 是主体里(不进嵌套的函数)用到的 <code>$N</code> / <code>${N}</code> / <code>${N:-…}</code>,照位置排,<code>local x="$N"</code>、<code>x=$N</code> 给它起名字;<code>prints</code> 是没重定向到别处的 <code>echo</code> / <code>printf</code>(进 if、循环、分情况,不进 <code>$(…)</code>、管道、嵌套的函数);<code>captured</code> 看同一文件里有没有 <code>$(函数名 …)</code>;<code>returns</code> 是主体里的 <code>return</code> 命令。</li> <li>Dockerfile:解析指令是每行注释 <code># 名字=值</code> 的名字(小写)和值;阶段取 <code>image_spec</code> 的 name / tag / digest 字段、<code>as</code> 字段(image_alias),stage 看镜像名是不是前面某个阶段的名字(不分大小写),选项是 <code>param</code>、<code>mount_param</code> 节点的原文 <code>--名字=值</code>(值在语法树里没有单独的节点,按原文拆,记下值从哪开始);变量是 <code>env_pair</code> 的 name / value、<code>arg_instruction</code> 的 name / default;健康检查的 none 看有没有 NONE,检查命令是里面的 <code>cmd_instruction</code>;数组写法取 <code>json_string_array</code>,shell 写法取 <code>shell_command</code> 里各个 <code>shell_fragment</code>(<code>line_continuation</code> 不算),只有一个 <code><<EOT</code> 的取它下面 <code>heredoc_block</code> 从第一行到最后一行。</li> <li>Python 变量:<code>assignment</code> 的 left / type / right,说明是紧跟在后面的属性 docstring 的字符串节点(见上面代码块树规则);right 是 <code>dictionary</code> 时 entries 是它的具名子节点(注释除外):<code>pair</code> 的 key / value(value 又是 <code>dictionary</code> 时连它的 entries)、<code>dictionary_splat</code> 里的表达式。</li> <li>Rust:函数(含 <code>extern</code> 块里没有函数体的 <code>function_signature_item</code>)的 name / parameters / return_type,<code>async</code> 只在写了时填;<code>self</code> 的类型节点是整个接收者(<code>&mut self</code>)。赋给 <code>let</code> 的闭包:name 是 <code>let</code> 的模式,parameters 来自 <code>closure_parameters</code>(不写类型的参数就是一个模式,没有类型),return_type 写了才有,<code>async</code> 直接写在闭包前面,没有 access、decorators。特质里、<code>impl Trait for Type</code> 里的项没有可见性(不填 access,也没有 access 属性)。函数体里 <code>let</code> 的 VariableFacts 只有一项 declarator(模式、类型、值),没有 access。结构体、枚举、特质、实现、模块的名称、说明、可见性见「Rust」(模块用 ClassFacts:name、doc、access)。<code>const</code>、<code>static</code> 的名称、类型、值是 <code>const_item</code> / <code>static_item</code> 的 name / type / value(VariableFacts 的一项 declarator),可见性同别的项:<code>pub</code> 写在 <code>facts.access</code>,其余写在块的 <code>access</code> 属性上。字段和变体携带的数据由中文直译读语法树,不另做 facts。编译属性(AttributeFacts):<code>inner_attribute_item</code> 里的 <code>attribute</code> 是名字(路径照写)后面跟 <code>= 值</code>(字符串取 <code>string_content</code>)或 <code>token_tree</code>;<code>token_tree</code> 按顶层逗号切成各段(每段是几个词),<code>cfg_attr</code> 的第一段读成 CfgPredicate(<code>all</code> / <code>any</code> / <code>not</code> 带括号的、只有一个名字或 <code>名字 = 值</code> 的配置项,别的照原文),后面各段同样读成名字、值、括号里各项的原文;placement 看 <code>#![…]</code> 的上一层(<code>source_file</code>)或上两层(<code>mod_item</code> / <code>impl_item</code> 的 <code>declaration_list</code>)。</li> </ul> <h2>中文模板直译</h2> <p dir="auto">结构照抄源码(按节点模板重排的语法结构除外,见下文),只替换三类东西,其余(标识符、属性名、字面量、标点)原样保留并按词性着色:</p> <ol dir="auto"> <li>关键字:只认语法树里的关键字节点(匿名节点),所以属性名 <code>i.number</code>、对象键不会被误翻。</li> <li>内置类型:只在类型位置翻(TS 的 <code>predefined_type</code>、Python 注解里的内置类型名)。</li> <li>运算符:<code>===</code> 全等、<code>!==</code> 不全等、<code>==</code> 等于、<code>!=</code> 不等于、<code>&&</code> 并且、<code>||</code> 或者(Python 的 <code>and</code> / <code>or</code> 本身是关键字)。取反换语序读成「x 不成立」:Python 的 <code>not x</code> 走节点模板,TypeScript 的 <code>!x</code> 走节点读法(见下文)。</li> </ol> <p dir="auto">同一套表和节点模板也用来翻属性表里的值:引擎能翻任意一个节点(只取它落在块头部里的那一段),参数类型、默认值、返回类型、变量类型和初始值、字典 / 对象各项的键和值都走这条路。</p> <p dir="auto">其他规则:空数组字面量 → 空数组、空对象字面量 → 空对象(Python 空列表 → 空列表、空字典 → 空字典);复合语句头部去掉结尾的 <code>{</code> 或 <code>:</code>;多行合成一行(连续空白压成一个空格,右括号前的尾逗号去掉);注释(文件开头的说明、挂在块上的注释)给去掉注释符号的 Markdown 原文(见「注释的 Markdown 排版」)。关键字与前后内容之间保留源码里的空格。</p> <p dir="auto">关键字按所在语法节点选择译法:同一个关键字在不同语法结构里意思不同时,表里写一个默认译法,再加若干「语境 → 译法」。语境是从关键字节点往上的节点类型链,外层在前:<code>raise_statement</code> 指父节点是 raise_statement,<code>for_statement > else_clause</code> 指父节点是 else_clause、祖父节点是 for_statement(关键字包在一层子句节点里时要看到祖父节点)。几条语境同时匹配时取最长的,都不匹配用默认译法。节点类型以 tree-sitter 语法为准。下面两张表里写成「(语境)译法 / (语境)译法」的关键字就是按这条规则区分的,排第一个的是默认译法。</p> <p dir="auto">节点模板:照抄源码结构读不通的语法结构,按语法节点类型给一个模板,把译文重排成中文语序。模板是一串项,每项要么是插入的译词,要么是子节点槽位:槽位按 tree-sitter 字段名引用子节点;语法没给字段名的,按具名子节点序号(0 起,注释不占序号)引用。命中模板的节点只输出模板里的项,节点自己的关键字和标点(<code>if</code> / <code>else</code>、<code>?</code> / <code>:</code>)不再输出;插入的译词一律按关键字(keyword)着色,模板各项之间空一格;槽位里的子节点照常递归翻译,里面的关键字、运算符、嵌套的同类结构照样处理。两种情况不重排、照常翻:模板引用的子节点不存在;节点没整个落在要翻的这一段里。目前的模板:</p> <table> <thead> <tr> <th>语言</th> <th>节点类型</th> <th>模板</th> </tr> </thead> <tbody> <tr> <td>TypeScript</td> <td><code>ternary_expression</code>(<code>c ? a : b</code>)</td> <td>如果 〈condition〉 则 〈consequence〉 否则 〈alternative〉</td> </tr> <tr> <td>Python</td> <td><code>conditional_expression</code>(<code>a if c else b</code>,没有字段名,具名子节点依次是 a、c、b)</td> <td>如果 〈1 号〉 则 〈0 号〉 否则 〈2 号〉</td> </tr> <tr> <td>Python</td> <td><code>not_operator</code>(<code>not x</code>;<code>not in</code>、<code>is not</code> 是单独的关键字,不走这里)</td> <td>〈argument〉 不成立</td> </tr> </tbody> </table> <p dir="auto">写在表达式里的遍历(推导式)不走节点模板:它的节数不定、要换成一个框,由语言插件的 <code>iteration</code>(翻译时从 <code>SourceDocument.language</code> 拿到)认出来,引擎按「推导式」一节的规则排进框里;和节点模板一样,没整个落在要翻的这一段里就不重排、照常翻。跨几行写的调用也不走节点模板:由语言插件的 <code>call</code> 认出来,引擎只在翻一行译文(<code>translate</code>)、变量 / 常量的值(<code>declaredValue</code>)和实参表里的值时按「调用与构造」一节排(每段一个 <code>TableSlot</code>,见 <code>expandedText</code>、<code>emitCall</code>、<code>argumentTable</code>),属性表别的值、库规则借的 <code>expression</code> 照常一行。</p> <p dir="auto">节点模板也排不了的写法(shell 的 <code>${x:-d}</code>、<code>[ -f x ]</code>、<code>case</code>、<code>for f in glob</code>,TypeScript 的 <code>unary_expression</code> 只有 <code>!x</code> 读成「〈argument〉 不成立」、<code>-x</code>、<code>typeof x</code> 照常翻)由翻译插件的节点读法排(<code>ZhTable.readers</code>:节点类型 → 读法,读法拿到 <code>ReaderContext</code>:照常翻子节点的 <code>say</code>、名字照抄带下标的 <code>name</code>、读一条命令的 <code>command</code>,能给一张排在这一段下面的表,分情况的「情况 | 做什么」就是它;返回 undefined 照常翻)。一条命令由语言插件的 <code>command</code> 认出、按命令名交给 <code>SourceDocument.commandRule</code>(见「命令规则」),引擎接上重定向,规则给了选项表就排在这一段下面;没有规则时命令名照抄(按函数着色)、各个词照常翻。解释器开关的叫法在 <code>ZhTable.switches</code>(errexit → 出错就停),解析指令的叫法在 <code>ZhTable.directives</code>(syntax → 语法),并成一块的几条(<code>TextBlock.group</code>)按第一条的节点类型查 <code>ZhTable.groups</code> 读成一行字加一张表。阶段、健康检查、启动、分构建参数 / 环境变量的变量这些属性表块的行由引擎按 facts 排;要执行的命令(<code>CommandForm</code>)数组写法照常翻(交给命令规则),shell 写法问 <code>SourceDocument.embedded</code>,几步用「然后」(一步失败就停的)或分号连起来。</p> <p dir="auto">TypeScript 关键字表(流程 = keyword,声明 = storage):</p> <table> <thead> <tr> <th>流程</th> <th>声明 / 字面量</th> </tr> </thead> <tbody> <tr> <td>import 导入、from 来自、as(导入 / 导出别名)作为 / (类型断言 <code>x as T</code>、<code>as const</code>)断言为、export 公共、default(<code>export default</code>)默认 / (<code>switch</code> 里的 <code>default:</code>)默认情况、if 如果、else 否则、for 遍历、of 取自、in(二元运算 <code>'k' in obj</code>)在 / (<code>for…in</code> 头部)取自、while 当、do 执行、switch 分支、case 情况、break 跳出、continue 继续、return 返回、throw 抛出、try 尝试、catch 捕获、finally 最终、await 等待、yield 产出</td> <td>const 常量、let 变量、var 变量、function 函数、async 异步、class 类、extends(类 / 接口继承)继承 / (泛型约束 <code>T extends X</code>、<code>infer U extends X</code>)限定为 / (条件类型 <code>A extends B ? … : …</code>)符合、implements 实现、interface 接口、type 类型、enum 枚举、new 新建、instanceof 属于、typeof 类型为、keyof 键名、readonly 只读、public 公共、private 私有、protected 受保护、static 静态、abstract 抽象、delete 删除、true 真、false 假、null 空、undefined 未定义</td> </tr> </tbody> </table> <p dir="auto">TypeScript 内置类型:string 字符串、number 数值、boolean 布尔、any 任意、unknown 未知、never 永不、void 无、object 对象、bigint 大整数、symbol 符号。<code>Promise</code> 等库类型不翻。</p> <p dir="auto">Python 关键字:import 导入、from(导入)从 / (<code>raise … from</code>)源自 / (<code>yield from</code>)自、as(导入别名、<code>with … as</code>、<code>except … as</code>)作为、def 函数、async 异步、await 等待、class 类、return 返回、if 如果、elif 否则如果、else(<code>if</code> 后)否则 / (<code>for</code> / <code>while</code> 后,循环没被 break 才执行)未中断时 / (<code>try</code> 后,没出错才执行)未出错时、for 遍历、in(比较里)在 / (for 头部)取自(推导式里的 for / in 不逐词翻,整个重排,见「推导式」)、while 当、try 尝试、except 捕获、finally 最终、raise 抛出、with 使用、yield 产出、lambda 匿名函数、pass 跳过、break 跳出、continue 继续、global 全局、nonlocal 非局部、del 删除、assert 断言、and 并且、or 或者、is 是、not in 不在、is not 不是、None 空、True 真、False 假、match 匹配、case 情况。</p> <p dir="auto">Python 内置类型(仅注解里):str 字符串、int 整数、float 浮点数、bool 布尔、list 列表、dict 字典、set 集合、tuple 元组、bytes 字节串。</p> <p dir="auto">一行译文样例(必须逐字一致):</p> <table> <thead> <tr> <th>源码</th> <th>译文</th> </tr> </thead> <tbody> <tr> <td><code>if (!folder)</code></td> <td><code>如果 (folder 不成立)</code></td> </tr> <tr> <td><code>return []</code></td> <td><code>返回 空数组</code></td> </tr> <tr> <td><code>for (const f of files) {</code></td> <td><code>遍历 (常量 f 取自 files)</code></td> </tr> <tr> <td><code>failed: { path: string, reason: string }[]</code></td> <td><code>failed: { path: 字符串, reason: 字符串 }[]</code></td> </tr> <tr> <td><code>return {⏎ locked: true,⏎ … }</code>(issueLock.ts 20–24 行)</td> <td><code>返回 对象 3 项</code>,下面各项 <code>locked | 真</code>、<code>prerequisiteNumber | prereq.number</code>、<code>prerequisiteColumn | prereq.column</code></td> </tr> <tr> <td><code>err instanceof Error ? err.message : String(err)</code></td> <td><code>如果 err 属于 Error 则 err.message 否则 String(err)</code></td> </tr> </tbody> </table> <p dir="auto">属性表样例(必须逐字一致;子行各列用 <code>|</code> 隔开):</p> <table> <thead> <tr> <th>源码</th> <th>属性表</th> </tr> </thead> <tbody> <tr> <td><code>export async function findEnvFiles(workspaceRoot: string): Promise<string[]> {</code>(envLock.ts 18–23 行,连同前面的 JSDoc)</td> <td>名称 findEnvFiles / 说明 <code>Scan the workspace folder rooted at …</code>(三行原文照留,按 Markdown 排成一段)/ 访问级别 公共 / 异步 是 / 输入参数 1 个:<code>workspaceRoot | 字符串 |</code> / 输出参数 <code>Promise<字符串[]></code></td> </tr> <tr> <td><code>async function chmodAll(workspaceRoot: string, mode: number): Promise<EnvLockResult> {</code></td> <td>名称 chmodAll / 访问级别 私有 / 异步 是 / 输入参数 2 个:<code>workspaceRoot | 字符串</code>、<code>mode | 数值</code> / 输出参数 <code>Promise<EnvLockResult></code></td> </tr> <tr> <td><code>import { promises as fsp } from 'node:fs'</code></td> <td>来源 <code>'node:fs'</code> / 仅类型 否 / 导入项 1 个:<code>promises 作为 fsp</code></td> </tr> <tr> <td><code>const ENV_PATTERN = '**/.env*'</code></td> <td>名称 ENV_PATTERN / 访问级别 私有 / 值 <code>'**/.env*'</code></td> </tr> <tr> <td><code>const issue = issues.find(i => i.number === issueNumber)</code></td> <td>名称 issue / 初始值 <code>issues.find(i => i.number 全等 issueNumber)</code></td> </tr> <tr> <td><code>def find_env_files(root: str) -> list[str]:</code>(sample.py,连同下一行的 docstring)</td> <td>名称 find_env_files / 说明 <code>Return every .env file under root.</code> / 访问级别 公共 / 异步 否 / 输入参数 1 个:<code>root | 字符串</code> / 输出参数 <code>列表[字符串]</code></td> </tr> <tr> <td><code>from .schemas import (⏎ UserAvatarUploadResponse, … )</code>(routes.py 19–23 行)</td> <td>来源 <code>.schemas</code> / 导入项 3 个:UserAvatarUploadResponse、UserDetailResponse、UserUpdateRequest</td> </tr> <tr> <td><code>payload = await file.read()</code>(routes.py,函数体里)</td> <td>名称 payload / 初始值 <code>等待 file.read()</code></td> </tr> <tr> <td><code>_default_port = 5680 if _role == "beat" else 5679</code></td> <td>名称 _default_port / 访问级别 私有 / 初始值 <code>如果 _role 等于 "beat" 则 5680 否则 5679</code></td> </tr> <tr> <td><code>_CONTEST_STATUS_KEYS = {⏎ "published": "contests:publish", "ended": "contests:end", "draft": "contests:update" }</code></td> <td>名称 _CONTEST_STATUS_KEYS / 访问级别 私有 / 初始值 字典 3 项:<code>"published" | "contests:publish"</code>、<code>"ended" | "contests:end"</code>、<code>"draft" | "contests:update"</code></td> </tr> <tr> <td><code>CONFIG = {"db": {"host": host, "opts": {}}, **defaults, 1: await load()}</code></td> <td>名称 CONFIG / 访问级别 公共 / 值 字典 3 项:<code>"db" | 字典 2 项</code>(下一层 <code>"host" | host</code>、<code>"opts" | 空字典</code>)、<code>展开 | defaults</code>、<code>1 | 等待 load()</code></td> </tr> <tr> <td><code>const result: EnvLockResult = { total: files.length, ok: [], failed: [] }</code>(envLock.ts 40 行)</td> <td>名称 result / 类型 EnvLockResult / 初始值 对象 3 项:<code>total | files.length</code>、<code>ok | 空数组</code>、<code>failed | 空数组</code></td> </tr> <tr> <td><code>export const handlers = { total: files.length, 'x-y': await load(), [key]: !ok, files, ...rest, async load(id: string): Promise<void> { … }, nested: { a: [], b: { c: 1 } } }</code></td> <td>名称 handlers / 访问级别 公共 / 值 对象 7 项:<code>total | files.length</code>、<code>'x-y' | 等待 load()</code>、<code>[key] | ok 不成立</code>、<code>files | files</code>、<code>展开 | rest</code>、<code>load | 异步 load(id: 字符串): Promise<无></code>、<code>nested | 对象 2 项</code>(下一层 <code>a | 空数组</code>、<code>b | 对象 1 项</code>,再下一层 <code>c | 1</code>)</td> </tr> <tr> <td><code>export abstract class Repository<K, T extends Entity = Entity> extends Base<string> implements Loader<T>, Disposable {</code>(classes.ts 3–9 行,连同前面的 JSDoc 和 <code>@Injectable()</code>)</td> <td>名称 Repository / 说明 <code>仓库基类。…</code> / 访问级别 公共 / 抽象 是 / 装饰器 <code>@Injectable()</code> / 类型参数 <code>K, T 限定为 Entity = Entity</code> / 父类 <code>Base<字符串></code> / 实现的接口 <code>Loader<T>, Disposable</code></td> </tr> <tr> <td><code>export default class {</code></td> <td>名称 匿名 / 访问级别 公共 / 抽象 否</td> </tr> <tr> <td><code>class Meta(Base, metaclass=ABCMeta):</code>(classes.py,带 <code>@dataclass(frozen=True)</code>)</td> <td>名称 Meta / 访问级别 公共 / 装饰器 <code>@dataclass(frozen=True)</code> / 父类 <code>Base, metaclass=ABCMeta</code></td> </tr> <tr> <td><code>class NotFound(AppResponse):</code>(classes.py,连同 docstring)</td> <td>名称 NotFound / 说明 <code>找不到资源。</code> / 访问级别 公共 / 父类 <code>AppResponse</code></td> </tr> </tbody> </table> <h2>预览数据与消息</h2> <p dir="auto">扩展端把代码块树转成可序列化的视图数据发给 webview:</p> <div data-attr-class="89b22fd727645604:code-block-container code-overflow-scroll"><pre data-attr-class="89b22fd727645604:code-block"><code data-attr-class="89b22fd727645604:chroma language-ts" data-code-language="ts"><span class="kr">interface</span> <span class="nx">BlockViewBase</span> <span class="p">{</span> <span class="nx">id</span>: <span class="kt">string</span> <span class="c1">// 在树里的位置,例如 '6.1.0' </span> <span class="nx">anchor</span>: <span class="kt">string</span> <span class="c1">// 块的位置记号(见「问 AI」):问答挂在它上面,webview 只比相等 </span> <span class="nx">kind</span>: <span class="kt">string</span> <span class="c1">// BlockKind(库规则认成的现成块也是,如 enum),或库规则卡片的种类 id(sqlalchemy.table) </span> <span class="nx">startLine</span>: <span class="kt">number</span> <span class="c1">// 编辑范围的起止行,0 起:块的完整范围,头部连同各子孙块、结尾(见 blockSpans);库规则认领的块是原块整条语句 </span> <span class="nx">endLine</span>: <span class="kt">number</span> <span class="nx">headerEndLine</span>: <span class="kt">number</span> <span class="c1">// 块自己那部分(头部)的最后一行,库规则认领的块是整条语句;问 AI 时贴外层块的开头用,webview 不用 </span> <span class="nx">code</span>: <span class="kt">string</span> <span class="c1">// 编辑范围的源码,去掉共同缩进 </span> <span class="nx">codeTokens</span>: <span class="kt">Segment</span><span class="p">[]</span> <span class="c1">// 源码着色(拼起来等于 code) </span> <span class="nx">defines?</span>: <span class="kt">SourceRange</span> <span class="c1">// 块自己的名字(函数名、类名、只声明一个变量时的变量名)在源码里的范围,扩展端从 facts 取(库规则的块取 RuleBlock.defines);跳到的定义是它时闪整块 </span> <span class="nx">attributes</span>: <span class="kt">Record</span><span class="p"><</span><span class="nt">string</span><span class="p">,</span> <span class="na">string</span><span class="p">></span> <span class="c1">// 筛选属性(含 kind),见「筛选」 </span> <span class="nx">concepts</span>: <span class="kt">BlockConcept</span><span class="p">[]</span> <span class="c1">// 认出来的概念,从左到右;webview 按图标名画,不自己用种类查 </span> <span class="nx">badge?</span>: <span class="kt">KindBadge</span> <span class="c1">// 库规则的卡片才有:筛选栏里这个种类的名字,卡片边框的颜色;图标在 concepts 里。和类块一样默认折叠 </span> <span class="nx">slot</span><span class="o">?:</span> <span class="p">{</span> <span class="nx">row</span>: <span class="kt">number</span><span class="p">,</span> <span class="nx">item</span>: <span class="kt">number</span> <span class="p">}</span> <span class="c1">// 库规则挂在父卡片子行上的子块才有(RuleSlot):挂在父卡片第 row 行的第 item 个子行上,点它才在这一行下面展开(它的块 id 记成折叠着就是收着) </span> <span class="nx">commentTarget?</span>: <span class="kt">CommentTarget</span> <span class="c1">// 「重写注释」写到哪(见「重写注释」);嵌着的代码里的块、语言没给出新写的办法又还没有注释的块没有,不给按钮 </span> <span class="nx">children</span>: <span class="kt">BlockView</span><span class="p">[]</span> <span class="p">}</span> <span class="cm">/** 「重写注释」写到哪,行号 0 起:换掉说明或块前面的普通注释(文件开头的说明卡片是它自己),或都没有时按 shell 新写在 line 这一行上面。 */</span> <span class="kr">type</span> <span class="nx">CommentTarget</span> <span class="o">=</span> <span class="p">{</span> <span class="nx">kind</span><span class="o">:</span> <span class="s1">'replace'</span><span class="p">,</span> <span class="nx">startLine</span>: <span class="kt">number</span><span class="p">,</span> <span class="nx">endLine</span>: <span class="kt">number</span> <span class="p">}</span> <span class="o">|</span> <span class="p">{</span> <span class="nx">kind</span><span class="o">:</span> <span class="s1">'insert'</span><span class="p">,</span> <span class="nx">line</span>: <span class="kt">number</span><span class="p">,</span> <span class="nx">shell</span>: <span class="kt">string</span><span class="p">,</span> <span class="nx">breakAt?</span>: <span class="kt">number</span> <span class="p">}</span> <span class="cm">/** </span><span class="cm"> * 属性表块(有 facts)和库规则认领的块只带 properties(「注释」一行由宿主插在里面),其余块只带 translation(文件开头的说明是 `{ markdown }`);没有翻译插件时是空数组。 </span><span class="cm"> * 一行译文里有跨几行写的字面量时 items 是拆开的各项(TextTranslation.items),有跨几行写的调用时 table 是摊开实参的表(TextTranslation.table),块带说明(TextBlock.doc)时 doc 是翻译插件读出的说明正文(Translator.doc 的 body), </span><span class="cm"> * 挂着注释时 rows 是「注释」一行,webview 依次排在译文下面。差异的标记里一行译文的卡片的 rows 同属性表逐行标。 </span><span class="cm"> */</span> <span class="kr">type</span> <span class="nx">BlockView</span> <span class="o">=</span> <span class="nx">BlockViewBase</span> <span class="o">&</span> <span class="p">({</span> <span class="nx">translation</span>: <span class="kt">PreviewText</span><span class="p">,</span> <span class="nx">items?</span>: <span class="kt">PropertyItem</span><span class="p">[],</span> <span class="nx">table?</span>: <span class="kt">PreviewTable</span><span class="p">,</span> <span class="nx">doc?</span>: <span class="kt">Markdown</span><span class="p">,</span> <span class="nx">rows?</span>: <span class="kt">PropertyRow</span><span class="p">[]</span> <span class="p">}</span> <span class="o">|</span> <span class="p">{</span> <span class="nx">properties</span>: <span class="kt">PropertyRow</span><span class="p">[]</span> <span class="p">})</span> </code></pre></div><ul dir="auto"> <li>扩展 → webview: <ul> <li><code>{ type: 'document', version, languageId, blocks: BlockView[], attributes: FileAttribute[], translators: { id, label }[], translatorId, error?: string, reveal?: Reveal, names: ShownName[] }</code>,文档每次变化(防抖 150ms)重发。<code>names</code> 是这个版本眼下已知的中文名(<code>{ offset, length, name, definition? }</code>:源码下标 [offset, offset + length) 的名字显示成 name,definition 是定义处本身),见「中文名」。<code>attributes</code> 是这份文件里出现过的筛选属性,按筛选栏的先后排:<code>{ id, name, values: { id, name, count }[] }</code>,名字取翻译插件的 <code>attributeNames</code>,没有的用 id。<code>reveal = { line, definition? }</code> 只随新预览的第一份文档走:从文本编辑器切过来时是它顶部那一行(滚到包含这行的块、顶到顶部,折叠的不展开);跳到定义时还带 <code>definition</code>(定义处名字在源码里的下标),按「跳到定义」一节展开、滚动、闪。</li> <li><code>{ type: 'reveal', line, definition? }</code>:同上,发给已经开着的预览。页面正在重新加载(标签页切回来)时收不到,扩展端改成随它 <code>ready</code> 后的第一份文档带过去。</li> <li><code>{ type: 'definitionProbed', id, found }</code>:回答第 <code>id</code> 次 <code>probeDefinition</code>。</li> <li><code>{ type: 'definitionPeeked', id, peek? }</code>:回答第 <code>id</code> 次 <code>peekDefinition</code> / <code>peekLink</code>。<code>peek</code> 是 <code>{ path, uri, line, block: BlockView, names: ShownName[], qa?: QaEntryView[] }</code>:定义所在的文件(从工作区文件夹算起)和这份文件本身、定义处那一行(0 起)、那块卡片连同子块、卡片上显示成中文名的名字(下标是那个文件的)、那块自己下面问过 AI 的各次(按时间先后,没问过时不带);没有卡片时不带。见「悬停看定义卡片」。</li> <li><code>{ type: 'hoverDelay', delay, hidingDelay }</code>:VS Code 的 <code>editor.hover.delay</code> / <code>editor.hover.hidingDelay</code>(毫秒,按这份文件读),<code>ready</code> 后发一次,设置改了再发。</li> <li><code>{ type: 'stickyScroll', enabled, maxLineCount }</code>:VS Code 的 <code>editor.stickyScroll.enabled</code> / <code>editor.stickyScroll.maxLineCount</code>(按这份文件读,语言自己的设置优先),<code>ready</code> 后发一次,设置改了再发;见「钉住卡片」。</li> <li><code>{ type: 'occurrencesFound', id, occurrences: { start, end, kind: 'text' | 'read' | 'write' }[] }</code>:回答第 <code>id</code> 次 <code>findOccurrences</code>,<code>[start, end)</code> 是源码下标,<code>kind</code> 同 <code>DocumentHighlightKind</code>;语言扩展没答就是空数组。</li> <li><code>{ type: 'moveOccurrence', step: 1 | -1 }</code>:两条命令发来,跳到下一处 / 上一处出现。</li> <li><code>{ type: 'displayNames', version, names: ShownName[] }</code>:version 这份文档里显示成中文名的名字,查到一步发一步;页面只收自己眼下这个版本的。</li> <li><code>{ type: 'filter', excluded: Record<属性 id, 值 id[]>, onlyChanges }</code>:整个工作区共用的块筛选和「只看改动」开关(见「代码差异」)。webview 发 <code>ready</code> 后先于第一份文档发一次(第一份文档按筛选画),之后任何一个预览改了都发给所有开着的预览(包括改的那个)。</li> <li><code>{ type: 'diff', diff: DocumentDiff | null }</code>:这份文件相比对比基准改了什么(见「代码差异」),每发一份文档之后算好了补发,基准挪了、换了从哪儿打开的再发;<code>null</code> 是不比了。<code>DocumentDiff</code> 是 <code>{ base, version, newFile?, blocks: Record<块记号, { change: 'add' | 'mod', outermost?, own?, marks? }>, removed: { parent, after?, block, line, kindName }[], stats: { add, mod, del }, lineCount }</code>:块按记号认;<code>marks</code> 是卡片里要标的(属性表各行、子行、一行译文、摊开实参的表、说明的增删,行和子行按下标,只在页面上正是 <code>version</code> 这一版时画);删掉的块 <code>block</code> 是旧版本的视图数据(只带它自己的内容、词不带下标),<code>parent</code> / <code>after</code> 是它挂在哪块下面、排在哪块后面(记号),<code>line</code> 是它在新文件里的位置。</li> <li><code>{ type: 'qa', available, threads: { anchor, entries: { id, question, answer, createdAt, profile?, images? }[] }[], running: { askId, anchor, question, text, activity? }[] }</code>:这份文件的问答(见「问 AI」)。<code>ready</code> 后发一次,存的问答、正在答的问题变了再发给开着这份文件的所有预览;<code>available</code> 为假时文件不在工作区文件夹里,不给「问 AI」;<code>images</code> 是问题带的图片在预览里能直接用的地址(<code>webview.asWebviewUri</code>);<code>running</code> 让重新加载的页面接上还在答的问题(不带图片,接上的这一问不显示缩略图,答完回到历史里照常有)。</li> <li><code>{ type: 'askProfiles', options: { id, label }[], selected }</code>:「问 AI」的 profile 下拉(<code>id</code> 是 profile 名,「默认」的 <code>id</code> 是 <code>''</code>、<code>label</code> 是「默认」)。<code>ready</code> 后发一次(每次现读 profile 目录),任何一个预览选了都发给所有开着的预览(包括选的那个)。</li> <li><code>{ type: 'answer', askId, text, activity? }</code>:<code>askId</code> 那个问题的回答长了(<code>text</code> 是到现在为止的全文)或换了在做的事。webview 攒着、最多每 80ms 画一次。</li> <li><code>{ type: 'answerEnd', askId, status: 'done' | 'failed' | 'stopped', text?, error?, entryId? }</code>:答完了。<code>done</code> 带最终回答和存下的那条的 id(历史里不再重复显示它),<code>failed</code> 带给人看的原因,<code>stopped</code> 是被叫停、没存。</li> <li><code>{ type: 'rewriteProgress', anchor, question, text, activity? }</code>:<code>anchor</code> 那块正在重写注释:<code>question</code> 是发给模型的那一句,<code>text</code> 是正文到现在为止写出来的,<code>activity</code> 是正在读什么。<code>{ type: 'rewriteEnd', anchor, status: 'done' | 'failed' | 'stopped', error? }</code>:重写完了(见「重写注释」)。</li> <li><code>{ type: 'repoLinksChecked', id, missing, unresolved, builtin }</code>:回答第 <code>id</code> 次 <code>checkRepoLinks</code>,<code>missing</code> 是仓库里没有的路径,<code>unresolved</code> 是找不到的不带路径的引用,<code>builtin</code> 是语言自带、又问不到定义的不带路径的引用(当普通文字)。<code>{ type: 'repoLinkBroken' | 'repoLinkOpened', path?, symbol? }</code>:点了以后打不开 / 打开了;没有 <code>path</code> 的是不带路径的引用。<code>{ type: 'repoLinkBuiltin', symbol, block? }</code>:点的不带路径的引用是语言自带的名字,没打开,改当普通文字。见「注释的 Markdown 排版」。</li> <li><code>{ type: 'openedFrom', from: 'uncommitted' | 'other' }</code>:这个标签页是从哪儿打开的(见「代码差异」),页面记进 webview 状态。<code>ready</code> 后发一次,开着时又从侧栏点开、换了从哪儿打开的再发。</li> </ul> </li> <li>webview → 扩展: <ul> <li><code>{ type: 'probeDefinition', id, version, offset }</code>:按住修饰键指到带 <code>offset</code> 的名字上时问有没有定义,<code>id</code> 每问一次加一;<code>version</code> 不是当前文档版本就不答。</li> <li><code>{ type: 'goToDefinition', version, offset }</code>:修饰键 + 点击,按「跳到定义」一节跳。这两条都只带下标:扩展端按发出去的文档认出规则标了引用的下标交给规则查(见「规则自己查定义」),其余问语言扩展。</li> <li><code>{ type: 'peekDefinition', id, version, offset }</code>:鼠标停在带 <code>offset</code> 的名字上,要它的定义卡片;<code>id</code> 每问一次加一,<code>version</code> 不是当前文档版本就不答。<code>{ type: 'peekLink', id, path?, symbol }</code>:鼠标停在注释里指到符号的链接上,要那个符号的定义卡片(<code>path</code>、<code>symbol</code> 同 <code>openRepoLink</code>)。见「悬停看定义卡片」。</li> <li><code>{ type: 'findOccurrences', id, version, offset }</code>:单击带 <code>offset</code> 的名字,问它在这份文档里的各处出现;<code>id</code> 每问一次加一,<code>version</code> 不是当前文档版本就不答。</li> <li><code>{ type: 'occurrenceNavigation', available }</code>:眼下能不能用键盘在各处出现之间跳,页面加载后报一次、之后变了才报;扩展端按它设 <code>superpowers.codeReader.hasOccurrences</code>。</li> <li><code>{ type: 'checkRepoLinks', id, paths, symbols }</code>:页面上的仓库路径问哪些没有,不带路径的引用问哪些找不到(<code>id</code> 每问一次加一)。<code>{ type: 'openRepoLink', path?, symbol? }</code>:点了注释里的仓库链接(带 <code>path</code>)或不带路径的引用(只带 <code>symbol</code>)。见「注释的 Markdown 排版」。</li> <li><code>{ type: 'ready', openedFrom? }</code>:页面准备好了;<code>openedFrom</code> 是 webview 状态里记着的这个标签页是从哪儿打开的,新开的页面没记过就不带。</li> <li><code>{ type: 'edit', startLine, endLine, text }</code>:用 <code>WorkspaceEdit</code> 把这几行整行替换成 <code>text</code>(每行补回原来第一行的缩进)。</li> <li><code>{ type: 'rewriteComment', anchor }</code>:给 <code>anchor</code> 那块重写注释(见「重写注释」);<code>{ type: 'stopRewrite', anchor }</code>:叫停,不写文件。</li> <li><code>{ type: 'selectTranslator', id }</code>:按 <code>languageId</code> 记到 <code>globalState</code>,重发文档。</li> <li><code>{ type: 'setFilter', excluded }</code>:改块筛选(整份替换);扩展端逐项核对后记进 <code>workspaceState</code>,再发 <code>filter</code> 给所有预览。webview 不在本地先改,只按扩展端发来的显示。</li> <li><code>{ type: 'setOnlyChanges', on }</code>:打开 / 关掉「只看改动」;扩展端记进 <code>workspaceState</code>(<code>superpowers.codeReader.onlyChanges</code>),再发 <code>filter</code> 给所有预览。</li> <li><code>{ type: 'ask', askId, anchor, question, images: { mediaType, base64 }[] }</code>:问 <code>anchor</code> 那块;<code>askId</code> 由 webview 起;<code>images</code> 是贴进输入框的图片(没贴是空的,<code>base64</code> 不带 <code>data:</code> 前缀)。扩展端按最近一次发出去的块树和源码找这块、拼问题,这块存过问答就接着最近那条的会话追问(见「问 AI」的追问),用扩展端记着的 profile;找不到(代码刚改过)、图片格式不认或问不了(这块正在答、同时答的太多)时直接回 <code>answerEnd</code> failed。</li> <li><code>{ type: 'stopAsk', askId }</code>:叫停,结束 claude 进程,这次不存。</li> <li><code>{ type: 'deleteQa', anchor, entryId }</code>:删掉一次问答。</li> <li><code>{ type: 'selectAskProfile', id }</code>:「问 AI」换 profile;扩展端记进 <code>globalState</code> 后发 <code>askProfiles</code> 给所有预览。webview 不在本地先改,只按扩展端发来的显示。</li> </ul> </li> <li>块的编辑状态和用户的折叠选择在 webview 里按块 id 记(<code>vscode.setState</code>):<code>{ editing: string[], folds: Record<id, boolean>, scroll? }</code>。<code>folds</code> 只记用户点过的(id → 折叠着没有),属性行和带下一层的子行也记在里面:属性行 id 是 <code><块 id>/p<行序号></code>(例如 <code>6.1.0/p3</code>),子行 id 是上一层(行或子行)的 id 加 <code>/<子行序号></code>,一层层往后接(例如 <code>6.1.0/p3/1</code> 是第 3 行的第 1 个子行,<code>6.1.0/p3/1/0</code> 是它下面的第 0 个);块 id 只有数字和点,只有行那一节带 <code>p</code>、其余各节是纯数字,所以块、行、各层子行的 id 互不相撞。子行 id 也是跳到定义时在页面上找要闪的那一项用的。没记的按默认:函数、类、枚举、异常块和库规则的卡片折叠,其余块、属性行和子行展开。眼下折叠着哪些由「默认 + folds」算出来,界面、跳到定义的展开都只看这一份。跳到定义展开祖先块、属性行时也记进 <code>folds</code>(记成展开)。读回来的状态逐项核对,旧版本存的、形状不对的项当没存过。文档刷新后保留;正在编辑且有焦点的块,刷新时不覆盖输入框内容。</li> </ul> <h2>颜色</h2> <p dir="auto">词性和内置种类的颜色跟着当前颜色主题走,和编辑器里看到的代码颜色一致(<code>theme/</code>):</p> <ul dir="auto"> <li>当前主题照 VS Code 自己的规则定:开着 <code>window.autoDetectHighContrast</code> 且当前是高对比主题时取两个高对比的设置,开着 <code>window.autoDetectColorScheme</code> 时按当前主题的明暗取 <code>workbench.preferredDarkColorTheme</code> / <code>preferredLightColorTheme</code>,否则是 <code>workbench.colorTheme</code>;老名字照 VS Code 换成现在的。在 <code>vscode.extensions.all</code>(内置主题也在里面)各扩展贡献的 <code>themes</code> 里按 id(没有用 label,再没有用文件名)找到主题文件。</li> <li>读主题文件:JSON 带注释、尾逗号;<code>include</code> 的在前、自己的在后;<code>tokenColors</code> 可以是数组,也可以是指向 <code>.tmTheme</code> 的路径;主题本身也可以是 <code>.tmTheme</code>(plist,<code>plist</code> 包解析)。</li> <li>叠用户设置:<code>editor.tokenColorCustomizations</code>(几组颜色、<code>textMateRules</code>、<code>"[主题名]"</code> 专属块)排在主题后面;<code>editor.semanticHighlighting.enabled</code>、主题的 <code>semanticHighlighting</code>、<code>semanticTokenColors</code> 和 <code>editor.semanticTokenColorCustomizations</code>(<code>enabled</code>、<code>rules</code>、专属块)定开没开语义上色、语义规则。先后照 VS Code 的 <code>colorThemeData.ts</code>。</li> <li>每种颜色查什么在 <code>theme/codeColors.ts</code> 的对照表:开了语义上色时先看语义规则(函数 <code>function</code>、类型 <code>class</code>、变量 <code>variable</code>、常量 <code>variable.readonly</code>、枚举 <code>enum</code>、构建阶段 / 模块 <code>namespace</code>),没有再依次查几个 TextMate 范围。范围照 vscode-textmate 匹配:规则的范围按点分段是查的范围的前缀才算,段数多的胜,一样具体时后写的胜,逗号分隔的选择器拆开,带父级的选择器不算。都没配的用编辑器前景色(<code>--vscode-editor-foreground</code>)。语义规则不分语言(所有预览共用一份),带语言的规则不算。</li> <li>宿主共用一份(<code>ThemeColors</code>),换主题(<code>onDidChangeActiveColorTheme</code>)、改上面这些设置时作废重读,发 <code>codeColors</code> 给所有 ready 的预览;页面 ready 时先于第一份文档发。页面设成 body 上的 <code>--cr-<词性></code>、<code>--cr-kind-<种类></code>(<code>codeColors.ts</code>)。主题找不到、读不出来时记一条警告,全用编辑器前景色。</li> </ul> <table> <thead> <tr> <th>词性</th> <th>查什么</th> </tr> </thead> <tbody> <tr> <td>keyword</td> <td><code>keyword.control</code></td> </tr> <tr> <td>storage / operator</td> <td><code>storage.type</code></td> </tr> <tr> <td>variable</td> <td>语义 <code>variable</code>;<code>variable.other.readwrite</code>、<code>entity.name.variable</code></td> </tr> <tr> <td>function</td> <td>语义 <code>function</code>;<code>entity.name.function</code>、<code>support.function</code></td> </tr> <tr> <td>type</td> <td>语义 <code>class</code>;<code>entity.name.type.class</code>、<code>support.class</code></td> </tr> <tr> <td>string</td> <td><code>string</code></td> </tr> <tr> <td>number / literal</td> <td><code>constant.numeric</code></td> </tr> <tr> <td>comment</td> <td><code>comment</code></td> </tr> <tr> <td>hint</td> <td><code>--vscode-descriptionForeground</code></td> </tr> <tr> <td>constant</td> <td>语义 <code>variable.readonly</code>;<code>variable.other.constant</code>、<code>variable.other.readwrite</code>、<code>entity.name.variable</code></td> </tr> <tr> <td>plain</td> <td><code>--vscode-editor-foreground</code></td> </tr> </tbody> </table> <p dir="auto">属性表里:说明按 comment,访问级别(公共 / 私有 / 受保护)和「作为」按 keyword,是 / 否按 storage,函数名称按 function,类名称按 type,未标注、无、N 个、字典 N 项、对象 N 项、展开、默认导出、全部、整个模块、匿名按 hint;输入参数表格的列名是界面文字(<code>descriptionForeground</code>),不是着色的词。推导式框里的「遍历」「的每个」「仅当」「取」「异步遍历」按 keyword,框头按「循环」块的种类色。父类名(TypeScript <code>extends</code> 后面、JavaScript <code>class_heritage</code> 里、Python 类名后面括号里直接写的名字)源码着色和属性表里都按 type。</p> <p dir="auto">带自己颜色的段(<code>Segment.color</code>,库规则按库的配色给,例如 FastAPI 的 HTTP 方法、成功的状态码):暗色、亮色两个写在元素的 style 上(<code>--color-dark</code> / <code>--color-light</code>),<code>.cr-colored</code> 按主题挑一个,盖过词性的颜色;词性照旧报(<code>role</code>)。</p> <p dir="auto">种类(种类图标的颜色 + 块底 7% + 边框 35%;图标见「预览(代码块树)」):同一类语义共用一个 <code>--cr-kind-*</code>,查主题的什么见下;主题给几类配了同一个色就同色,靠图标区分。import 导入、copy 复制查 <code>keyword.control.import</code>;function 函数、impl 实现同函数名;class 类、type 类型同类型名;trait 特质同 storage;attribute 同 constant;enum 枚举查语义 <code>enum</code>、<code>entity.name.type.enum</code>;exception 异常查 <code>support.type.exception</code>;field 字段、const 常量、variable 变量、label 标签同变量名;if 条件、else 否则、case 分情况查 <code>keyword.control.conditional</code>;loop 循环查 <code>keyword.control.loop</code>;try 尝试、catch 捕获、finally 最终、health 健康检查查 <code>keyword.control.trycatch</code>;return 返回、start 启动查 <code>keyword.control.flow</code>;statement 语句、interpreter 解释器、directive 解析指令查 <code>source</code>(一般就是编辑器前景色);comment 注释查 <code>comment</code>(虚线框);options 选项、setting 设置查 <code>constant.other.option</code>;command 命令、pipeline 管道、run 执行查 <code>entity.name.command</code>,没配落回 <code>entity.name.function.call</code>;stage 阶段、module 模块查语义 <code>namespace</code>、<code>entity.name.namespace</code>。查不到具体的会落回更泛的(<code>keyword.control.loop</code> 落回 <code>keyword.control</code>)。库规则的种类用规则给的颜色(<code>badge.color</code> 的 dark / light,webview 写在块的 style 上按主题挑),内置 SQLAlchemy 数据表 #e8a860 / #a3590d,Celery 应用 #98c379 / #4d7f17,定时任务 #56b6c2 / #127f8c,数据类 #ef7fb0 / #b3306c,FastAPI 接口 #61afef / #1a6fb5,FastAPI 路由器 #7aa2f7 / #34548a。编辑状态:底色 <code>--vscode-editor-background</code>,边框 <code>--vscode-focusBorder</code>。</p> <h2>代码位置</h2> <div data-attr-class="89b22fd727645604:code-block-container code-overflow-scroll"><pre data-attr-class="89b22fd727645604:code-block"><code data-attr-class="89b22fd727645604:chroma language-text" data-code-language="text">vscode/src/codeReader/ contract.ts 通用契约 registry.ts 注册表 + CodeReaderApi runtime.ts web-tree-sitter 初始化、语法包加载、解析 blockView.ts CodeBlock → BlockView(源码着色、去缩进、调用翻译);嵌着的代码:解析那几段、分步、换那种语言的翻译插件转子块、着色叠一层(EmbeddedPart) attributes.ts 块的筛选属性(从 facts 推出通用属性 + 语言自己报的)、文件里出现过的属性和块数 libraryRules.ts 库规则的宿主部分:按导入启用、按注册顺序认领、出错退回、查规则标的引用 workspaceIndex.ts 工作区索引(库规则的 WorkspaceIndex,和宿主自己的中文名索引)的宿主部分:建、缓存、跟着文件改动更新,查到的项带着认的时候给的东西(不碰 VS Code) nameResolution.ts 中文名落到预览里哪些名字上:本文件问各处出现、别的文件先查中文名索引再问定义,按文档版本查一次、没答上再查、跟着改动挪位置(不碰 VS Code) workspaceFiles.ts 工作区索引在 VS Code 里怎么找文件、读文件、盯改动(排除规则) workspaceScope.ts 工作区索引还要跳过的文件:嵌套的 checkout、忽略文件忽略的(不碰 VS Code) rules/sqlalchemy.ts 内置 SQLAlchemy 库规则(只 import contract 和同目录的共用代码) rules/celery.ts 内置 Celery 库规则:两张卡片、频率换算、任务名索引 rules/enum.ts 内置 enum 库规则(Python):枚举类认成枚举块,成员照 Python 的规则认 rules/dataclass.ts 内置 dataclasses 库规则(Python):数据类卡片,字段照 dataclasses 的规则认 rules/fastapi.ts 内置 FastAPI 库规则(Python):路由器卡片(接口表)、接口卡片(请求表、响应表),参数照 FastAPI 的规则放,异常类的状态码、类型别名、枚举经索引查 rules/react.ts 内置 React 库规则(TSX / JSX):一块一张卡片——组件卡片(名称标签、说明、构造参数),组件体里的状态 / 引用 / 数据源 / 派生值 / 副作用 / 事件处理函数 / 渲染输出卡片,略去 "use client" 和组件的 JSDoc,导入的属性类型经索引查 rules/reactTree.ts React 规则读语法树的办法:认组件、构造参数、一块语句属于哪个组件体(enclosingComponent)、组件体的归类、名字用在哪、渲染的 JSX 大纲、别的文件里的属性类型(纯函数) commandRules.ts 命令规则的宿主部分:命令名 → 规则(先注册的算),规则出错记下、当它没读 commands/reading.ts 命令规则共用的:认选项(连写拆开、带值的、长选项、--)、拼译文(动作、括号里的意思、照抄不认识的选项) commands/systemCommands.ts 内置命令规则:新建用户 / 用户组(useradd、adduser、addgroup / groupadd)、gpg、install、chown,管 '*' 的只问版本的命令 commands/packageCommands.ts 内置命令规则:apt-get / apt、apk、pip / pip3、npm、pnpm、yarn、corepack、uv(sync、pip、run……)、uvicorn commands/shellCommands.ts 内置 shell 命令规则(rm、mkdir、curl、tar、unzip、find、cp、mv、echo / printf、exit、cd、pwd、basename、dirname、readlink、shasum、cut、du、cat、grep、sed、chmod、ln、touch、source、read、sleep、continue / break……) localSymbols.ts 语言插件认的同一文件里的名字怎么用:盖住位置的名字、跳到哪处定义、各处出现(不碰 VS Code) ruleLookups.ts 库规则认领时查别的文件里的定义(RuleContext.lookup):索引 + 语言扩展查定义(不碰 VS Code) calleeKinds.ts 调用的名字定义成哪种块(SourceDocument.calleeKind):按被调用的表达式原文记、问最后那个名字(不碰 VS Code) deferredAnswers.ts 上面两样共用的:排视图数据时先交出已知的、记下问了什么,发出预览后按版本查一次、没答的隔一会儿再查,变了叫宿主重发(不碰 VS Code) (blockView.ts 的 readDefinitionKind:定义处落在哪块自己的名字上;editor/definitions.ts 的 definitionKind:打开定义所在的文件交给它) rules/pythonTree.ts Python 库规则共用的读语法树的办法(名字最后一节、字符串内容、调用的参数、别名、注解里的 X[...]、库里的名字) rules/segments.ts 库规则共用的拼属性表文字的办法 rules/children.ts 库规则共用的挑子块的办法(类主体里带注解的赋值、成员的说明节点:挂在成员上的注释、行尾注释、属性 docstring) docTags.ts 说明的原文按 KDoc 拆成正文和块标签(Doc:@param / @return / @throws),翻译插件的 doc 用它(纯函数) rules/docRows.ts 讲说明的几行,中文直译和库规则共用:正文、对不上的参数、参数按名字对说明(paramDocs)、输出后面的 @return、异常表 symbolLinks.ts 注释里不带路径的引用指到哪:导入引进来的名字(importedNames,readBlockViews 交出)、代码里用到的本文件没定义的名字(outsideUses,取自名字绑定)、本文件定义的优先再是导入的、再是代码里用到的语言自带的名字,语言自带又问不到定义的当普通文字(resolveReference,符号和定义由预览去问)(不碰 VS Code) languages/shared.ts 语言插件共用的建树工具:一串语句分块(listBlocks)、注释挂到它讲的块上(attachComments);异常类(withExceptions:按整份文件认异常类,类块换成异常块,并类属性、取构造参数) commentRewrite.ts 重写注释:发给模型的那一句、读结构化结果(含逐字的进度)、新正文套回定界符 / 新写一段、核对后写回(不碰 VS Code) languages/typescript.ts (call:调用的结构,new 是新建;TYPESCRIPT_EXCEPTIONS:认异常类的父类名、类字段、constructor) languages/python.ts (iteration:推导式的结构;call:调用的结构;PYTHON_EXCEPTIONS:自带的异常类、类属性、__init__) languages/rust.ts Rust:代码块树 rules/rustTree.ts Rust 库规则读语法树的办法(可见性、属性、字段、变体) rules/rustRows.ts Rust 属性表的行(中文直译和库规则共用) rules/rustImpl.ts impl、trait 的方法挂在方法表的子行上,点方法名展开 rules/serde.ts serde:可序列化的结构体 rules/thiserror.ts thiserror:错误枚举 rules/axum.ts axum:接口 rules/sqlx.ts sqlx:查询 rules/tauri.ts tauri:桌面命令 languages/displayNames.ts 中文名的共用部分:第一小句、行尾记号、按优先级汇总 languages/pythonNames.ts Python 的中文名写法(Annotated、@Comment、行尾记号、docstring) languages/bash.ts bash / sh / zsh(shellscript):代码块树、facts(解释器、选项、按位置的参数、输出、返回)、command(命令的结构、重定向)、symbols(同一文件里的函数、变量) languages/bashNames.ts Shell 的中文名写法(行尾 # @Comment) languages/dockerfile.ts Dockerfile:代码块树(阶段、并块)、facts(解析指令、阶段、健康检查、启动、分作用范围的变量)、embedded(RUN 这些 shell 在哪几段)、command(数组写法的命令)、symbols(阶段名、变量)、中文名 languages/typescriptNames.ts TypeScript / JavaScript 的中文名写法(@Comment 装饰器、行尾记号、JSDoc) translators/zhTemplate.ts 模板直译引擎(按语言的表驱动,词性、遍历结构从 SourceDocument.language 取;推导式排进框见 iterationGroup) translators/typescriptZh.ts TS 表 translators/pythonZh.ts Python 表(含注解位置的判断) translators/bashZh.ts Shell 表:节点读法(展开、特殊变量、条件、通配、&& / ||、管道、遍历、分情况)、开关的叫法 translators/dockerfileZh.ts Dockerfile 表:各条指令的读法(RUN 问嵌着的 shell 读出来的样子)、并块的读法(COPY、LABEL)、解析指令的叫法 explorer/fileTree.ts 侧栏文件树(FileTreeDecorator:给行添信息的接口;showChanges:审查工单时只摆 PR 改过的文件;nodeOf / getParent:按 Uri 找树上那一行、找上一层) explorer/copyText.ts 复制菜单的命令和复制出什么(纯函数) explorer/copyCommands.ts 复制命令:写剪贴板、状态栏提示 explorer/revealCommands.ts 在系统文件管理器中显示:按系统叫法的三个命令都交给 VS Code 的 revealFileInOS review/changedFiles.ts 改动状态 → M / A / D、颜色,PR 改过的文件 → 按目录分好的树(纯函数) review/reviewRules.ts 能审查的列(「审查」「进行中」)、弹框列哪些工单、PR 号、能不能审查、标题栏的字(纯函数) review/pickList.ts 选工单弹框的各项:先摆出来、文件数读到一个换一个、还转不转圈、每项的字(纯函数) review/reviewSource.ts 看板数据:弹框列的工单(看板上摆着的)、重读单张工单、PR、PR 改过的文件 (panel/kanbanSnapshot.ts 的 boardIssues:看板上现在摆着的工单——这次加载的,否则存下的快照;KanbanWebviewPanel.issuesOnBoard 接上面板和 workspaceState) review/reviewWorktree.ts 本机这张工单的工作副本在哪、备好 PR 最新的代码(已有的快进、没有的建) review/changeDecorations.ts FileDecorationProvider:审查时给改过的文件上色、挂徽标 review/reviewMode.ts ReviewMode:选工单弹框、进出审查、刷新、按工作区记住,审查时预览对比什么(diffBase) uncommitted/uncommittedView.ts UncommittedView:侧栏「未提交的更改」一栏,盯 git 状态重摆,点开的文件带 `from: uncommitted`;纯函数在 uncommittedFiles.ts uncommitted/headBases.ts HeadBases:从「未提交的更改」点开的预览对比上一次提交(diffBase),git 状态变了通知预览重新比 explorer/treeLocator.ts TreeLocator:侧栏的树定位到某个文件 / 目录(跟着当前文件走、目录链接),按 FileTreeProvider 的 nodeOf、getParent 找行 explorer/treeReveal.ts 定位用到的判断:树上一行和要找的路径什么关系、explorer.autoReveal / autoRevealExclude 怎么认(纯函数) diff/diffBase.ts DiffBase:对比基准(文案、旧内容怎么取、缓存用的 key、基准里没有这个文件时的说明);OpenedFrom、previewDiffBases:预览对比什么(审查按位置,上一次提交只给从「未提交的更改」点开的) diff/blockDiff.ts 两棵块树比:兄弟块对齐(名字 / 最长公共子序列 / 相似度)、新增修改删除、删掉的块放哪、统计(纯函数) diff/viewDiff.ts 一块卡片里的内容比:属性表按属性名、子行按第一列、着色的词逐截、Markdown 逐词;删掉的块只留自己的内容、去掉源码下标(纯函数) diff/previewDiff.ts PreviewDiff:一个预览的差异,发完文档再比、旧内容按 key 记着、只发最后一次的结果;DocumentDiff 消息的形状(不碰 VS Code) (git/fileAt.ts 的 fileAtRef:某个提交上某个文件的内容,没有这个文件是 null) (gitea/repoContext.ts 的 resolveRepoContext:当前工作区的 Gitea 仓库和 token,看板「改动」tab 共用;git/worktree.ts 的 fetchPullRequestHead / addDetachedWorktree / fastForwardWorktree:拉 PR 的提交、建不挂分支的 worktree、不丢东西地快进) pathMap.ts 按文件 / 目录记东西的表(文件注释、问答共用):路径的键、改名 / 挪位置 / 删除时怎么改、.spx 里 JSON 的读写、条目带着的文件哪些不再有人带、删文件(不碰 VS Code) folderStore.ts FolderStore:每个工作区文件夹一份表,内存里的一份、文件监听、跟着改名 / 删除走、读写排队、改表后删掉不再有条目带着的文件 notes/noteMap.ts 保存一条注释(纯函数) notes/store.ts .spx/code-notes.json 叫什么、每条怎么认 notes/fileNotes.ts FileNotes:注释的 FolderStore、一行的注释 notes/commands.ts 「添加注释 / 编辑注释」输入框 explorer/rowDecoration.ts 一行名字后面的灰字(注释)、悬停提示、contextValue,入口文件的绿色播放图标(纯函数) explorer/rowDecorator.ts RowDecorator:把注释和入口交给 rowDecoration 画行(FileTreeDecorator) entries/entryPoints.ts 项目写明的入口:四种清单 / Go 源码 → 入口文件和名字(纯函数,读文件经 FolderReader) entries/entryMarks.ts 预览里名字对上的块,概念列表第一个换成入口(绿色播放图标)(纯函数) entries/workspaceEntries.ts WorkspaceEntries:各工作区文件夹扫一遍、文件变了重扫、报出入口变了的文件 qa/anchor.ts 块的位置记号(BlockAnchors)(不碰 VS Code) qa/qaMap.ts .spx/code-qa.json 的形状、每条怎么认、记一次 / 删一次、再问时接着哪个会话(纯函数) qa/prompt.ts 问 AI 的话:找到那块、外层块的开头、带行号的代码、追问 qa/answerStream.ts stream-json 的每行 → 会话 id、逐字的回答、在做的事、最终回答 / 失败 qa/claudeAsk.ts 命令行参数(只读、选的 profile、要结构化结果时的 --json-schema)、问一次(问题连同图片从 stdin 发,追问找不到会话时重新问)、重写注释问一次(askCommentBody)、失败给人看的话 qa/qaImages.ts 问题带的图片:认哪些格式、存到 .spx/code-qa/ 叫什么名字、写文件(不碰 VS Code) qa/askProfiles.ts AskProfiles:问 AI 用哪个 profile,下拉的选项、记在 globalState、通知所有预览(不碰 VS Code) qa/asks.ts QaAsks:块下面正在进行的对话,一块一个、能叫停;问 AI 最多同时 3 个,重写注释排成一队、一次一段(RunPolicy,不碰 VS Code) qa/codeQa.ts CodeQa:问答的 FolderStore、开问(问过的块接着最近的会话,用选的 profile,带贴的图)、答完存(带 profile 名、图片)、删,发给预览的问答、图片目录 (cc/spawnClaude.ts 的 runClaudeLines:跑 claude、给了 input 就写进 stdin、stdout 逐行交出、超时、叫停,问 AI 用它;buildStreamJsonLine 拼 stream-json 的一条用户消息) editor/previewEditor.ts CustomTextEditorProvider editor/messages.ts 消息类型 editor/blockFilter.ts 块筛选和「只看改动」开关:workspaceState 读写、通知所有预览 editor/definitions.ts 查定义(vscode.executeDefinitionProvider)、规则查到的位置换成 VS Code 的、几处定义的挑选框 editor/definitionSources.ts 名字的定义去哪儿查:文档里规则标的引用问规则,其余问语言扩展 editor/definitionAction.ts 查到的定义 → 怎么跳(纯函数) editor/definitionPeek.ts 定义卡片取哪块(peekBlock)、卡片上带哪些中文名(namesOnCard、definitionSiteNames) editor/openTarget.ts 打开别的文件用哪种编辑器:语言有代码阅读的用预览(滚到定义处),其余用默认的打开方式(选中名字);侧栏点文件、跳到定义、注释里的链接共用 editor/occurrences.ts 查名字的各处出现(vscode.executeDocumentHighlights) editor/occurrenceNavigation.ts 跳到下一处 / 上一处高亮:命令、上下文键 theme/currentTheme.ts 当前正在用的颜色主题:照 VS Code 的规则定设置里的名字,在装着的扩展里找主题文件 theme/themeFile.ts 读颜色主题文件:JSON(带注释、include 链、tokenColors 指向 .tmTheme)或 .tmTheme(不碰 VS Code) theme/tokenStyles.ts 主题叠上用户设置后一个 TextMate 范围、一种语义记号是什么颜色,照 VS Code 和 vscode-textmate 的规则(不碰 VS Code) theme/codeColors.ts 词性、种类 → 查什么的对照表;ThemeColors:所有预览共用的一份代码配色,换主题、改设置时重读、通知预览 index.ts registerCodeReader(context): CodeReaderApi vscode/webview-ui/code-reader.html + src/codeReader/ 预览界面(React,Vite 第二个入口;块右上角的图标按钮在 BlockTools.tsx,悬停提示在 src/components/ui/tooltip.tsx(和看板共用,页面根上挂一次),块的概念图标(图标名 → 图标组件只在 kindIcons.tsx;内置种类 → 图标名在 contract.ts 的 KIND_ICONS,宿主写进概念列表)在 kindIcons.tsx,属性表在 PropertyGrid.tsx,子行排成表格还是列表在 subRows.ts,着色的词和框在 SegmentText.tsx,中文名换字在 displayNames.ts,Markdown 渲染在 markdown.ts / TextPreview.tsx(不带路径的引用的行内规则也在 markdown.ts),仓库链接和引用的点击、问坏没坏在 repoLink.ts / useRepoLinks.ts,悬停看定义卡片什么时候弹、收、放在哪在 definitionPeek.ts、接到页面上在 useDefinitionPeek.ts、浮窗和只读的卡片在 DefinitionPeekPopup.tsx,筛选栏在 FilterBar.tsx,筛选规则在 filter.ts,扩展端发来的代码配色设成颜色变量在 codeColors.ts,编辑时盖住哪些块、点「编辑」退出哪些块在 editing.ts,高亮各处出现的规则在 occurrences.ts、接到页面上在 useOccurrences.ts,问 AI 的问答区在 AskThread.tsx、状态和规则在 qa.ts、贴图拖图的规则在 qaImages.ts、收发在 useQa.ts;代码差异:页面上怎么标(块 id、删掉的块挂哪、只看改动折哪几段、各处改动、上一处 / 下一处跳到哪)在 diffView.ts,筛选栏第二行和新文件横幅在 DiffBar.tsx,卡片里的增删在 DiffText.tsx(Markdown 的增删在 markdown.ts 的 renderMarkdownDiff),总览条在 OverviewRuler.tsx,跳到一处改动在 useChangeNav.ts;钉住卡片:钉哪几张、顶上去多少在 stickyScroll.ts,量页面、收设置在 useStickyScroll.ts,钉住的几行在 StickyLines.tsx) </code></pre></div><h2>测试</h2> <ul dir="auto"> <li>语言插件:<code>envLock.ts</code>、<code>issueLock.ts</code> 的代码块树(种类、header 行号、层级)与原型 <code>CODE_TREE</code> 一致,只是紧贴函数的 JSDoc 算进了函数头部;Python 样例覆盖 import / def / docstring / if-elif-else / for / try-except / with / 推导式;函数的说明:JSDoc 各种挂法(export、默认导出、方法隔着装饰器、函数值的变量、嵌套函数)和不挂的情况,docstring 连同它前面的注释进头部。类:<code>classes.ts</code>、<code>classes.py</code> 覆盖 export + 抽象 + 泛型 + extends / implements + 装饰器 + JSDoc、匿名默认导出、赋给变量 / 类字段的类表达式、函数里的类、类里的类、docstring、metaclass 关键字参数、下划线私有。</li> <li>编辑范围:各样例里每块的编辑范围盖住它的子孙、兄弟块之间最多共用一行、首尾不是空行,「编辑」写回原样不变;父块的编辑框是整块代码(TSX 组件连 JSDoc、函数体、收尾括号),子块只有它自己连同子孙;展平的 else / elif / catch 各到下一块开头之前,Shell 的 <code>fi</code> 归最后一块;函数、类说明所在的行在函数、类块的头部里。</li> <li>折叠:默认函数、类、枚举、异常块和库规则的卡片折叠、其余展开;用户的选择盖过默认、重新加载后还在(嵌套子行的也是);各层子行的 id 互不相撞;旧版本存的状态不报错;跳到定义时默认折叠的函数、类也会展开。</li> <li>属性表信息:<code>envLock.ts</code>、<code>sample.py</code>、<code>routes.py</code>(原型里的 FastAPI 路由)加内联片段,覆盖各种导入写法、形参写法、类成员访问修饰符、解构和多变量声明、Python 下划线规则;字典 / 对象字面量的各项:Python 字符串键、名字键、嵌套字典、<code>**</code> 展开、空字典、类属性,TypeScript 名字键、字符串键、计算属性名、简写、展开、方法和 getter(头部到主体之前)、嵌套对象、空对象,不是字面量的值没有 entries。</li> <li>字典 / 对象的属性表:项数、键 | 值两列、嵌套一层层展开、「展开」、方法头部、空的一行;键和值里的名字、字符串带下标(嵌套的也是);每项标的定义是键,展开项不标;跳到定义落在对象属性名上只闪那一项、连外层各层一起展开,只是显示着的名字闪整块、展开到它那一层。</li> <li>推导式:Python 语言插件读出的结构(调用唯一参数的生成器带 sharedParentheses、几个参数之一的不带,列表带条件,字典分出键和值,集合的解构变量,几节 for / if 照源码顺序,async for,套着的各自认,字面量和 for 语句不算);中文直译「推导式」一节的样例表逐字一致,框的样式、框头、关键字着色,顿号连写、条件位置、另起一句、异步遍历、集合、框里的框、多个参数旁边的生成器、跨行带注释的生成器,属性表的默认值和字典项的值里也换;框里的名字带下标且指回源码原文,高亮各处出现、跳到定义都认框里的名字(只显示在框里的推导式变量 → 框所在的块);TypeScript 不认。</li> <li>调用与构造:语言插件读出的结构(Python 按位置 / 按名字 / <code>*x</code> / <code>**x</code>、唯一的生成器实参、被调用的不是名字时没有 name、装饰器和父类列表不算;TypeScript <code>new</code> 是新建、被调用的不含 new、<code>...x</code>、带标签的模板字符串和不带括号的 <code>new X</code> 不算);中文直译按「调用与构造」一节的样例:<code>routes.py</code> 的 <code>return UserDetailResponse(…)</code> 定义是类(异常也算)时「返回 新建 UserDetailResponse · 11 个参数」、11 行表、名字带下标,没查到 / 查不到 / 是函数时「调用」、名字按函数着色;<code>update_user</code> 的初始值「等待 调用 user_service.update_user · 10 个参数」、表是这一行的子行(列名 参数 / 值)、第一行「第 1 个」按说明词着色;一行写完的 <code>get_user(...)</code>、<code>file.read()</code> 照旧一行、不问定义;「展开」「展开关键字」、「第 N 个」只数按位置传的;<code>fixtures/calls.ts.txt</code>(本仓库的代码)new 是新建、不带 new 是调用、都不问定义;链式调用跨了几行、回调主体是子块的调用不排。嵌套:<code>paymentConsoleRoutes.py</code> 的 15 行表逐字一致,<code>credentials_present</code> 那一格套 4 行表、<code>CredentialPresence</code> 同样问定义、按类型着色、带下标,<code>bool(…)\n and Path(…).is_file()</code> 照旧一行;自造的三层(Outer → Middle → Leaf、带 await 的 fetch)一层层套、每层都问定义;<code>chainedCalls.py</code> 的 <code>(调用 base64.b64encode · 1 个参数).decode()</code>、被调用的表达式里的调用照常一行(圆括号贴着内容),<code>[0]</code>、<code>or</code> 后面跟着时括起来、前面有东西或后面是逗号时不括,实参值里的 <code>X(\n…\n).model_dump()</code> 也括起来、表套在格子里;<code>nestedCalls.ts.txt</code> 三个 <code>commands.registerCommand(…)</code> 各套一张表,<code>new Server(new Options(…))</code> 套 new。<code>calleeKinds.test.ts</code>:按表达式原文记、同一版本不再查、被调用的不是名字不问、查了没有不算变、没答的隔一会儿只再查没答的。视图数据带 <code>table</code>、<code>readDefinitionKind</code> 认类 / 函数 / 异常 / 变量、<code>renderedNames</code> 算上表里(连套着的表)的名字、<code>documentMessage</code> 把 calleeKind 交给翻译插件;webview 跳到定义、键盘跳到高亮的各处出现都认一行译文下面那张表和值那一格里套着的表(套在变量初始值那一行下面的展开那一行)。</li> <li>只经契约:只照 contract 写的假语言插件(<code>iteration</code> 把一个普通调用认成遍历)配中文模板,那个调用换成遍历框;同一个翻译插件配上不给 <code>iteration</code> 的语言插件照常翻。测试文件不 import 任何语言插件模块。</li> <li>输入参数表格:函数的「输入参数」行带列名 名称 / 类型 / 默认值(TS、Python 一样),默认值格子只有译文、没有就是空格子,别的行没有列名;webview 按有没有列名选表格还是列表,表格的格子按列数取,列宽模板(<code>tableTemplate</code>)只看列数。</li> <li>翻译:上面两张样例表逐字一致;属性名不被翻;每个被翻的词单独成段;节点模板嵌套时正确、槽位里照样翻、没命中模板的节点照抄;取反读成「x 不成立」(Python 的 <code>not</code> 在 if 条件、和 <code>and</code> 连着、括号里、条件表达式里,<code>not in</code>、<code>is not</code> 照旧;TypeScript 的 <code>!a</code>、<code>!(a && b)</code>,<code>-x</code>、<code>typeof x</code>、非空断言照旧);属性表里的值和一行译文用同一套表和模板。</li> <li><code>blockView</code>:<code>codeTokens</code> 拼起来等于 <code>code</code>;去缩进正确;函数、类、导入、常量、变量块只带属性表,其余块只带译文。</li> <li>块标签:拆法(<code>docTags.test.ts</code>:多行续写、空行结束、<code>@returns</code> / <code>@exception</code> 同义、JSDoc 的 <code>{类型}</code>、<code>[名字=默认值]</code>、横杠、标签前后的正文、没有标签时原样、别的标签和围栏代码块里的行留在正文、异常名写成引用);卡片(<code>translators/zhTemplate.test.ts</code> 的「说明里的块标签」):<code>fixtures/conversationBrief.py</code>(luffy 的 <code>render_conversation_brief</code>)说明里没有 <code>@param</code> / <code>@return</code>,输入参数表「说明」列四个参数的原文,输出参数后面跟 <code>@return</code>;<code>fixtures/localRuntimeGate.py</code>(luffy 的 <code>gate.py</code>)「异常」表两行,第一行是完整链接、第二行是不带路径的引用;对不上的 <code>@param</code> 排在说明下面、提示色、悬停「没有这个参数」;类的 <code>@param</code> 照写成表、异常类的对到构造参数(<code>*args</code> 去掉星号对)。React 组件(字段没注释时用 <code>@param</code>、<code>props.x</code>、讲整个参数的跟在构造参数后面、对不上的、异常表)和事件处理函数(<code>rules/react.test.ts</code>)、FastAPI 请求表的说明列和响应后面的 <code>@return</code>(<code>rules/fastapi.test.ts</code>)、Rust 函数和结构体(<code>languages/rust.test.ts</code>)、数据类字段(<code>rules/dataclass.test.ts</code>)。</li> <li>注释挂到它讲的块上(各语言的 <code>languages/*.test.ts</code>「注释挂到它讲的块上」,<code>outline</code> 的 <code>notes</code> 是挂着的各段注释在哪):Python、TypeScript、Rust、Shell、Dockerfile 各一份,覆盖隔着空行的注释挂到后面那块、主体最后的挂到前面那块、主体里只有注释时挂到父块、文件最后的挂到最后一块、文件开头隔着空行的是文件的说明而紧挨着第一块代码的挂到它上面、<code>#!</code> / 写编码的 / <code>//!</code> / <code>@file</code> / 模块 docstring 是文件的、说明和注释并存、函数体里语句前面的注释;luffy 的 <code>fixtures/acquisitionRepository.py</code> 导入后面的注释挂到类上(<code>blockView.test.ts</code>:类卡片「名称、注释、访问级别」、编辑范围连着注释、写回原样);TypeScript 的「名称、说明、注释」、几段注释是几段 Markdown、一行译文的卡片的 <code>rows</code>;嵌着的代码里的块没有 <code>commentTarget</code>;改了注释那块算修改、「注释」一行逐词标(<code>diff/blockDiff.test.ts</code>);规则略去挂着注释的块不合约定(<code>libraryRules.test.ts</code>)。webview(<code>BlockTree.test.tsx</code>):「注释」一行照属性表画,一行译文的卡片排在译文下面,有没有「注释」一行按钮都只有一个、排在右上角那排最前面,没有注释时叫「按注释规范写注释」,不能写注释的块没有按钮。</li> <li>重写注释(<code>commentRewrite.test.ts</code>、<code>qa/claudeAsk.test.ts</code>):发给模型的就是 <code>/ecc:writing-code-comments 路径:起-止</code> 一句;读结构化结果(正文逐字长出来、读文件是在做的事、只回一段话没有正文、报错);命令行在只读那套后面加 <code>--json-schema</code>、stdin 里只有那一句;交回结构化正文 → 已有说明换说明(Python docstring 填回三引号、JSDoc 填回 <code>/** */</code>,不在声明上面再插一行)、只有前面的普通注释时换掉那几段(套回 <code>#</code> 和缩进)、文件开头的说明换它自己、说明和普通注释都有时只换说明;都没有时 Python 函数在主体里插 docstring、TypeScript 在声明上面插 <code>/** */</code>、Rust 在项上面插 <code>///</code>、只有行注释的语言仍插在块上面;没交回正文、出错、重写期间代码变了 → 失败不写,叫停 → 不写;套回壳的几种写法(三引号、<code>#</code>、<code>/** */</code>、<code>//</code>、<code>///</code>)。</li> <li>注释的 Markdown:「注释」一行、文件开头的说明、函数和类「说明」的视图数据是 <code>{ markdown }</code>,原文照抄(reST 角色、双反引号、<code>{@link}</code>、HTML 都不转换,块标签拆出去),行注释连成的块留得住缩进四格的代码块;渲染时 HTML 当文字、带协议的网址和邮箱成链接、不带协议的文件名不成链接,<code>javascript:</code> / <code>command:</code> / <code>./</code> / <code>..</code> / 绝对路径的链接和 http 图片不渲染,仓库内图片按原图比例显示,点了打开原图,没有就写「找不到图片」,仓库路径链接能点、点了打开文件并跳到符号;跳到定义不认 Markdown 里的名字。不带路径的引用:行内规则(<code>webview-ui/.../markdown.test.ts</code>:<code>[Name]</code>、<code>[A.b]</code> 是 <code>symbol:</code> 链接,<code>`[x]`</code>、<code>items[i]</code>、<code>[会话进展]</code>、<code>[名字](路径)</code>、<code>[Agent](javascript:x)</code> 各自的结果,问 AI 的回答不认、带增删的注释照样认);问答(<code>repoLink.test.ts</code>:<code>symbol:</code> 地址只认标识符、点了发不带 <code>path</code> 的 <code>openRepoLink</code>、和仓库路径一起问、找不到的标坏、打开成功不再标、语言自带的名字当普通文字且点了不发、点了才知道是语言自带的也改当普通文字、同样的链接不再问、文档换了版本再问);扩展端(<code>symbolLinks.test.ts</code>:本文件优先于导入且不问语言扩展、本文件的 <code>Class.member</code>、类有成员没有就是找不到、导入的名字问定义再在那个类底下找成员、整模块导入从顶层找、找不到;代码里用到的语言自带的名字先挑用在说明的那个声明里的那处问定义、<code>Class.member</code> 在定义所在文件里找成员、没写到的语言自带的名字是 <code>builtin</code>、语言扩展答不上的也是;Python / TypeScript 代码里用到的本文件没定义的名字不含注释、字符串、属性名里的;两门语言自带的名字;Python / TypeScript 各种导入写法引进来的名字和问定义的位置,<code>gate.py</code> 的导入)。</li> <li>行替换:多行块替换后缩进正确。</li> <li>跳到定义:TS / Python 译文和属性表里的名字、点分模块名每一节、模块字符串带下标且指回源码原文,关键字、运算符、说明词不带(前面有中文注释也对得上);查到的定义 → 不动 / 原地 / 别的文件 / 挑选框,别的文件用哪种编辑器(<code>editor/openTarget.test.ts</code>:有代码阅读的用预览、带位置时滚到那一行闪定义处,没有的用默认的打开方式选中名字,打不开成文本的用默认的打开方式);webview 里按下标找目标块、要展开的祖先块和属性行,滚动位置。悬停看定义卡片:取哪块(<code>editor/definitionPeek.test.ts</code>:Python 源码走完整的语言插件 + 翻译插件,类名 → 类那块连同子块,参数 → 所在的函数,变量 → 变量那块,块外面 → 没有,挂在子行上的子块去掉 <code>slot</code>;卡片上的中文名只带画出来的、长度对得上的,别的文件只认定义本身),什么时候弹、收(<code>definitionPeek.test.ts</code>:停够延迟才问、没到时间移开不问、没有卡片不弹、迟到的回答丢掉、链接问 <code>peekLink</code> 点标题发 <code>openRepoLink</code>、没收到延迟不弹、移开过 <code>hidingDelay</code> 才收、移进浮窗不收、扫过别的名字不往后推、浮窗里不弹别的、换成新名字的卡片、Esc / 滚动后不再弹、换文档版本收起;指到符号的链接才弹;放在下方 / 上方 / 空间大的那边、钳在视口内),只读的卡片(<code>DefinitionPeekPopup.test.tsx</code>:类收起子块、没有编辑 / 问 AI / 重写按钮、中文名用卡片带的、引用不成链接;<code>markdown.test.ts</code> 的 <code>peek</code>),卡片带的问过 AI(<code>editor/previewEditor.test.ts</code>:本文件、别的文件、工作区外的库源码都按卡片那份文件和那块的记号带上,图片换成页面地址,子块上问的不带,没问过不带 <code>qa</code>;<code>qa/codeQa.test.ts</code>:放行各工作区文件夹的图片目录;<code>DefinitionPeekPopup.test.tsx</code>:「问过 AI(N)」只列问题、点开显示 Markdown 回答和图片、再点收起、大图画在浮窗里、没问过没有这一节)。闪哪一处(TS、Python 源码走完整的语言插件 + 翻译插件 + 视图数据):参数 → 它那个子行,导入项 → 它那一项,多变量声明里的一个 → 它的名称行,类的泛型参数 → 「类型参数」那一行,函数名 / 类名 / 单个变量名(含解构模式)→ 整块,循环变量 → 所在的块,翻译插件没填 <code>defines</code> → 整块(照样展开属性行);翻译插件给参数、导入项、声明的变量、泛型参数标的范围指回源码原文;块自己的名字只有函数、类和单变量声明有。</li> <li>高亮各处出现:段的源码范围和出现重叠才亮、边界相接不算、没下标的不亮,讲别处名字的段(<code>refers</code>)按那个名字算、自己照抄的位置不算(<code>occurrences.test.ts</code> 的 <code>sourceOf</code>),点分模块名每一节各按自己的范围认(真实的 Python 译文和属性表:定义处按写入、用到处按读取);写入 → 强高亮色,读取、按文字 → 普通高亮色,同时碰上按写入;单击名字问、点空白处清、点按钮 / 链接 / 筛选栏不动、名字在按钮里照样问,拖选、双击第二下、带修饰键的点击不动,⌘ / Ctrl + 点击照旧发跳到定义、不问出现处;回答连同名字显示、空回答不亮、换名字时回答到前旧的留着、迟到的回答丢掉、答过「没有」再点照样再问、清掉后才到的回答丢掉、换文档版本或翻译清掉而同版本同翻译重发不清、没有文档时不问。键盘跳(<code>occurrences.test.ts</code>、<code>reveal.test.ts</code>、<code>editor/occurrenceNavigation.test.ts</code>):回答照源码先后排、点中的那处是当前那一处、「2/5」和「N 处」;往后 / 往前、到头接着跳,找不到的跳过、总数照旧、一处都找不到不动,点中的不在回答里从点中的地方数起;只有当前那一处描框;能不能跳(高亮、焦点、输入框)第一次必报、之后变了才报;页面上找第一处显示着它的地方、要展开的块 / 属性行 / 子行、跳过筛掉 / 编辑 / 盖住的块、Markdown 不算;<code>package.json</code> 的两条命令、默认键、<code>when</code>、命令面板,扩展和页面的代码里不写键、不拦键,上下文键有一个预览能跳就真、变了才设,命令发给能跳的预览、没有就发给活动的;按键那一刻的位置跳转时回一次、隔太久 / 点过鼠标 / 没按过键不回,跳完守住跳到的位置、过了 <code>KEY_SCROLL_MS</code> 或又按键、点鼠标不守。</li> <li>钉住卡片(<code>stickyScroll.test.ts</code>、<code>StickyLines.test.tsx</code>、<code>BlockTree.test.tsx</code>):没有子块的高卡片(折叠着的函数)名称那行滚到后面、下面还看得见就钉,滚完时被顶上去;上沿刚滚过它那一行才钉、里层按自己那一行算,外层在上里层在下,同层前一张滚完、后一张还没到只钉外层;子块快滚完时最里那行被顶上去、正好到行顶时整行顶到上一行下面,最后一张子块同时是外层的结尾时里层先走、再顶外层;比上限深时留外层的;最多行数按设置和可视区域的四分之一;滚到顶上时上面钉着多高;钉的没变不重画。一行里是概念图标和标题:属性表第一行的值(中文名照换、不带属性名、不带说明)、一行译文、Markdown 只取第一行、空属性表只有图标。画出来的子层记着它是哪张卡片的,收着时没有。</li> <li>中文名: <ul> <li>语言插件认写法(<code>languages/pythonNames.test.ts</code>、<code>languages/typescriptNames.test.ts</code>,真实解析):Python 注解 <code>Annotated</code>(<code>typing.Annotated</code>、<code>t.Comment</code>,f-string、别的调用不算)、装饰器(函数、类)、行尾记号(多行参数表、<code>*args</code>、<code>self.x</code>、变量、字段、枚举成员,记号前面有别的字,记号的范围)、单独成行 / 那一行没有定义的不算、函数那一行给参数、docstring 的谷歌写法和 Sphinx 写法(带类型、星号、续行、别的段落不算)、第一小句和 12 个字的限制、明写的记号盖过说明文字、没取的记号照样去掉;TypeScript 装饰器(类、<code>@d export class</code>、字段、构造函数和方法的参数、方法隔着别的装饰器、名字最后一节)、行尾 <code>//</code> 记号(参数、变量、枚举成员、类字段、接口属性,块注释不算)、JSDoc <code>@param</code>(类型、短横、可选、属性不算、export 函数、方法、箭头函数变量、类字段)、JavaScript 默认值参数;预筛和各方言的文件。</li> <li>记号不是说明(<code>nameResolution.test.ts</code>,走 <code>readBlockViews</code>):Python、TypeScript 枚举成员的行尾记号去掉、前面的字照留、只有记号的没有说明列。</li> <li>落到哪些名字上(假的语言扩展):画出来的名字按下标认、本文件的定义处标 definition、长度对不上 / 没画出来的不换;别的文件的只问原名在索引里的、定义处要对上、本文件的索引项不算;语言扩展没答上算没查完;先交本文件的再交全部。工作区中文名索引(内存里的工作区):按原名查出位置和中文名、没写中文名的文件照常读但不认、<code>lookup</code> 照旧只给位置、一种 languageId 一份。</li> <li>跟着版本走:每个版本查一次、手上有挪过来的就不先交一半、查到一半文档又改了丢掉、跟着改动挪位置(前面的挪、碰到的丢、后面的不动)、没答上照间隔再查到次数为止。</li> <li>页面(<code>webview-ui/.../displayNames.test.tsx</code>,渲染成 HTML):只收这个版本的、下标和长度都对上才换、用到的地方写中文名和「原名 x」悬停提示、下标照旧、定义处后面淡淡的原名、框里也换、单击时筛选栏写中文名且当前那一处按下标认。</li> <li>真 VS Code(Pylance):routes.py 的条件读成「如果 当前用户.role 不等于 UserRole.管理员」(管理员来自 models.py),参数表「当前用户 current_user」,docstring 兜底的「要删的那一项的编号」,单击高亮、⌘↓ 跳、⌘ 点击「管理员」跳进 models.py 的预览(「管理员 ADMIN」,没有说明列),改文档时中文名一帧都没少。</li> </ul> </li> <li>Shell: <ul> <li>语言插件(<code>languages/bash.test.ts</code>,真实解析):<code>fixtures/fetchRuntime.sh</code> 的代码块树(种类、行号、层级,同原型 <code>SHELL_BLOCKS</code>,原型略去的行也在);elif / else 展平、while / for 的主体是子块、条件串和命令串;解释器(env、写死的路径)、选项(连写拆开、<code>-o</code>、<code>+</code>、<code>set --</code> 不是)、变量(local 一句几个、没值的、readonly 和顶层全大写写死的是常量)、函数(按位置的参数和名字、打印的几句、被 <code>$(…)</code> 拿去用、写了的 return);命令的词(写死的文字、有展开的没有)和重定向;同一文件里的名字(函数、赋值、local、for、read、展开,第几个参数不算);行尾 <code># @Comment</code>。</li> <li>中文直译(<code>translators/bashZh.test.ts</code>,走 <code>readBlockViews</code>):<code>fetchRuntime.sh</code> 整份逐行同 <code>FETCH_READING</code>(即上面「Shell 脚本」的样例);展开(长度、去头去尾、截取、没设时、替换、特殊变量)、条件、重定向、命令规则(不认识的选项照抄、没有规则的照抄)、<code>&&</code> / <code>||</code>、遍历、<code>$(dirname "$0")</code>、常量;字符串色 / 变量色 / 常量色;规则出错照抄命令并报告。</li> <li>宿主:命令规则按语言、注册顺序给(<code>registry.test.ts</code>),shellscript 的 document 消息带命令规则读的译文、shell 的种类名、同一文件里的名字(<code>editor/documentMessage.test.ts</code>),跳到之前最近的定义、定义处算写入(<code>localSymbols.test.ts</code>)。</li> </ul> </li> <li>Rust(<code>languages/rust.test.ts</code>、<code>rules/rustLibs.test.ts</code>,真实解析):<code>fixtures/rustLang.rs</code> 的导入、结构体、枚举、实现(方法挂在方法表的子行上)、函数(异步、函数体里 match 的各支)、特质(同实现)逐行同预览;函数体子块(执行器的 <code>device_resume</code>:let、let … else、if、隐式返回;if / else if let / else、match 的守卫和各支、排在最后的 if;三种循环;闭包、嵌套 fn、unsafe / async / 带标签的块、展平的 <code>{}</code>、实参里的闭包和 async 块;函数体里的注释;特质的默认方法和 impl Trait for 的方法没有可见性),调用的结构和跨几行写的调用;给结构体加一个字段,字段表那一行标成新增。<code>fixtures/rustComments.rs</code> 开头连着写的 <code>//</code> 合成一段,是文件的说明,预览是去掉 <code>//</code> 的三段 Markdown;别的注释挂到后面那一项(「注释」一行),行尾 <code>//</code> 不单独成块,<code>//!</code> 连着几行合成一段、是文件的说明,<code>///</code> 并进下一个函数当说明、普通注释同时是它的「注释」,<code>mod</code>、<code>impl</code> 身体里同一套。编译属性:Tauri <code>main.rs</code> 的 <code>cfg_attr</code> 卡片逐行(属性的着色、「非 调试构建」、「整个 crate」、windows 子系统的说明),别的文件顶层的 <code>allow</code> 是本模块、列出 lint 名,认不得的属性没有说明、不知道文件名时是所在模块,<code>cfg_attr</code> 里几个属性是一张表、套着的条件加括号,<code>isCrateRoot</code> 的几种路径。<code>fixtures/rustLibs.rs</code> 的 serde(改名、不输出、字段名改驼峰)、thiserror(展示、透传来源)、axum(方法、路径、路径参数)、sqlx、tauri 逐行同预览;没导入 serde 时 derive 仍是结构体;改 axum 的路径,路径那一格标成改过。<code>axum::routing</code> 这种来源算导入了 axum。</li> <li>Dockerfile: <ul> <li>语言插件(<code>languages/dockerfile.test.ts</code>,真实解析):两份 luffy 的 Dockerfile 的代码块树(同原型,后端的三行注释是阶段说明);并块(紧挨着才并,空行、注释、别的指令隔开,COPY 要从同一个地方,ADD、heredoc 的 COPY 不并,第一个 FROM 之前的是顶层);ONBUILD 和各种设置;facts(解析指令、阶段的名称 / 序号 / 说明 / 标签 / 摘要 / 平台 / 接着前面的阶段、构建参数和环境变量并块、健康检查两种写法和 NONE、启动);嵌着的 shell 的几段(续行去掉、heredoc 正文);数组写法的命令(VOLUME、SHELL 的数组不算);同一文件里的名字(阶段名在 --from、--mount 的 from、FROM 里,变量);中文名;着色。</li> <li>中文直译(<code>translators/dockerfileZh.test.ts</code>,走 <code>readBlockViews</code>,RUN 交给 bash 的插件):两份 luffy 的 Dockerfile 整份逐行同 <code>NEXT_READING</code>、<code>BACKEND_READING</code>;阶段的各种写法、变量的展开和注解、RUN 的选项 / 数组写法 / heredoc / 一条循环 / 带重定向的一串、没有 shell 插件时照抄、复制和 ADD、设置、健康检查、启动;阶段名和表里变量名的源码下标、RUN 的源码按 shell 着色。</li> <li>shell 的分步(<code>languages/bash.test.ts</code>):只用 && 连着的拆开、整串带着的重定向算在最后一条上,条件串、有 || 的、分几句写的不拆;<code>a && echo x > f</code> 在 shell 脚本里也把重定向写在 echo 上(<code>translators/bashZh.test.ts</code>)。</li> <li>命令规则(<code>commands/buildCommands.test.ts</code>):装软件包、装依赖、跑程序、建用户、密钥、装文件、改属主各条的读法;选项到 4 个、写了几遍时排成表;<code>'*'</code> 的规则只认 <code>--version</code>;命令名 → 规则先注册的算、没有的交给 <code>'*'</code>、出错记下来当没读。</li> <li>宿主(<code>editor/documentMessage.test.ts</code>):注册了 shell 和 Dockerfile 时 RUN 的各步是子块、按 shell 的种类,筛选栏有 Dockerfile 的种类名,名字连同 shell 里的一起交出。</li> </ul> </li> <li>注册表:注册 / 注销后 <code>languageIds</code> 与可选翻译正确;同一语言多个翻译时按 <code>globalState</code> 选中;库规则按语言过滤、保持注册顺序,注销后不再给。</li> <li>库规则(宿主):导入了库(<code>from sqlalchemy.orm import …</code>、<code>import sqlalchemy as sa</code>、<code>import os, sqlalchemy</code>、函数里的导入)才启用,<code>sqlalchemy_utils</code>、相对导入 <code>.sqlalchemy</code> 不算;先注册的规则优先;规则抛错、替身不合约定时记下来、接着问下一个、都没认照常显示,document 消息不带 error;编辑范围是整个类、没并进来的子块(夹在中间的注释、两个字段中间的方法)照源码顺序留下,卡片「编辑」写回整个类,能编辑的范围只在块和它的子孙之间重叠;筛选的种类计数、名字取规则的标签;规则自己报的筛选属性名字取规则的 <code>attributeNames</code>(翻译插件给了的以它为准,<code>attributes.test.ts</code> 的合并、<code>documentMessage.test.ts</code> 的「请求方法」),规则起名时抛错记下来、照写 id;<code>rules/</code> 下的非测试文件只 import contract。挂在卡片子行上的子块(<code>RuleSlot</code>)记下行和子行,不是留下的子块、没有那一行 / 子行、放了两处的记下来(<code>libraryRules.test.ts</code>);webview 里它默认收着、子行名称是展开它的按钮、展开时画在那一行下面、那一行垫上种类色,收起那一行时跟着收起(<code>BlockTree.test.tsx</code>);默认折叠、跳到定义 / 上一处下一处 / 只看改动要展开的是那一行和它自己(<code>viewState</code>、<code>reveal</code>、<code>diffView</code> 的测试)。</li> <li>同一行不在两处能改(webview <code>editing.test.ts</code>):块编辑时它的子孙都不画(卡片留下的方法连同主体、类的方法),跳到被盖住的块改闪它;子块编辑时只盖住自己的子孙;点「编辑」时重叠的块(祖先、子孙、<code>} else {</code> 共用一行的兄弟块)退出编辑;任意点法下正在编辑、画出来的块两两不重叠。</li> <li>SQLAlchemy 规则:luffy <code>referral/models.py</code> 的节选(<code>fixtures/sqlalchemyModels.py</code>)里 <code>ReferralRelation</code> 的属性表逐字同上面的样例,其余模型覆盖 <code>Mapped[bool]</code>、<code>int | None</code> 可空、<code>default=None</code>、<code>server_default</code>、别名的 <code>PgUUID</code>、<code>_enum</code> 辅助函数、关系、唯一约束、删除时置空 / 级联删除;枚举类、函数照常显示;卡片的编辑范围是整个类、同一行只在块和它的子孙之间共用;两个字段中间写着的方法照常是子块、卡片「编辑」写回整个类、字段前面的注释并进卡片、讲方法的注释留下;类名、字段名的定义范围和带下标的词都指回源码原文;webview 里跳到类名闪整块、跳到字段名只闪字段表里那一行并先展开「字段」那一行,卡片默认折叠;<code>import sqlalchemy as sa</code> 的老写法 <code>Column</code>、类型全名、只写注解的字段、不认得的类型照写法、docstring 当说明、方法留作子块。</li> <li>库规则的引用:引用标的不是自己、规则没给 <code>resolve</code>、段没带下标时当替身不合约定;查引用交给标它的规则,规则不在、没给 <code>resolve</code>、出错时记下来、当作没有定义;视图数据里的引用(下标 → 引用)只从规则认领的块的属性表里取,查定义时是引用的问规则、其余问语言扩展,只对记下它的那个文档版本有效(<code>editor/definitionSources.test.ts</code>)。</li> <li>工作区索引(内存里的工作区,<code>MemoryIndexHost</code>):第一次查才建、同一个 id 只建一份;查到的是名字的行列(列按 UTF-16)、照文件和行排好;预筛不过的不解析;改了只重看那一份、新建加进来、删目录连里面的去掉;新建 / 删掉问过在不在的文件(<code>__init__.py</code>)时问过它的文件重看;建索引期间的改动建好后补上;读不了、规则出错的记下来、别的照常;dispose 后不再盯着。还要跳过的文件(<code>workspaceScope.test.ts</code>,内存里的文件夹):子目录里 <code>.git</code> 是文件的 git worktree、<code>.git</code> 是目录的子仓库整个跳过,工作区自己的 <code>.git</code> 不算;<code>.gitignore</code> 忽略的目录跳过,每层规则相对那一层、取反照 gitignore 写法,<code>.ignore</code> 也读;<code>search.useIgnoreFiles</code> 关着时不读忽略文件、嵌套的 checkout 照样跳过;同一个目录只查一次。</li> <li>Celery 规则:<code>fixtures/celeryApp.py</code> 的应用卡片和定时任务卡片逐字同上面的样例(列名、「12 条,另有展开 1 处」、行尾注释、条目里单独成行的注释、跨行的 <code>10⏎ * 60</code>、<code>每 get_settings().… 秒</code>、展开一行);任务名是引用、下标指回源码原文;应用卡片的位置参数按参数顺序认、别的参数各一行、展开、属性 docstring、别名;没导入 celery、不是 <code>Celery(...)</code> 的不认;频率换算(秒数算式、<code>timedelta</code>、<code>crontab</code> 各种写法、<code>schedule</code>、换算不了的「每 X 秒」和照写法);说明取哪些注释;条目名和任务名不同、没写任务名、任务名不是字符串、值不是字典、选项一列有才有;<code>conf.update(beat_schedule=…)</code>、不带 <code>beat_schedule</code> 的 <code>conf.update</code> 和 <code>autodiscover_tasks</code> 照常。任务名索引:显式 <code>name=</code>、默认「模块路径.函数名」(包、<code>src</code> 下的命名空间包、没有包)、<code>@shared_task</code> / <code>@shared_task()</code> / <code>@app.task</code> 不带括号 / 老写法 <code>@task</code>、<code>async def</code>、函数里的只认显式名字、<code>name=</code> 不是字符串的不认、同名几处都给、文件改了和新建 <code>__init__.py</code> 后跟着变、没有任务装饰器的文件不解析。</li> <li>枚举(<code>rules/enum.test.ts</code>、<code>languages/typescript.test.ts</code>):<code>fixtures/enumModels.py</code> 的 <code>CommissionStatus</code> 逐字同上面的样例,<code>SecretScope</code> 的 docstring 当说明、没有说明列;种类 enum、叫「枚举」、访问级别同原来的类、没有标签(badge),编辑范围是整个类;Python 成员规则(dunder、sunder、私有名、<code>_ignore_</code>、lambda、<code>property</code>、<code>nonmember</code>、只写注解的不是成员,<code>auto()</code> 写「自动」,<code>member(x)</code> 取 x,说明取讲它的注释、行尾注释、属性 docstring,装饰器);方法、嵌套类、不是成员的属性照源码顺序是子块,讲成员的注释跟着并进来、类主体最后的注释挂在最后一块上,「编辑」写回整个类;类名、成员名的定义范围和带下标的词指回源码原文;没导入 enum、SQLAlchemy 的 <code>Enum</code>、不是枚举类型的父类不认,<code>import enum as e</code> 的 <code>e.IntEnum</code> 认。TypeScript:<code>enums.ts</code> 里 export / const / declare / 函数里的 enum 都是枚举块、JSDoc 进头部,写的值、自增算出的值(接不上数字就没有)、说明(上面的注释和 JSDoc、行尾注释,同一行的 JSDoc 讲下一个),访问级别;自增的值和「自动」按 hint。TypeScript 和 Python 同样的成员得到一样的种类、属性表行、列名和格子着色(除了 Python 多的「继承」)。宿主:规则认成现成属性表块时照语言插件报的这种块显示、并进来的不是子块时照常显示;webview 枚举块默认折叠。</li> <li>数据类(<code>rules/dataclass.test.ts</code>):<code>fixtures/dataclassModels.py</code> 的 <code>SandboxAccess</code> 逐字同上面的样例,<code>AdminUserRow</code>、<code>RedirectPayResult</code>(<code>field(init=False, default=…)</code>)、<code>SmsSent</code>(<code>default_factory=dict</code>)的行;种类 <code>dataclasses.dataclass</code>、叫「数据类」、颜色、访问级别同原来的类;编辑范围是整个类、写回原样,<code>__post_init__</code> 是子块;类名、字段名的定义范围和带下标的词指回源码原文;装饰器的写法(别名、<code>dataclasses.dataclass</code>、<code>dc.dataclass</code>)认,pydantic 的、<code>other.dataclass</code> 不认,没导入 dataclasses 不认;每个选项的译法、默认值不讲、别的照写、<code>**x</code>、别的装饰器一行;字段规则(<code>ClassVar</code>、<code>typing.ClassVar</code> 留作子块,<code>InitVar</code> 的类型和「仅初始化时使用」,<code>KW_ONLY</code> 记号并进来、后面的字段「只能按名字传参」、单独写 <code>kw_only=False</code> 的「可按位置传参」),<code>field(...)</code> 的默认值、<code>default_factory</code>(名字、lambda)、各参数、<code>metadata</code> 照写、<code>doc=</code>,说明取讲它的注释、行尾注释、属性 docstring;没注解的类属性、字段中间的方法照源码顺序是子块,「编辑」写回整个类。</li> <li>Pydantic 数据模型(<code>rules/pydantic.test.ts</code>):<code>fixtures/aigcMetadata.py</code> 的 <code>AIGCMetadata</code> 逐字同上面的样例(7 个无默认值字段、选项「严格 · 禁止多余字段」、<code>model_config</code> 不单成一块),<code>TextTrace</code> 的可空字段默认值是「空」;<code>fixtures/createShareRequest.py</code> 的 <code>CreateShareRequest</code> 逐字核对字段、说明、长度约束,<code>@field_validator</code> 仍是子块;种类 <code>pydantic.model</code>、叫「数据模型」、颜色、图标 <code>braces</code>;同一文件里继承模型的也算,<code>TypedDict</code>、<code>NamedTuple</code>、普通类、没导入 pydantic 的不是;别名、<code>Field</code> 的默认值 / <code>default_factory</code> / 约束、说明优先于注释,<code>ClassVar</code> 和没注解的类属性留下;类型里的名字带源码下标;索引只收本文件里能确定的模型、没写 <code>BaseModel</code> 的文件不解析,别的文件里的父类查不到先当普通类、查到再换成卡片;差异:加一个字段标新增,改一个默认值标那一格。</li> <li>FastAPI 接口(<code>rules/fastapi.test.ts</code>):<code>fixtures/routes.py</code> 的 <code>update_user</code> 逐字同上面的样例(查到 <code>Forbidden</code> 是 403 时四条消息写在一行,没查到时不列),名字、颜色、图标、列名、筛选属性、定义范围、编辑范围是整个函数、函数体是子块;着色(URL 的 <code>{参数}</code> / 路径,花括号里的参数名 <code>refers</code> 到函数的参数,方法的 Swagger 颜色、成功的状态码绿色、自动带的 422 淡色,带下标的词指回源码原文);<code>fixtures/skillHubRoutes.py</code> 的 <code>list_skills</code>(Annotated 里的 Query、<code>_</code> 的依赖)、<code>get_skill</code>(HTTPException 的 status_code / detail)、<code>install</code>(Request 不算、<code>-> None</code> 没有内容);<code>fixtures/consoleRoutes.py</code> 的 <code>create_user</code>(<code>status_code=201</code>、Pydantic 模型、调别的函数间接抛的不算)、<code>list_users</code>(查到的类型别名 <code>FilterQuery</code> 是依赖、Literal 是简单类型);参数放在哪儿的各条(路径里的名字含 <code>{x:path}</code>、各种标记两种写法、上传文件、枚举和类型别名查到的、Pydantic 模型、<code>Security</code>、<code>Depends()</code>);标签、依赖、<code>responses=</code> 叠上路由对象的,<code>status.HTTP_…</code> 常量、<code>response_model=None</code>、<code>response_description</code>、<code>raise … from</code>、只写 <code>raise</code>、嵌套函数里的;<code>FastAPI()</code> 上的接口、别的装饰器一行;不认的(没导入、路由对象在别的文件、不是路由对象、<code>websocket</code>、不带括号)。跨文件(工作区索引 + 假的语言扩展):<code>update_user</code> 的 <code>Forbidden</code> 按定义处查到 <code>core/exceptions/schemas.py</code> 的 403,同名的别处的类不算;索引认的类上的 <code>status_code</code>(带注解、常量)、枚举类、<code>Annotated</code> 类型别名,普通类不认。路由器卡片:<code>fixtures/routes.py</code> 的 <code>router</code> 逐字同上面的样例,名字、颜色、图标、列名、编辑范围、定义范围;接口表的函数名带下标、路径里的 <code>{user_id}</code> 各 <code>refers</code> 到各自函数的参数、方法的颜色;接口卡片的「路由器」行排在说明后面、名字带下标指回路由器;写了依赖、默认响应才有这两行,<code>FastAPI()</code> 没有前缀,属性 docstring 是说明,没挂接口时「0 个」、不画表格。</li> <li>React 组件(<code>rules/reactTree.test.ts</code>、<code>rules/react.test.ts</code>,luffy 的九个真实组件):认组件(七个文件里的组件、小写工具函数和不渲染 JSX 的不认;memo / forwardRef 包着的、默认导出、文件末尾 export 的、null 和 JSX 混着返回的;组件体里的语句、提前返回的 if 里的 return 认得出在哪个组件里);构造参数(行内类型字面量、同文件接口带 JSDoc、回调、children、默认值、改名、其余属性、方法签名、<code>FC<Props></code>、<code>forwardRef<R, Props></code>、导入的类型查不到和查到了);组件体的归类、名字用在哪(副作用、渲染时重置、挂在 <code><main></code> 的 ref、处理函数绑在每个按钮上);渲染大纲(原生标签、组件、类、条件、三元、列表、片段、注释、几处返回)和 className 的写法。卡片:组件卡片只有名称 / 说明 / 构造参数,组件体照源码顺序是子块,<code>"use client"</code> 不单独成块(文件里没组件时、它挂着注释时照常成块),JSDoc 是组件块的说明;<code>fixtures/planChecklist.tsx.txt</code> 逐字同上面的样例;QueuedBubble 的状态、引用、变量、副作用、两个事件处理函数、渲染输出照源码顺序、函数体是子卡片,提前 <code>return null</code> 是如果卡片;状态的更新位置、引用的使用位置只写名字和位置;数据源的几种说明;useMemo / useCallback 的派生值、带清理的副作用、触发时机的几种写法、没绑在 JSX 上的处理函数写由谁调用;直接返回 JSX 的组件把大纲写在卡片里;九个文件认领下来都合约定;差异:加一个属性、改一个副作用的依赖,组件卡片属性表那一行新加、副作用卡片「触发时机」逐截标。宿主:<code>libraries</code> 空的规则都启用;略去的块不出块、有子块的不能略去。webview:小标签、条件小框、类名小标的样子(<code>SegmentText.test.tsx</code>)。</li> <li>规则查别的文件里的定义(<code>ruleLookups.test.ts</code>):第一遍当没查到、记下问了什么(同一索引同名的只记一处);查到后叫重排,交出定义处正是的那一项;同一版本不再查;索引里没有同名的不问语言扩展,定义处不是索引里的项算没有,没变不叫重排;新版本先用上一版的、再查、变了才叫重排;语言扩展没答时已知的不动、隔一会儿再查。</li> <li>带自己颜色的词(webview <code>SegmentText.test.tsx</code>):<code>cr-colored</code>,暗色、亮色写在 style 上;讲别处名字的段 <code>data-offset</code> 记那个名字的下标。</li> <li>异常(<code>languages/exceptions.test.ts</code>,真实解析 + 中文直译 + 视图数据):<code>fixtures/appExceptions.py</code> 的 <code>AppException</code>、<code>NotFound</code> 逐字同「异常」的样例(列名随有没有类型、说明变),<code>GatewayTimeout</code> 的多段说明,方法是子块;<code>fixtures/commerceExceptions.py</code> 16 个类都是异常、只有说明的四行、写了 <code>__init__</code> 的构造参数;认哪些类(自带的异常类、<code>Warning</code>、不合后缀的自带类 <code>StopIteration</code> / <code>KeyboardInterrupt</code>、<code>pkg.errors.AppException</code>、同一文件里写在后面的父类一层层往下传、泛型和别的父类混写,关键字参数、<code>Mapping[...]</code>、<code>*bases</code> 不算,函数里的类也认);类属性(带不带注解、只写注解、说明取讲它的注释 / 行尾注释 / 属性 docstring,连着赋值、解构赋值、方法留作子块,讲方法的注释、类主体最后的注释挂在方法、最后一块上),构造参数去掉 self、<code>*args</code> 开头的不去;<code>@dataclass</code> 的异常类照样是异常卡片;编辑范围是整个类、改卡片写回整个类,类名、属性名、参数名的定义范围和带下标的词指回源码原文;TypeScript <code>fixtures/exceptions.ts.txt</code>(<code>extends Error</code>、类字段含 static、参数属性、JSDoc、写在后面的父类、类表达式、<code>ns.X<T></code>,父类名不合习惯的照常是类)、JavaScript 的类字段;TypeScript 和 Python 同样的异常得到一样的行和列名;筛选的种类叫「异常」、没有 badge;属性的中文名和去掉行尾记号。webview:异常块默认折叠(<code>viewState.test.ts</code>),跳到类名闪整块、属性名 / 构造参数名只闪表里那一行(<code>reveal.test.ts</code>)。</li> <li>筛选:块的属性(TS、TSX 有抽象、仅类型,JavaScript / JSX 没有这两个属性、属性表也没有这两行,Python 有访问级别、异步);语言插件自己报的属性叠上去、盖过推出来的,kind 不变;文件里出现过的属性按契约顺序排、语言的排后面,值按第一次出现排、带所有层的块数,名字取翻译插件的、没有的显示 id,中文直译的名字和属性表一致;下拉框只给两种以上值的属性(或一种值正被去掉的),按钮上的字;全选 / 取消全选只动这份文件里的值、别的文件的去掉状态和别的属性不动、取消全选藏掉有这个属性的块(整棵子树)、再全选全回来;同一属性内「或」、不同属性「且」、没有这个属性不受影响、藏整棵子树、「已隐藏」只数直接筛掉的;筛选记在 <code>workspaceState</code>、新实例读得回来、所有订阅的预览收到、形状不对的当没有;跳到被筛掉的块临时露出路径、目标的子块照常筛、文档刷新只留正在编辑的。</li> <li>文件注释:键相对工作区文件夹、<code>/</code> 分隔,根和外面的路径没有键,手写的 <code>./</code>、结尾 <code>/</code> 规范掉;保存去空白、清空即删;文件改名、目录挪位置里面各层一起走、名字只是前缀相同的不动、跨工作区文件夹先取出再放进去;删目录连带里面的;读写:文件缺失 / 解析失败 / 不是对象返回空表、跳过不合法条目、建 <code>.spx</code>、键排序、删光留 <code>{}</code>;有注释 / 没注释的行的描述、悬停提示(Markdown 转义)、contextValue,和 <code>package.json</code> 里菜单的 when 子句对得上。</li> <li>侧栏行上的菜单(<code>explorer/rowMenus.test.ts</code>,读 <code>package.json</code>):行尾按钮先「复制」子菜单再注释,有没有注释换「添加 / 编辑注释」,工作区根都没有;「复制」子菜单带 <code>$(copy)</code>,注释图标 <code>$(note)</code> / <code>$(edit)</code>;子菜单和右键菜单里文件四项、目录三项(有没有注释一样)、菜单上的字;复制命令都登记了、和代码里的命令表一致、命令面板里藏起来;「在系统文件管理器中显示」文件和目录(含未提交、有注释)都有、工作区根和分组行没有、审查时也有,三个系统各只出自己那一项、字和 VS Code 自带的一样,自成一组排在复制和注释中间,不挂行尾按钮、命令面板里藏起来。</li> <li>侧栏定位:树上一行和要找的路径的关系(自己、上层目录、名字开头相同的兄弟目录不算、别的协议不算);<code>explorer.autoReveal</code> 的 <code>true</code> / <code>false</code> / <code>focusNoScroll</code> / 没读到;<code>autoRevealExclude</code> 命中路上任何一层都不跟(<code>explorer/treeReveal.test.ts</code>)。树的数据源(<code>explorer/fileTree.test.ts</code>,假的 vscode 和磁盘):多层目录 <code>getParent</code> 交回 <code>getChildren</code> 给过的同一个对象、根的上一层没有;<code>nodeOf</code> 找到文件、目录,顺着 <code>getParent</code> 走回根;工作区外、<code>files.exclude</code> 藏了的、盘上没有的找不到;只摆改动的文件时在就找到、上一层一直走到分组行,没摆的找不到,同一文件在两组里取最先摆的,分组没名字时根在最上面。</li> <li>审查工单: <ul> <li>改动的文件树(<code>review/changedFiles.test.ts</code>):原型那 6 个文件加一个改名的,目录层级照留、每层只有通往改过的文件的目录,文件带改动和相对仓库根的路径,目录没有改动;排法同普通的树;文件换成同名目录两项都留着;没有文件时根下面是空的。状态 <code>added</code> → A、<code>deleted</code> → D、别的都是 M,三种颜色是 git 装饰色、删掉的提示「PR 里删掉了」。</li> <li>选工单弹框(<code>review/pickList.test.ts</code>):一项的字(有 / 没有工作副本、正在读、读不到、没有 PR);一打开就摆出各项、有 PR 的在读、在转圈,读到一个只换那一项(别的项是同一个对象、次序不变),都读完(没读到也算)停止转圈,没 PR 的、没有工单的一开始就不转;按列分组:先「审查」再「进行中」、每组前一行列名、没有工单的列不出标题、换掉一项后分组不变;工单从哪来:看板这次加载过用它、没加载过用存下的快照(JSON 里字符串的 PR 号认得,只留「审查」列和有 PR 的「进行中」列的 Gitea 工单)、都没有或快照坏了是 undefined。</li> <li>规则和字(<code>review/reviewRules.test.ts</code>):标题栏的 description / message;弹框列哪些(「审查」列都列、「进行中」列只列有 PR 的、别的列不列、只要 Gitea 的)和次序(先「审查」组再「进行中」组,组里工单号大的在前),列名,PR 号只认正整数;「审查」「进行中」列有 PR 的能审查,工单没了、两列都离开了、没有 PR(两列都一样),PR 没了、合并了、关了的原因。</li> <li>菜单(<code>explorer/rowMenus.test.ts</code>):标题栏先「选工单」、审查时才有「显示全部」、再「刷新」,字和图标;审查时注释按钮不出、复制照旧;命令面板里「显示全部」只在审查时有。</li> <li>工作副本(<code>test/git/reviewWorktree.test.ts</code>,真的 git:bare 的 origin 上推 <code>refs/pull/7/head</code>):拉 PR 的提交建不挂分支的 worktree(父目录不在也建、本机不多出分支),目录手删后同一路径能再建;快进:PR 往前走了且干净 → 快进,已经最新 / 本地在它之上 → 不动,只有未跟踪的文件照样快进,有没提交的改动、本地另有提交 → 不动且改动留着,目录不是工作副本的根 → 报错、外面的仓库不动。</li> </ul> </li> <li>代码差异: <ul> <li>块对齐和整棵树(<code>diff/blockDiff.test.ts</code>,真实的 Python 解析):原型的 <code>mutex.py</code>(改之前是编的)比出「新增 2 · 修改 3 · 删除 1」,新增块的子孙跟着算新增、只有最外面挂小标,<code>try_acquire</code> 自己改了、带属性表的标记,子块 <code>token</code> 改了、<code>ok</code> 和返回没变,<code>release</code> 只有子块改了(不算自己改了),删掉的 <code>_is_owner</code> 排在新增的 <code>renew</code> 后面、只带自己的内容、没有源码下标;没变的文件没有改动;子块里删掉的排在最前面时没有 after。对齐:有名字的按种类 + 名字配(挪了位置也配、同名照先后、种类不同不配、改名一删一增),没名字的按最长公共子序列和相似度配,种类不同不按相似度配。</li> <li>卡片里的标记(<code>diff/viewDiff.test.ts</code>):只是源码下标不同不算改;大部分没变时逐截标、删掉的去掉下标,改得多整段旧值 → 新值;框按样式、框头、框里的词比;Markdown 按词、中文按字;一种文字换成另一种按字比;子行按第一列配(加行、删行放回原来的位置、改了的只标那一格、新加的行下面各层也是新加的);属性行按属性名配、子行改了不另标值;一行译文、摊开实参的表、说明各比各的;属性表和一行译文没法比。</li> <li>对比什么看从哪儿打开的(<code>diff/diffBase.test.ts</code>、<code>editor/previewEditor.test.ts</code>、<code>explorer/fileTree.test.ts</code>、<code>viewState.test.ts</code>):审查工作副本里的不管从哪儿打开都对比 PR 目标分支,别的只有从「未提交的更改」点开的对比上一次提交;新开的预览按打开请求、开着的再从侧栏点开换成这次的(不比了发 null)、跳定义点链接打到开着的不变、新开的不比、只切标签页不变;「编辑」再切回「预览」照原来的,切过去的文本编辑器标签页关了就忘掉(<code>editor/switchEditor.test.ts</code>);没有打开请求的按页面记着的、页面没记过的不比,打开请求比页面记着的优先;「未提交的更改」一栏的文件行命令带 <code>uncommitted</code>、别的栏带 <code>other</code>;页面记着从哪儿打开的、认不得的值当没记过。</li> <li>什么时候比(<code>diff/previewDiff.test.ts</code>):比完发出去(统计、删掉的块带种类名、一共几行);基准里没有这个文件是新文件;没有基准不发、发过后基准没了发一次 null;旧内容按 key 记着、key 变了重取;同一份文档同一个基准不再比;取不到旧内容记一笔、清掉、下次重取;慢的旧结果不盖掉新的。</li> <li>取旧内容(<code>git/fileAt.test.ts</code>,真的 git):取的是提交里的、不是工作区改过的;提交里没有这个文件是 null;提交不存在抛错。对比基准的字(<code>uncommittedFiles.test.ts</code>、<code>reviewRules.test.ts</code>、<code>changedFiles.test.ts</code>):未跟踪的按状态认(两种 <code>git.untrackedChanges</code> 设置都认)、上一次提交里没有时的三种说明;PR 目标分支的文案;改名的取改名前的路径、PR 里新加的目标分支上没有。「只看改动」开关(<code>editor/blockFilter.test.ts</code>):记进 <code>workspaceState</code>、关了不记、改了发给所有预览带着眼下的块筛选。</li> <li>页面(<code>diffView.test.ts</code>、<code>markdown.test.ts</code>、<code>viewState.test.ts</code>):块按记号认成页面上的 id、找不到的不标、不是这一版不画卡片里的标记,删掉的块按层挂、找不到挂处的和被筛掉的不画,新文件不逐块标,标了改动的默认展开;统计的字;删掉的块收起时的名字;只看改动折哪几段(改过的块下面按层折、删掉的块打断、被筛掉的不算、点开过的和要露出来的不折);各处改动的先后和行、被筛掉的不算;上一处 / 下一处接着走、按视口顶上的行找、到头绕回;带增删的 Markdown 照常排版、跨行逐行包、新加的列表项留住记号。</li> </ul> </li> <li>复制出什么(<code>explorer/copyText.test.ts</code>):文件、目录的名字 / 相对路径 / 绝对路径;不带后缀只去最后一个扩展名(<code>a.test.ts</code>、<code>archive.tar.gz</code>),<code>.env</code> 原样、<code>.env.local</code> → <code>.env</code>、没有扩展名的原样;相对路径总用 <code>/</code>,绝对路径照本机写法(Windows 反斜杠)。</li> <li>块右上角的图标按钮(webview <code>BlockTools.test.tsx</code>,渲染成 HTML):一组一个按钮、没有字、悬停提示(<code>data-tooltip</code>,没有原生 <code>title</code>)和 <code>aria-label</code> 是「编辑」/「预览」、有图标、编辑时带按下标记;给了「问 AI」时它排在前面、只有图标、问答区展开时带按下标记和 <code>aria-expanded</code>;给了「重写注释」时它排在最前面(「问 AI」左边)、只有图标,正在重写时禁用、<code>aria-busy</code>,块还没有注释时叫「按注释规范写注释」。</li> <li>块的概念图标(webview <code>kindIcons.test.tsx</code>,渲染成 HTML):内置种类 → 图标名和原型一一对应(<code>contract.ts</code> 的 <code>KIND_ICONS</code>);这组里每个名字画的就是 lucide 里同名的图标;webview 配了图标的名字和契约的 <code>KIND_ICON_NAMES</code> 一样、没有多的(<code>kindIconNames.test.ts</code>);只有图标、没有字,悬停提示(<code>data-tooltip</code>,没有原生 <code>title</code>)是各自的名字,整排 <code>role="img"</code> 的 <code>aria-label</code> 把名字用顿号连起来,20px 的框、16px 的图形,横排间距紧;语言的种类按 id 上色,规则的图标用规则给的颜色。概念列表(<code>blockView.test.ts</code>、<code>rules/dataclass.test.ts</code>):普通类只有「类」;数据类是「类」和「数据类」,<code>frozen=True</code> 再加「不可修改」的锁,<code>slots</code> 不加;同一个 id 只留第一个(语言的种类优先于后面重复的);悬停名是这些。各库规则的测试核对 <code>badge</code> 的名字、颜色、图标。</li> <li>悬停提示(webview <code>components/ui/tooltip.test.ts</code>):停 500ms 才弹、在同一个元素里移动不重新计时、没等到就移开不弹、弹着时移到另一个上立刻换、移到没提示的地方就收、按下鼠标 / 按键 / 滚动就收且鼠标不离开不再弹、等着弹时按下这次不弹、键盘聚焦立刻弹失焦收、提示是空的不弹;位置在上方、上方放不下翻到下方、左右钳在视口内。Markdown 链接的标题写成 <code>data-tooltip</code>(<code>markdown.test.ts</code>);网页 TSX 里的原生 <code>title</code> 由 lint 规则拦(<code>button</code>、<code>span</code> 上报错,<code>iframe</code>、组件属性不报)。页面上(无头 Chrome,真的移鼠标、按 Tab):种类图标中心点命中的是图标里的线条、没有盖在上面的东西;停 250ms 还没弹、停满 500ms 在图标上方弹「函数」「接口」,颜色是悬停框的那组变量;Tab 到「编辑」立刻弹、Esc 收;页面上没有原生 <code>title</code>;看板上「待接管」、禁用的 .env 锁按钮(禁用的按钮照样弹)、刷新按钮、摘修复弹框里「迁移」的三行文件名(一行一个)都弹,Tab 到按钮立刻弹、Esc 收。亮、暗两套颜色都看过。</li> <li>问 AI: <ul> <li>位置记号(<code>qa/anchor.test.ts</code>,真实的 Python 解析):有名字 / 没名字的块的记号、同一层重复的编号、两个函数里一样的条件互不影响;上面插几行、改空白记号不变;改别的函数的名字只动它和它里面的块;没名字的块自己的源码改了记号变、有名字的块改签名不变。</li> <li>存法(<code>qa/qaMap.test.ts</code>):记一次、删一次、删光不挂 / 不记;文件缺失、解析失败、不是对象当没有;认不出的块和问答跳过、没会话 id 的照认;再问时接着这块最近那条的会话(删了最近的接着剩下里最近的,没问过、没记会话 id 的重新开);用哪个 profile 答的跟着这条存、读得回来,没有的、空的、不是字符串的当没有;手改的顺序、重复的块读进来整理好;写出来排好序、两格缩进、读得回来、删光留 <code>{}</code>;改名、挪目录、删除跟着走。图片:文件名跟着这条存、读得回来,跑到别处的路径、不是字符串的名字跳过;删一次问答它的图片跟着删、别的留着;源文件删了图片跟着删,改名、挪目录图片不动。</li> <li>图片文件(<code>qa/qaImages.test.ts</code>):认的四种格式;写进 <code>.spx/code-qa/</code> 的名字和内容;没带图片不建目录;只认这边起的名字。条目带着的文件(<code>pathMap.test.ts</code>):改表后没有哪条再带着的才删、另一条还带着的留着;删文件时已经没了的不算错、跑到工作区文件夹外面的不碰。</li> <li>问的话(<code>qa/prompt.test.ts</code>):完整问题逐字(要求、文件、外层块的开头、带行号的代码、问题),顶层块没有外层、函数连主体一起贴,很长的块只贴开头、外层块每层最多几行,找不到这块没有,追问的话。</li> <li>命令行(<code>qa/claudeAsk.test.ts</code>,假的 claude):只读的参数、没有跳过权限、问题从 stdin 按 stream-json 进、追问带 <code>--resume</code>;stdin 的那条消息是文字加按先后的图片,没贴图只有文字、问题不上命令行,追问和会话找不到重问都带图;问一次的过程和结果、在工作区文件夹里跑、带超时和叫停、带选的 profile 的 <code>--settings</code>(「默认」不带);追问接着会话(换了 profile 也带 <code>--resume</code> 和新的 <code>--settings</code>)、会话找不到时按完整问题重问(照样带选的 profile)、别的失败不重试;失败给人看的话。</li> <li>profile 下拉(<code>qa/askProfiles.test.ts</code>):profile 列表 → 选项(默认 profile 在不在里面、一个都没有只剩「默认」)、没选过选哪个、选过的不在了回到默认;选了记进 <code>globalState</code>、新的实例读得回来、所有订阅的预览收到;选的那个的路径进命令行的 <code>--settings</code>,「默认」不带。</li> <li>stream-json(<code>qa/answerStream.test.ts</code>,照本机 claude 2.1 的输出截的):会话 id、逐字的回答、读文件、新回复换掉前面那句、没开逐字片段时一样、一条回复里两段空一行、result 没带回答时用眼下的、失败的原因、坏行和子代理的消息不算。</li> <li>正在答的问题(<code>qa/asks.test.ts</code>):进展、答完、叫停(结束进程、报 stopped、这块能再问)、出错、同一块只答一个、最多几个、全部叫停。</li> <li>跑 claude(<code>cc/spawnClaude.test.ts</code>,假的 claude 脚本):逐行交出、最后没换行的那段、参数和 cwd、给了 input 写进 stdin 再关上 / 不给时 stdin 是空的不卡住、退出码非零带原话、叫停马上结束进程、超时。</li> <li>页面(<code>qa.test.ts</code>、<code>AskThread.test.tsx</code>):问一次、答完记下存成的那条、收起结束这一轮、重新加载接上还在答的、停止和失败、不认识的消息不理;问的时间、回答的前两行;问答区没问过不画、没展开时「问过 N 次」下面默认摊开历史(没有输入框)、展开的历史 / 当前这一轮 / 输入框、出错和停止;profile 下拉在输入框和发送按钮中间、写选中的名字、是能用键盘打开的弹出列表按钮、没收到选项时写「默认」,扩展端发来的下拉问答刷新不丢;历史每条的灰字写「时间 · profile 名」,没有 profile 的只写时间;带图的历史和当前这一轮在问题下面、回答前面一排缩略图(能点开看大图、不能去掉),没带图的不画;这一问带的图片答完、失败、停止都还在,重新加载接上的没有。贴图拖图的规则(<code>qaImages.test.ts</code>):剪贴板里只取文件、文字不算;四种格式、5 MB(正好 5 MB 的算)、连同已有最多 6 张,没加上的按原因合成一句提示;发出去的 base64 去掉 <code>data:</code> 前缀。(webview 的单元测试认 <code>@/</code>,靠 <code>vscode/vitest.config.mts</code> 里同 Vite 的别名。)</li> <li>页面上贴图(无头 Chrome,模拟粘贴、拖放、按键):粘贴图片加缩略图且不拦文字、只粘文字不加图;太大、不是图片、超过张数不加并提示;✕ 去掉后光标回到输入框;点缩略图出大图、焦点在「关闭」、Esc 只关大图不收起问答区、焦点回到缩略图,点一下也关;拖进来的图加上;Shift+Enter 不发送、Enter 发送带上图片、发送后缩略图清空、这一问下面显示它带的图;输入框里 Esc 照旧收起。亮、暗两套颜色都看过。</li> </ul> </li> </ul> <h2>收尾</h2> <p dir="auto">按仓库约定:<code>package.json</code> 版本 patch +1 → <code>pnpm ext:package</code> → 版本号改动一并 commit → 本机 <code>code --install-extension <vsix> --profile augment</code>。</p> </body></html>