# 代码阅读(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 的写法)是这个变量的说明:常量、变量块进属性表「说明」一行(紧跟名称),字段块排在一行译文下面;不再单独成块,它的源码行算在这一块里,「编辑」时和赋值一起改 | | 注释按 Markdown 显示 | 注释块和函数、类、变量、字段的「说明」按 Markdown 渲染,像 Kotlin 的 KDoc:去掉注释符号、引号和共同缩进后的原文原样交给 Markdown,不按语言转换(reStructuredText 的 `:func:`、JSDoc 的 `@param` 都照抄,注释以后慢慢改成 Markdown 写法);段内单个换行是软换行,空行分段;仓库内图片按原图比例显示,点了打开原图,没有就写「找不到图片」 | | 跳到定义 | 和普通编辑器一样 ⌘ / Ctrl + 点击名字跳到定义,定义由 VS Code 装的语言扩展(Pylance、TypeScript 语言服务……)查;目标语言有代码阅读就在预览里打开 | | 高亮名字的各处出现 | 和普通编辑器一样单击名字就高亮它在这个文件里的各处出现,由语言扩展按语义认(同名的别的东西不算),颜色用 VS Code 的单词高亮色,再用 focusBorder 色补一笔(当前那一处描框、别的画细线);看不见的(折叠、筛掉的)点的时候不自动展开,筛选栏上写「名字 · 2/5」;⌘↓ / ⌘↑(可改绑的命令)在各处之间跳,跳到折叠着的地方会展开 | | 侧栏文件树 | 只看和打开:展开目录、打开文件、刷新、全部折叠,外加给目录 / 文件记注释、按审查工单只看 PR 改过的文件;不做新建 / 重命名 / 删除 | | 审查工单 | 标题栏第一个按钮「选工单」列出看板「审查」列的工单和「进行中」列里已经有 PR 的工单(按列分组),选中后树里只剩这张工单的 PR 改过的文件(目录层级照留、全部展开,按 M / A / D 着色,删掉的列出来但打不开);点开的是本机这张工单的工作副本里 PR 最新的代码,不标差异、不开 diff;本机没有工作副本就拉 PR 分支建一个。弹框列的是看板上现在摆着的工单(不再加载一遍),选中后重读那张工单和它的 PR。见「审查工单」 | | 文件注释 | 读代码时给目录 / 文件随手记一句:存在各工作区文件夹的 `.spx/code-notes.json`,跟着仓库走;在 VS Code 里改名、挪位置、删除时注释跟着变 | | 问 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 项」,各项展开成「键 \| 值」两列的子行,值又是字典 / 对象时一层层往下展开;参数默认值照旧一行 | | 输入参数 | 排成带表头和边框的表格:名称 \| 类型 \| 默认值,表头写了「默认值」,格子里不再写「默认 」 | | 库规则 | 第四种插件:按库认出一种写法,把那块整个换成专门的卡片(内置 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`。 - 鼠标停在目录 / 文件行上,行尾一排按钮(`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)?$/` 挑。 ### 文件注释 读代码时给目录或文件随手记一句(原型画板「文件注释 · 悬停出按钮 / 填写 / 已保存」)。 - 鼠标停在目录 / 文件行上,行尾「复制」后面的按钮(`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`),由 `index.ts` 接上。 ### 审查工单 看板「审查」列常压着好几张工单,逐张读 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 #`(`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//head`(Gitea 给每个 PR 留的引用),能不丢东西时快进到 PR 最新的提交(`headSha`):有没提交的改动(未跟踪的文件不算)、本地另有 PR 之外的提交时照原样用;拉取、快进出错也照原样用,提醒一声。目录必须是工作副本的根,否则不动(`git -C` 会找到外面的仓库上去)。 - 没有就拉 PR 的分支(同上的 `refs/pull//head`)建一个:和实施的 worktree 同一个目录模板(插件设置里的 worktree 目录,`$feature_name` 换成 `review-<工单号>`),停在 PR 最新的提交上、不挂分支(`git worktree add --detach`),不和实施用的 `feature/…` 分支抢名字。不写进工单的本机记录:看板上删 worktree 会连带删工单的 feature 分支,审查建的这个不该走那条路。 - 审查时的树:根只有一个,是工作副本,名字照写工作区文件夹的;下面只有 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 # · 改了 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 只留第一个: 1. 语言的种类,就是这块原来的种类(类、函数、变量……)。图标是内置种类的(`contract.ts` 的 `KIND_ICONS`),名字是翻译插件给 `kind` 值起的(见「筛选」;没有翻译插件时是种类 id)。不另填颜色:图标按 id 用 CSS 的 `--cr-kind-*`(见「颜色」)。 2. 语言插件另声明的概念(`LanguageAdapter.concepts`,可选)。认到才出现(比如异步、抽象)。不填就只有种类那一个。宿主不按语言写判断。 3. 库规则认领成自己的卡片时,上面的留下,不换掉。规则的种类追加在后面:id 是 `RuleCard.kind`,名字、图标、颜色是 `badge` 的。规则还可以再带认出的概念(`RuleCard.concepts`)。筛选的 `kind`、卡片边框色仍是规则的种类(`badge.color`),不因为多了图标就改。认成现成属性表块的(`RuleFacts`,枚举)没有 badge,概念就是换成的那种块的语言种类。 每种内置种类由插件定图标、颜色、名字。同一类语义共用一个颜色,图标区分具体是哪种: | 种类 | 图标 | 种类 | 图标 | 种类 | 图标 | | --- | --- | --- | --- | --- | --- | | import 导入 | `import` | field 字段 | `tag` | try 尝试 | `shield` | | function 函数 | `square-function` | const 常量 | `lock` | catch 捕获 | `shield-alert` | | class 类 | `box` | variable 变量 | `variable` | finally 最终 | `shield-check` | | type 类型 | `braces` | if 条件 | `split` | return 返回 | `corner-down-left` | | enum 枚举 | `list-ordered` | else 否则 | `corner-down-right` | statement 语句 | `code` | | exception 异常 | `triangle-alert` | loop 循环 | `repeat` | comment 注释 | `message-square-text` | | interpreter 解释器 | `terminal` | options 选项 | `sliders-horizontal` | case 分情况 | `signpost` | | command 命令 | `square-terminal` | pipeline 管道 | `square-terminal` | | | | directive 解析指令 | `terminal` | stage 阶段 | `layers` | run 执行 | `square-terminal` | | copy 复制 | `copy` | setting 设置 | `sliders-horizontal` | label 标签 | `tag` | | health 健康检查 | `activity` | start 启动 | `play` | trait 特质 | `network` | | impl 实现 | `square-function` | | | | | 库规则的种类用规则给的图标(`KindBadge.icon`),跟在语言的种类图标后面:SQLAlchemy 数据表 `database`、数据类 `package`、Pydantic 数据模型 `braces`(和类型块同一个图标,颜色不同)、Celery 应用 `server-cog`、定时任务 `calendar-clock`、FastAPI 接口 `globe`、FastAPI 路由器 `network`、React 组件 `atom`、副作用 `zap`、事件处理 `mouse-pointer-click`、serde 可序列化 `braces`、thiserror 错误 `triangle-alert`、axum 接口 `globe`、sqlx 查询 `database`、tauri 桌面命令 `square-function`。数据类写了 `frozen=True` 再加一把锁(`lock`,悬停「不可修改」)。实现块再由规则 `rust.impl` 换成卡片,左边因此有两个「实现」图标。 这组图标由插件自己定义,是一个封闭的集合:契约里的 `KIND_ICON_NAMES`(上表 26 个——命令、管道、执行共用一个,解释器、解析指令共用一个,选项、设置共用一个,字段、标签共用一个——加库规则用的 9 个(React 组件的原子、副作用的闪电、事件处理的点击手势也在里面),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 排版」),其余块是一行译文;按词着色,等宽字体(中文部分用系统中文字体回退)。一行译文里有跨几行写的调用时,摊开实参的表排在译文下面(见「调用与构造」);块带说明(Python 字段的属性 docstring)时,说明按 Markdown 再排在下面。 - 编辑:这一块在源码里的完整代码(按语法树着色):头部连同各子孙块、收尾括号,可以直接改,改完按这整段写回;蓝色边框。按钮换成眼睛「预览」;Esc 也回到预览。 - 复合语句(函数、条件、循环……)块的预览只讲它自己的头部那几行(主体交给子块);收尾括号、空行不成块。函数、类的说明(JSDoc / docstring)也算在函数、类块的头部里;Python 常量、变量、字段的属性 docstring 算在它那一块里(中间隔着的空行也是)。 - 编辑范围是块的完整范围(`blockView.ts` 的 `blockSpans`,和语言无关):从头部开头,到头部、整条语句、各子孙块里最靠后的那个字符。else、elif、catch 这类展平成兄弟块的子句,整条 if / try 按各块的开头切开:每块到下一块开头之前,最后一块到语句结尾(Shell 的 `fi` 归它);`} else {` 写在一行时两块都含这一行。 - 库规则认领的块(数据表卡片等,见「库规则」):种类的图标、颜色和名字由规则给,内容是规则给的属性表;编辑范围同别的块,是原块整条语句(类就是整个类),「编辑」时整段一起改。规则没并进卡片的子块(方法、别的成员)不管写在类主体哪儿(两个字段中间也算),都照源码顺序是卡片的子块,点它们的「编辑」只改它们自己连同子孙那几行。 - 同一行不会同时在两处能改:一块在编辑时它的子孙都不画(代码都在它的编辑框里),回到预览就照原样出来;点一块的「编辑」时,编辑范围和它重叠的别的块(祖先、子孙、共用一行的兄弟块)退出编辑。跳到定义的目标块正被盖住时闪盖住它的那块。 - 折叠:函数、类、枚举、异常块和库规则的卡片打开时默认收起子块(属性表照常显示),其余块默认展开;用户点过折叠箭头的块按他点的,记在视图状态里(见「预览数据与消息」)。 - 顶部筛选栏:见「筛选」;翻译下拉框(多于一种翻译时才显示)也在栏里。 - 解析失败:显示错误原因,并提示用「编辑」查看源码。 ### 筛选 预览顶部钉住一条筛选栏,按块的属性藏掉一部分块(原型画板「筛选 · 默认 / 按访问级别 / 按种类」)。 - 三层分工,webview 不认识任何语言: 1. 语言插件给每块报和语言无关的属性「属性 = 值」,都是稳定的英文 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` 总是块的种类。 2. 翻译插件给属性和值起名(`Translator.attributeNames`:`access` → 访问级别、`public` → 公共、`kind` 的 `function` → 函数、布尔值 → 是 / 否),和属性表里的属性名、值是同一套叫法(中文直译里属性表的「访问级别」「异步」「抽象」「仅类型」行和筛选共用一份表);种类图标的悬停提示也取它。库规则的卡片自己报的属性(`RuleCard.attributes`,FastAPI 接口的 `httpMethod`)由规则起名(`LibraryRule.attributeNames`,用词归规则、按翻译插件的显示语言挑):宿主先取翻译插件给的名字,没给的再取规则给的(`attributes.ts` 的 `mergeAttributeNames`)。没起名的显示 id。 3. webview 只按属性 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`,由开着的侧栏筛选给:`TreeFilter.diffBase`,`TreeFilters` 转给预览): - 「未提交的更改」开着、文件在工作区的仓库里(`workspaceOrder`,审查的工作副本不算):对比上一次提交(HEAD),文案「对比:上一次提交」。 - 审查工单、文件在这张工单的工作副本里:对比 PR 目标分支与 PR 头的合并基(Gitea PR 的 `merge_base`,没给时退回目标分支的提交,同看板「改动」tab)上的版本,改名的取改名前的路径(`reviewBaseFile`);文案「对比:PR 目标分支 」(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 项」这类汇总)跟着变、不另标——原型里参数多了一行时「输入参数」的个数也没标。 - 子行(参数表、导入项、字典的各项、摊开实参的表)按第一列的字配对:多出来的(新参数)整行淡绿底、左边 +,没了的放回原来的位置、整行淡红底划掉、左边 −;配上的逐格比。有加行、删行的表格每行第一格前多一个记号位(+ / −,没变的空着),列照样对齐。 - 多出来的属性行、没了的属性行同样整行淡绿 / 淡红底,+ / − 画在行左边外侧。删掉的内容取自旧版本,词去掉源码下标(不能跳定义、不参与高亮)。 - 删掉的块:放回原来的位置——旧版本里它前面最近一个配上的兄弟所配的新块后面,再越过紧跟着的新增块(新增的在前、删掉的在后,原型里删掉的 `_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` 色。没有默认值的格子留空。折叠「输入参数」那一行时整张表收起;子行 id、跳到定义闪「它那个参数」、高亮名字的各处出现都和列表一样,闪的是表格里那一行(行边框和相邻行共用,改在行里面描一圈聚焦色)。哪些子行排成表格由翻译插件定:给了列名(`PropertyRow.columns`)的排成表格,没给的(导入项、字典 / 对象的各项)照旧是列表,webview 只看有没有列名。 | 种类 | 行 | | --- | --- | | 函数 | 名称(函数色;匿名函数写「匿名」)/ 说明(有说明才显示:JSDoc 去掉 `/**`、`*/` 和每行开头的 `*`,docstring 去掉引号;再去掉共同缩进和首尾空行,`@param` 这类标签行照抄;得到的原文按 Markdown 渲染,见「注释的 Markdown 排版」;里面的名字不能点)/ 访问级别 / 异步(是 / 否)/ 装饰器(有才显示,源码原样照抄、按源码着色,多个换行)/ 输入参数(`N 个` 或「无」,展开成表格,列名 名称 \| 类型 \| 默认值,每个参数一行:名称 \| 类型(没标注写「未标注」)\| 默认值(译文,没有留空))/ 输出参数(返回类型的译文,没标注写「未标注」) | | 类 | 名称(类型色;匿名类 `export default class {}` 写「匿名」;赋给变量、类字段的类表达式写变量名、字段名)/ 说明(有说明才显示,规则同函数)/ 访问级别 / 抽象(是 / 否,只有 TypeScript / TSX 有,看 `abstract class`,TypeScript 的类都有这一行;JavaScript / JSX 没有)/ 装饰器(同函数)/ 类型参数(有才显示:TypeScript `` 每个参数的译文,逗号隔开,如 `K, T 限定为 X = Y`;Python 3.12 的 `class G[T]` 同样)/ 父类(有才显示:TypeScript `extends` 后面的表达式连着泛型实参的译文,如 `Base<字符串>`;Python 类名后面括号里的参数列表照写的顺序逐项翻,关键字参数、`*bases` 也在这一行,如 `Base, metaclass=ABCMeta`;逗号隔开)/ 实现的接口(有才显示:TypeScript `implements` 后面每个类型的译文,逗号隔开) | | 枚举 | 名称(类型色)/ 说明(有说明才显示,规则同函数)/ 访问级别(同类)/ 装饰器(有才显示,同函数)/ 继承(有才显示: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;讲的定义是参数名)。见「异常」 | | 导入 | 来源(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`)不算字面量本身,照旧一行译文。 ### 异常 原型没画,照枚举卡片的画法。异常是语言自己的概念,不是某个库的写法,所以和 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)` 三个都是)。 - 按命名习惯认的部分和它的限制:父类从别的文件导入时语言插件看不到那个文件,只能按名字认(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`,lucide `repeat`,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`)跨了几行的调用,而且整个落在要翻的这一段里(块头部;TypeScript `describe('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`),一层层往下套,多深都一样;套着的表不能折叠,总是展开。每一层都和最外层一样:先按「调用」画、查到定义再刷新成「新建」,名字带源码下标,跳到定义、高亮各处出现、中文名照旧。排版:每套一层往右缩 5px(`PropertyTable` 外面那层的左边距),第一列按内容定宽、窄了能收,长的值在格子里换行,不出横向滚动条。 - 放在哪: - 变量、常量块:初始值 / 值那一行写上面那一行字,表是这一行展开的子行(`PropertyRow.columns = ['参数', '值']`),带折叠箭头、默认展开。值是字典 / 对象字面量时照旧按「字典 N 项」展开,不看里面的调用。 - 一行译文的块(返回、语句、字段……):译文里调用的位置换成那一行字(「返回 新建 UserDetailResponse · 11 个参数」),表排在译文下面(`TextTranslation.table` → 视图数据的 `table`),总是展开;块带说明时说明排在表下面。 - 分工:语言插件报结构(`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,只多认几种块的种类。 - 块(图标、颜色见「预览(代码块树)」「颜色」): 1. 解释器(interpreter,属性表):首行 `#!`。「解释器」写程序名(`bash`),「怎么找」写 `按 PATH 去找(/usr/bin/env bash)`,写死路径的写 `写死的路径(/bin/sh -e)`。 2. 选项(options,属性表):一条 `set -euo pipefail` 一块,每个开关一行:行名是开关的意思(出错就停、用到没设的变量就报错、管道里任一步失败都算失败、打印执行的每条命令……),值是「是 / 否」,后面淡淡写源码里的写法(`-e`、`-o pipefail`、`+x`)。`set -- a b` 这种不是开关,照常是命令。 3. 变量、常量(variable / const,属性表,同别的语言的变量块):赋值和 `local` / `export` / `readonly` / `declare`;`local url="$1" sha="$2" out` 一句几个是一块,没给值的写「无」。常量是 `readonly` 的,和脚本顶层名字全大写、值里没有展开的。 4. 分情况(case):每个情况都短(最多 3 句、每句一行写完、没有注释和嵌套的语句块)时整条一块,一行「按 x 分情况」,下面一张表「情况 \| 做什么」,做的几句在格子里一句一行;否则头部是「按 x 分情况」,每个情况一块子块「情况:以 -apple-darwin 结尾」,要做的各句是它的子块。 5. 函数(function,属性表):紧贴在上面的注释(中间没空行)是「说明」,不单独成块。shell 的函数没有参数表,参数按位置取:「输入参数」写「N 个,按位置」,表「位置 \| 函数里叫」,函数里用 `local x="$N"`(或 `x=$N`)起了名字的写名字,没起的写 `$N`。「输出」是函数自己打印到输出的那几句(`echo` / `printf`,重定向到别处的不算;进 if、循环、分情况,不进 `$(…)`、管道、嵌套的函数),同一文件里有 `$(函数名 …)` 时后面淡淡写「(调用处用 $(fetch …) 拿到)」。「返回」:写了 `return N` 的写那几个值,什么都不打印、也没写 return 的写「最后一句成不成功」。主体各句是子块,同别的语言的函数,默认收起。 6. 命令(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)」)。 7. 管道(pipeline):`a | b | c` 读成「a → b → c」;最后一段是循环(`find … | while read -r link; do … done`)时读成「找出 dest 下面所有符号链接 → 逐行读进 link」,循环的主体是子块。 8. 条件、遍历(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。 - 值和展开(一行译文、变量的值、表格里都一样):`$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,只多认几种块的种类。 - 块(图标、颜色见「预览(代码块树)」「颜色」): 1. 解析指令(directive,属性表):文件最开头连着的 `# syntax=…`、`# escape=…`、`# check=…`,一条一行:「语法 docker/dockerfile:1.7(BuildKit 按这个版本读这份文件)」「转义字符 …」「检查 …」。别的注释、空行、指令之后的不算。 2. 阶段(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 + 点击跳到那个阶段,单击高亮各处出现。阶段默认展开。 3. 执行(run):RUN 里的 shell 由 shell 的语言插件分步(`LanguageAdapter.steps`):只有一条命令时一行,就是那条命令读成的(「pnpm 装依赖(严格按 lock 文件、不跑安装脚本)」),一条循环、条件的子块归这个 RUN;只用 `&&` 连着的一串拆开,一条一步,头一行写「执行」,3 条以上加「(N 条命令,一条失败就停)」(原型 2 条时只写「执行」,10 条时带条数);分几句写的(heredoc 里一行一句)是「(N 条命令)」。续行的 `\` 去掉、各段拼起来读(`RUN apt-get install -y \⏎ curl git`);`RUN < 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` → 不隔离权限 / 隔离权限,别的照抄。 4. 复制(copy):「[从阶段 X | 从镜像 Y] 复制 源 → 目标(属主 u:g)」;源是 `.` 写「整个构建目录」,目标是 `.` 写「./」;`--chmod` → 权限,`--parents` → 保留目录层级,`--exclude` → 跳过 X,`--link` → 单独成层。ADD:源全是网址写「下载」,全是本地的压缩包(.tar、.tar.gz、.tgz……,ADD 会自动解开)写「解压」,git 仓库写「拉取仓库」,`--checksum` → 校验;`COPY < 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 模块开头的 docstring 和别的当注释用的单独字符串)的预览,函数、类、常量、变量属性表的「说明」,字段译文下面的说明。视图数据里它们是 `{ markdown }`(见「通用契约」的 `Markdown`),webview 按数据决定怎么渲染,不猜。 - 原文:翻译插件去掉注释符号(`#`、`//`、`/*` `*/` 和每行开头的 `*`)、引号和中文名记号(`SourceDocument.markers`,见「中文名」),再去掉行尾空白、首尾空行和共同缩进,保留换行,不做别的转换。一个注释节点(块注释、docstring)的第一行紧跟在注释符号 / 引号后面,不参与算共同缩进(同 PEP 257);几条行注释连成的一块每行地位相同,第一行也算,缩进四格的代码块才留得住。 - 渲染: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 不放行外部图片),`![alt](url)` 显示成 `!` 加链接。仓库内图片按原图比例显示,点了打开原图,没有就写「找不到图片」和路径。 - 仓库路径链接(`[名字](从仓库根起的路径)`,可带 `#类名` 或 `#类名.成员`)能点,点了打开文件并跳到符号。 - 样子:正文用界面字体(`--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 里的名字不能点,不参与跳到定义。 ### 跳到定义 - 能点的:预览里(属性表的名称、类型、来源、导入项、默认值、值、字典 / 对象各层子行的键和值、类型参数、父类、实现的接口,以及一行译文)照抄自源码的名字——标识符、属性名、类型名、点分模块名的每一节(`luffy_agent.core.db` 三节各自能点,和 VS Code 一样)、字符串(TypeScript 的模块路径 `'./x'` 就是字符串)。翻出来的关键字、运算符、说明词不能点。 - 链接和编辑器里一样:只有按住修饰键(macOS ⌘,其余平台 Ctrl)、鼠标停在**查得到定义**的名字上时,名字加下划线、换成链接色(`--vscode-editorLink-activeForeground`)、光标变手形;松开修饰键或移开就恢复。有没有定义问扩展端(`vscode.executeDefinitionProvider`,文档 uri + 名字在源码里的位置;库规则标了引用的名字问规则,见「规则自己查定义」,webview 不分这两种);和编辑器一样,每回指到一个名字上(或指着名字按下修饰键、换了文档版本)都现问,回答不留到下次用——Pylance、tsserver 刚启动那几秒会答「没有」;只认最近一次询问的回答,迟到的丢掉。 - ⌘ / Ctrl + 点击: - 一处定义,在当前文件:原地滚过去; - 一处定义,另一个文件,语言有代码阅读:在同一个编辑器组用预览打开(`vscode.openWith`),滚过去;这个文件的预览已经在这一组开着就切过去、发给它,不重新打开; - 一处定义,语言没有代码阅读:在同一个编辑器组用普通编辑器打开,选中定义处的名字; - 几处定义:先弹挑选框(每项「相对路径:行号」,下面一行是那行源码),挑一处再按上面跳; - 没有定义:不动。 - 和普通编辑器里 ⌘ + 点击一样:目标是否按「预览标签」打开听 `workbench.editor.enablePreview`;`workbench.editor.enablePreviewFromCodeNavigation` 为假(默认)时先把当前预览标签固定住,免得被目标替掉。 - 闪哪一处:页面上**就是这处定义**的最具体的那一处,只按源码位置判断(定义提供者给的定义处下标,对上块和属性行带的源码范围),不看属性名文字: 1. 定义是块自己的名字(函数名、类名;只声明一个变量时的变量名,解构模式整个算)→ 整块。块的这段范围由扩展端从 facts 取(`BlockView.defines`),和翻译插件无关。 2. 定义是属性表里某一行 / 子行讲的东西(参数、导入项、一条语句声明几个变量时的某一个、类的泛型参数、对象字面量一项的键——TypeScript 对象的属性名就是定义,嵌套几层都算)→ 只闪这一行 / 子行。范围是翻译插件从 facts 取的节点(`PropertyRow.defines`、`PropertyItem.defines`;字典 / 对象的项标的是键,展开项不标),定义处落在范围里就算(`*args` 的定义在 `args` 上、解构参数里的名字都对得上)。 3. 其余(定义在一行译文里,例如循环变量;翻译插件没填 `defines`)→ 显示着定义处那个名字的块,名字在属性行的子行里时展开那一行(在嵌套的子行里时连它上面各层一起展开);页面上找不到这个名字就取包含定义那一行的块。 - 同一文件原地跳、跳进另一个文件的预览,规则一样。 - 滚过去:目标所在块的祖先块折叠着就展开,目标在折叠的属性行子行里(参数、导入项、字典 / 对象的项)就展开那一行,在嵌套的子行里就连它上面折叠着的各层子行一起展开。目标已在视口里不滚,否则滚到视口正中(比视口高就顶部对齐)。然后目标的边框和底色往聚焦色(`--vscode-focusBorder`)闪三下,共约 1.5 秒后复原;同一处再跳一次从头闪;系统开了「减少动态效果」时改成静止高亮 1.5 秒。目标是属性行 / 子行但块正在编辑(页面上没有属性表)时闪整块。 - VS Code 里没有这种语言的定义提供者(shell):语言扩展答不上时,同一文件里的名字由语言插件认(`LanguageAdapter.symbols`,见「Shell 脚本」),跳到用到的那处之前最近的定义。 - 不按修饰键单击名字是高亮它的各处出现(见下一节);⌘ / Ctrl + 点击名字不会触发块的编辑切换、折叠,也不动高亮。 ### 高亮名字的各处出现 - 能点的名字和「跳到定义」一样:带源码下标的段(属性表、一行译文、字典 / 对象各层子行的键和值,点分模块名每一节各算各的)。「编辑」状态的块里的源码、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-foreground` 50%)。从上到下: - 灰字的历史:这块以前问过的,每条一行问题(`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、手改)文件监听重读,开着的预览跟着变。 ## 通用契约 放在 `vscode/src/codeReader/contract.ts`,语法树类型用 `@vscode/tree-sitter-wasm` 自带的 web-tree-sitter 类型。 ```ts export type BlockKind = | 'import' | 'function' | 'class' | 'enum' | 'exception' | 'type' | 'field' | 'const' | 'variable' | 'trait' | 'impl' // 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` 的 Base(表达式)和它后面的泛型实参 ;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, entries?: EntryFacts[] } // 'a': 1、a: 1、[k]: v;TS 简写 { a } 键和值是同一个节点;entries 同 DeclaratorFacts.entries | { kind: 'spread', value: Node } // **other、...other:value 是展开的表达式 | { kind: 'method', name: Node, node: Node, header: SourceSpan } // 对象里的方法、getter、setter:header 同 BlockShape.header,只到主体开始之前 /** 写在表达式里的遍历(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 entries?: EntryFacts[] // 值是字典 / 对象字面量(Python dictionary、TS object)时是它的各项,空字面量是空数组;别的值没有 } 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 export interface BlockShape { /** 这一块对应的语法树节点(整条语句)。 */ node: Node /** 块自己显示 / 编辑的那一段:复合语句只到主体开始之前,其余是整条语句;块自己的说明(facts.doc、TextBlock.doc)也算在里面。 */ header: SourceSpan children: CodeBlock[] /** 语言自己报的筛选属性:通用词表以外的(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 } export type FactBlock = | FunctionBlock | ClassBlock | EnumBlock | ExceptionBlock | ImportBlock | VariableBlock | 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 : never : never export type TextKind = Exclude /** 预览成一行译文的块(注释块是 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 /** 着色用的词性,源码着色和译文着色共用。 */ 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 照旧是照抄的位置。 */ export interface Segment { text: string, role: TokenRole, offset?: number, reference?: RuleReference, color?: ThemeColor, refers?: SourceRange } /** * 框的样式,决定框头图标和框的颜色:iteration 写在表达式里的遍历(渲染大纲的「对 … 的每一项」也是),图标、颜色同「循环」块; * condition 按条件(「当 … 时」「如果 …」),图标、颜色同「条件」块;branch 条件的一支(「是」「否」「否则」),同「条件」块的颜色、不带图标; * skip 这个条件下不渲染,图标、颜色同「返回」块;tag 小标签(「具名导出」「回调」),只有框头的字、颜色是 color; * classes 收起来的一串类名(「样式 N 个类」),中性色,点开、悬停看 detail。 */ export type GroupStyle = 'iteration' | 'condition' | 'branch' | 'skip' | '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 } /** 预览里显示的一段文字:着色的词(一行译文、属性值),或整段 Markdown 说明(注释块、函数和类的说明)。 */ export type PreviewText = Inline[] | Markdown /** * 属性表里的一行或一个子行讲的是哪个定义:定义出来的那个名字的节点(取自 facts)在源码里的范围—— * 参数名(连 `*` 前缀、整个解构模式)、导入项(名称到别名)、声明的变量名、类的泛型参数(整串)、字典 / 对象一项的键。跳到的定义落在里面就只闪这一行; * 第三方翻译插件可以不填,不填就闪整块。 */ export interface Defines { defines?: SourceRange } /** 属性表里展开的一个子行:几列对齐的格子;items 是展开在它下面的下一层子行(嵌套的字典 / 对象),一层层可以一直往下套。 */ export interface PropertyItem extends Defines { cells: Inline[][] items?: PropertyItem[] table?: PreviewTable // 排在最后一格里、文字下面的表:实参的值里跨几行写的调用的实参表,一层层往下套,总是展开 } /** * 属性表的一行:属性名、着色的值或 Markdown 说明(函数、类的「说明」)、展开在下面的子行(输入参数、导入项、字典 / 对象的各项)。 * columns 是子行各列的列名(输入参数的名称 / 类型 / 默认值):给了的子行排成带表头的表格(只有一层),没给的排成列表。 */ export interface PropertyRow extends Defines { label: string, value: PreviewText, items?: PropertyItem[], columns?: string[] } /** 一张带表头和边框的表:列名,每行几列对齐的格子,同 PropertyRow 给了 columns 的子行。 */ export interface PreviewTable { columns: string[], items: PropertyItem[] } /** 一行译文的块翻出来的:一行译文(注释块是 Markdown 说明);跨几行写的调用还有摊开各个实参的表,预览排在这一行下面。 */ export interface TextTranslation { text: PreviewText, 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 /** node 是写在表达式里的遍历(推导式、生成器表达式)时给出它的结构,别的节点没有;没有这种写法的语言不填。 */ iteration?: (node: Node) => IterationFacts | undefined /** node 是一次调用(含新建对象)时给出它的结构,别的节点没有;没给的语言不展开调用,照常一行译文。 */ call?: (node: Node) => CallFacts | 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 // languageId → 工作区里这种语言的文件(glob):宿主建中文名索引 mentions: (source: string) => boolean // 便宜的文字预筛 find: (root: Node, source: string, languageId: string) => DisplayNameFacts } /** 翻译插件翻一块时拿到的这份源码:源码字符串,解析它的语言插件,和要从注释、说明里去掉的记号。 */ export interface SourceDocument { source: string language: LanguageAdapter 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 // access → 访问级别 values: Record> // 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[] // 一个表达式 / 类型节点整个翻成一行译文,译法同属性表的值;库规则借它 doc: (node: Node, document: SourceDocument) => Markdown | undefined // 说明文字(注释、docstring、当说明用的字符串)→ Markdown 原文,同属性表的「说明」 attributeNames?: AttributeNames // 筛选属性和值叫什么,和属性表的叫法一致;筛选栏、种类图标的悬停提示用它 } // ── 库规则(见「库规则」一节)── /** 一种颜色在暗色、亮色主题下各用哪个;高对比主题跟着暗 / 亮走。 */ export interface ThemeColor { dark: string, light: string } /** 种类图标:插件自己定义的一组 lucide 图标(kebab-case 名字);前 22 个给内置种类,后面的给库规则。规则要新图标就加进这组,没有按任意名字找图标的路。 */ export const KIND_ICON_NAMES = ['import', 'square-function', /* …22 个内置种类的… */ 'database', 'package', 'server-cog', 'calendar-clock', 'globe', 'network', 'atom', 'zap', 'mouse-pointer-click'] as const export type KindIconName = (typeof KIND_ICON_NAMES)[number] /** 内置种类 → 图标名。宿主写进概念列表,webview 不自己查。 */ export const KIND_ICONS: Record /** 一块上认出的一个概念。颜色不填的是语言的种类(按 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。 */ 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 里的对象本身),不再单独成块;没并的照源码顺序留作子块,编辑范围同别的块,是原块整条语句 slots?: RuleSlot[] // 画在卡片某一行下面的子块,一块只能放一处 } /** * 卡片里的一处子块:block(没并进来的子块)不排在卡片后面,画在第 row 行下面当子卡片,照常转成块(翻译、库规则、编辑、问 AI、差异都照常)。 * item 是这一行的子行里讲它的那一个:这时它默认收着,点那个子行的第一格才展开、一次只展开一个(React 组件的事件处理表点名称)。 */ 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;没有翻译插件时照抄源码 doc: (node: Node) => Markdown | 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 // 第一次查时建,照文件、行排好 } 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 // 查自己标的引用;没给就不能标引用 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 就能用上任何翻译插件的属性表;自己语言的字典 / 映射字面量填进 `DeclaratorFacts.entries` 就能展开成子行(中文模板直译按字面量的节点类型查叫法,见 `ZhTable.mapLiterals`)。写在表达式里的遍历(推导式这类)由语言插件的 `iteration` 给结构(节点 → `IterationFacts`),翻译插件排成一个 `SegmentGroup`(中文直译是样式 `iteration`、框头是结果类型的译名);webview 只认框的样式 id,不认语言。调用由语言插件的 `call` 给结构(节点 → `CallFacts`),跨几行写的由翻译插件排成一行字加一张表(`PreviewTable`,见「调用与构造」);webview 只画表。 - 语言插件的信息只经契约到翻译插件:宿主翻每一块时把 `{ source, language }`(`SourceDocument`,language 是解析这份源码的语言插件)交给 `translate` / `describe`,翻译插件要词性、遍历结构都从 `language` 取,不 import 任何语言插件的模块(lint 规则挡住 `translators/` 下的非测试文件 import `languages/`)。所以第三方翻译插件(英文、AI 翻译)拿得到和内置翻译插件一样的信息,第三方语言插件也能配上任何翻译插件。 - 第三方语言插件要给的:`id`、`languageIds`、`grammarPath`、`toBlocks`(块的种类、头部、子块;函数、类、枚举、异常、导入、常量、变量块带 facts)、`tokenRole`。可以给的:facts 里语言有才填的项(async、abstract、typeOnly、access、doc、`DeclaratorFacts.entries`)、`BlockShape.attributes`(通用词表以外的筛选属性)、`iteration`(有写在表达式里的遍历的语言)、`call`(调用的结构)、`displayNames`(这门语言里给定义起中文名的写法)。没给的就少那一样:没有 entries 的值不展开,没有 `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"` 讲成组件卡片的「客户端组件」标签,组件上面没并进块头部的 JSDoc 讲成「说明」),预览不画它、不占块的位置(后面的块 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` 行下面当子卡片(React 组件的副作用画在「副作用」那一行下面);它照常转成块,照样问各规则(副作用那句也被 React 规则认成自己的卡片),编辑、问 AI、差异、筛选都照常。给了 `item` 的是那一行的子行里讲它的那一个:默认收着,点那个子行的第一格才展开、同一行里别的收起(事件处理表点名称)。一块只能放一处,不能同时并进来;`row` 要有这一行、`item` 要有这个子行。 - 只经契约:规则拿到的是块(kind、node、header、children、facts)和 `RuleContext`;源码里的名字照抄时自己带下标(`Segment.offset`),表达式、类型交给 `context.translate`(当前翻译插件的 `expression`,名字带下标,⌘ + 点击、高亮各处出现照常),说明文字交给 `context.doc`(当前翻译插件的 `doc`,和别的「说明」一样按 Markdown 显示),别的文件里定义的名字交给 `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`),宿主不替规则多并。没并进来的子块(方法、规则不讲的注释)不管写在哪(字段中间也算),都照源码顺序是替身的子块,预览里照常看得见。替身的编辑范围同别的块,是原块整条语句(从头部开始,到原块连同各子块的最后一行,类就是整个类),盖住留下的子块的行:替身编辑时预览不画这些子块,同一行不会同时在两处能改(见「预览(代码块树)」)。内置规则并成员时连同紧挨在它前面的注释块一起并(讲的是这个成员),讲方法的注释、最后面的注释照常留下。 - 卡片在预览里:左边的图标是语言的概念(原来的类、函数……)再加 `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`)。扩展端记着最近发出去的那份文档里哪些下标是引用(只对那个文档版本有效),是引用的交给标它的规则的 `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`):从上一个子块之后起紧挨在它上面的注释块(几条行注释连成的一块是几条)、它这一行行尾的注释、紧跟在它后面的属性 docstring,照源码顺序(同 Celery 定时任务的「说明」取法)。 - 并进来的子块:各个成员连同紧挨在它前面的注释块;方法、嵌套类、不是成员的属性、讲方法的注释、最后面的注释照常是子块(不管写在哪),枚举块默认收起它们。 样例(`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 色。 样例(`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=3` 3 的倍数、`pattern` 匹配、`min_items` / `max_items` 至少 / 最多 N 项、`max_digits` 最多 N 位数字、`decimal_places` 小数 N 位。`default`、`default_factory`、`description` 不在这里重复。不认得的照写 `名=值`,不丢掉。 - 说明:`Field(description="…")` 的字符串优先(不是字符串就照写法翻);没有就用字段上方的注释、这一行行尾的注释、紧跟在后面的属性 docstring,原文用空格连起来,comment 色。 样例(`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 | | 路由器 | 挂在哪个路由对象上:它的名字(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、请求体 | | 响应 | 值是各状态码用 ` / ` 连起来(hint),下面一张表格「状态码 \| 内容 \| 说明」,一个状态码一行,按状态码从小到大,不是三位数字的(`"5XX"`)排在最后 | 请求表: - 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 的规则,按下面的先后,第一条对上的算: 1. 写了 `Depends(...)` / `Security(...)`(默认值 `= Depends(x)`,或 `Annotated[T, Depends(x)]`):依赖,不是请求带的。 2. 名字在路径里(`{user_id}`、`{file_path:path}`,前缀和路径都算):路径参数。 3. 写了 `Path` / `Query` / `Header` / `Cookie` / `Body` / `Form` / `File`(同上两种写法):照写的,`Header` 进「请求头」、`Cookie` 进「Cookie」,`Body` / `Form` / `File` 进「请求体」。 4. 类型是别处写好的类型别名(`Int4Query = Annotated[int \| None, Query(...)]`、`FilterQuery = Annotated[FilterList, Depends(...)]`):经工作区索引查到它的标记(见下),照 1、3 放。 5. `Request`、`Response`、`WebSocket`、`HTTPConnection`、`BackgroundTasks`、`SecurityScopes`:FastAPI 自己填的,不列。 6. 上传文件(`UploadFile`、`list[UploadFile]`):请求体。 7. 简单类型:查询参数。简单类型是 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` 这些;没标注类型的也是。 8. 别的(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 文件都启用)。原型:`code.js` 的「React 组件」一节(`TITLEBAR_CARD`、`LEGAL_CARD`、`reactCard`),插件菜单「React 组件」那一页的四张画板;样例是 luffy 的 `components/desktop/DesktopTitlebar.tsx`(`fixtures/desktopTitlebar.tsx.txt`,简单)、`components/LegalDocPage.tsx`(`fixtures/legalDocPage.tsx.txt`,复杂),说明板的例子 `DeleteAppDialog`、`DesktopSettingsDialog`、`AppVaultSummary`、`ShareButton`、`PosterDialog` 也复制进了 `fixtures/`。三种卡片: - 组件:种类 `react.component`,名字「React 组件」,图标 `atom`,颜色暗 #61dafb / 亮 #0a7ea4(组件色,渲染大纲里的组件、「客户端组件」标签同色),和函数块一样默认收起留下的子块。 - 副作用:种类 `react.effect`,名字「副作用」,图标 `zap`,颜色暗 #e5c07b / 亮 #9a6a00。 - 事件处理:种类 `react.handler`,名字「事件处理」,图标 `mouse-pointer-click`,颜色暗 #dcdcaa / 亮 #795e26。 认哪些是组件:名字大写开头;是函数声明、值是函数的变量(箭头函数、函数表达式,直接返回 JSX 的也算),或被 `memo(…)` / `forwardRef(…)`(`React.memo` 也算,可以套几层)包着的函数;函数自己的 return(嵌套函数里的不算)里有一处返回 JSX(元素、片段,三元、`&&` 的某一支是 JSX 也算;和 `null` 混着返回也算)。小写开头的工具函数、不返回 JSX 的大写函数照常是函数块。组件里又定义的组件自己也是一张组件卡片,不算处理函数。 整块换成组件卡片,不再是普通函数卡片的输入参数 / 输出参数。文件开头的 `"use client"`(文件里有组件时)和组件上面没并进块头部的 JSDoc(值是 `memo(…)`、直接返回 JSX 的常量块)略去(`RuleOmit`),讲成卡片上的「客户端组件」标签和「说明」。属性表(有才显示,没有就不画): | 行 | 内容 | | --- | --- | | 名称 | 组件名(组件色,带下标);后面跟小标签(`tag` 框):「具名导出」「默认导出」(同 import / export 关键字的颜色;`export default X`、`export { X }` 写在文件别处也算)、「客户端组件」(组件色)、「记忆化」「转发 ref」(同 const 关键字的颜色,memo / forwardRef 包着) | | 说明 | 组件上面的 JSDoc,Markdown | | 构造参数 | `N 个`(hint);属性类型写的是一个名字(接口、类型别名、导入的类型)时后面写「 · 来自 XxxProps」,名字带下标(⌘ + 点击跳到类型)。下面一张表格「名称 \| 类型 \| 默认值 \| 必填」,有属性带说明时多一列「说明」。没写参数时写「无」 | | 状态 | useState / useReducer:「名称 \| 初始值 \| 改它的」 | | 引用 | useRef:「名称 \| 初始值 \| 用在」 | | 读取的数据 | 别的钩子(`useXxx(…)` 赋给变量):「变量 \| 来自 \| 意思」 | | 派生值 | 组件体里别的常量,和 useMemo / useCallback:「名称 \| 怎么算 \| 什么时候算」 | | 副作用 | `N 个`(hint),下面每个 useEffect / useLayoutEffect / useInsertionEffect 一张副作用子卡片 | | 事件处理 | 组件体里定义的函数(函数声明、值是函数的常量):「名称 \| 绑定在 \| 做什么」,点名称在表下面展开它的子卡片 | | 渲染 | `N 处返回`(hint),下面是返回的 JSX 画成的结构大纲 | 构造参数:属性类型的字段照类型里的顺序一行一个(类型字面量、同文件的接口 / 类型别名,交叉类型各部分接起来,`Readonly<…>`、`PropsWithChildren<…>` 看里面;没标注参数类型时 `const X: FC` 的泛型实参、`forwardRef` 的第二个泛型实参),解构里有、类型里没有的接在后面,`...rest` 最后(类型写「其余的属性」)。名称是属性名(定义处带下标:解构出来的是解构那处,没解构的是字段;跳到定义只闪这一行),`children` 后面标「子元素」、函数类型的属性(`(x) => R`、方法签名)标「回调」(组件色的小标签),解构时改了名写「(组件里叫 x)」;类型照翻译插件翻,方法签名写成函数签名;默认值取解构的默认值,没有写「—」;必填看类型的 `?`(是 / 否,storage 色),没类型可看时写了默认值的「否」、别的「—」;说明是字段上面的 JSDoc / 注释、这一行行尾的注释(comment 色,几行接成一行)。类型是导入的名字时经工作区索引查它的字段(索引 `react.propTypes`:看工作区里所有 `**/*.{ts,tsx}`,预筛有 `interface X` / `type X` 才解析,按 typescriptreact 解析;从文件顶层认出接口、类型别名,键是名字、值是各字段的名字、类型写法、可省略、回调、说明的 JSON),查法同「规则认领时查别的文件里的定义」:先照解构的出行、类型「未标注」,查到了按字段重排、重发;别的文件里的类型照写文字(type 色)。 状态、引用、读取的数据、派生值: - 名称、变量带下标(解构模式照常翻),跳到定义只闪这一行。 - 状态的初始值:useState 的实参、useReducer 的第二个实参(后面括号里写 reducer);改它的:改状态的函数名(function 色,带下标),后面括号里写在哪儿改,「;」隔开:「副作用」(几个时「副作用 2」)、处理函数名、派生值名、「 的 onChange(传 e.target.value)」(绑在 JSX 属性上;在列表里一项项渲染的前面加「每个」)、「渲染时,当 … 时」(渲染时按条件重置状态的 `if (…) { setX(…) }`:主体都是在调改状态的函数,这句并进卡片)。 - 引用的用在:「挂在
的 ref 上」(元素写成 ``:组件用组件色、能跳到定义(`refers` 到标签名),原生标签灰色,带 `aria-label` / `title` 字符串时后面写「(返回)」),别处用到的「副作用 1、handleSectionClick 里用它」。 - 读取的数据的来自照常翻;意思:库自带的钩子照词表(next/navigation 的 `usePathname` 当前路径、`useRouter` 路由器,跳转用、`useSearchParams`、`useParams`、`useSelectedLayoutSegment(s)`,react 的 `useId`、`useTransition`、`useSyncExternalStore`、`useOptimistic`、`useActionState`、`useDeferredValue`,react-dom 的 `useFormStatus`);`useContext(X)` 是「从 X 上下文读」;传选择器的 `useXxx((s) => s.a.b)` 是「从 xxx store 读 a.b」(store 名:导入路径最后一节是 store 时取上一节,`@/lib/desktop/store` 是 desktop;否则去掉 store、use 字样,再不行取钩子名),选择器不是直接取属性的写「从 xxx store 算 …」;自己项目里的(相对路径、`@/` 这类别名、同文件)是「自定义钩子」,别的库的是「xxx 的钩子」(hint)。 - 派生值的怎么算照常翻:useMemo 是算值的那个表达式,useCallback 写「函数 」加函数;主体是语句块时写「N 句」,那句照常是块、放进这一行对应的子行上,点名称展开(useCallback 的用事件处理子卡片,useMemo 的照常是变量块);什么时候算:普通常量「每次渲染」,useMemo / useCallback 按依赖读(同副作用的时机)、后面写「(useMemo)」。 副作用子卡片(组件体里那句 `useEffect(…)` 照常是块,被规则认成这张卡片,放进卡片「副作用」那一行下面):时机按依赖数组读,`[a, b]` 是「a、b 变了时」,`[]` 是「只在挂载时」,没写是「每次渲染后」(keyword 色),useLayoutEffect 后面加「,浏览器绘制前」;做什么是紧贴在上面的注释(几行接成一行,hint 色;这段注释并进组件卡片),没写注释时写「N 句」;副作用函数里的语句照常是块,放进「做什么」那一行下面;最后返回清理函数时那句 return 并进来,多一行「清理」:`() => …` 翻它的主体,语句块一句句翻、「;」隔开。 事件处理:表里名称是函数名(function 色,带下标);绑定在从 JSX 属性反查:「