323 KiB
代码阅读(Code Reader)设计
目标
代码几乎全由 agent 写,逐行读太慢。给插件加一个独立的「代码阅读」入口:打开一个源码文件时,可以切到「预览」,把代码拆成一棵代码块树,每块用模板直译成中文,扫一眼就知道这个文件在干什么;要看细节时单块切回源码,并且可以直接改。
它和现有的 Superpowers 看板(活动栏小火箭)是两套东西,只是放在同一个插件(vscode/)里。
视觉和交互以 Figma 原型为准:docs/prototype/code-reader/figma-plugin/(code.js 里的数据和 test.js 里的期望值就是本文的样例)。
已确认的决定
| 事项 | 决定 |
|---|---|
| 流程 | 源码 →(tree-sitter)语法树 → 通用代码块树 → 译文 |
| 第一版翻译 | 只做中文模板直译;AI 解读以后作为另一个翻译插件加 |
| 第一版语言 | TypeScript / TSX(含 JavaScript / JSX)、Python、Shell 脚本(bash / sh / zsh)、Dockerfile |
| 扩展方式 | 写代码注册:语言插件、翻译插件都实现约定接口,通过本插件 activate() 导出的 API 注册;内置实现也走同一个 API |
| 多种翻译怎么选 | 预览顶部筛选栏最右边的下拉框,整个文件一起换,按语言记住选择;只有一种时不显示 |
| 块筛选 | 预览顶部钉住一条筛选栏,按块的属性(种类、访问级别、异步……)筛;属性由语言插件按和语言无关的 id 报(通用的由宿主从 facts 推),名字由翻译插件起,webview 不认识任何语言;筛选整个工作区共用、记住 |
| 块的种类 | 块左边只画种类图标(插件自己定义的一组 lucide 图标里的一个,和一行字等高,种类色),种类名是鼠标停上去的提示和读屏念的文字,不再写字;内置种类的图标、颜色、名字由插件定(图标在 webview 一处、颜色在 CSS、名字由翻译插件起),库规则的种类由规则给(KindBadge 的名字、颜色、图标)。见「预览(代码块树)」 |
| 侧栏点文件 | 支持的语言默认打开成「预览」;不支持的语言用普通编辑器打开 |
| 块内修改 | 改的就是文件本身:未保存标记、⌘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 #<PR 号>(description);第二行(detail)<文件数> · 本机有工作副本或<文件数> · 本机没有工作副本,选中后拉取 PR 分支。文件数先写正在读改动文件数…,读到换成改了 N 个文件(listPullRequestFiles,各项一起读),没读到写读不到改动文件数、原话记进日志。每补一项整组换一次items,光标按工单号留在原来那项上。本机有没有工作副本按盘上现在的看。 - 「审查」列里 state JSON 没有 PR 号的工单也列出来,第二行只写
没有关联 PR;选中它只提示#<工单号> 没有关联 PR,什么都不变。 - 当前工作区没有 Gitea 仓库、没配 token:关掉弹框,报错。
- 日志(Superpowers 输出通道,来源
codeReader):列了几张、数据来自看板这次加载的还是存下的快照;哪张的文件数没读到和原话;进审查的每一步、不进的原因、出错。
- 工单:看板上现在摆着的,不再加载一遍(整个看板重新加载要翻每张工单的评论,几百张工单时很慢,哪个请求卡住就一直转)。面板这次会话里加载过就用它最近一次加载的(
- 选中后进审查:先重读这张工单(看板的单张加载
loadSingleIssue,弹框里的是看板上摆着的,可能旧了),确认它还在「审查」或「进行中」列、有 PR,PR 还开着(没了、合并了、关了都不进,提示原因),再读 PR 改过的文件、备好工作副本,都好了才换树。这期间视图上转进度(withProgress的viewId),标题下面那行写正在做哪一步(正在读取 PR #447 改过的文件…、正在拉取 PR #447 的分支…)。出错:报错,树不换(原来在审查别的工单就还是那张)。 - 工作副本(PR 最新的代码):
- 本机有这张工单的工作副本就用它:看板记着的 worktree(
worktreePath,盘上还在)优先,其次是之前审查它时建的。用之前先git fetch origin refs/pull/<PR 号>/head(Gitea 给每个 PR 留的引用),能不丢东西时快进到 PR 最新的提交(headSha):有没提交的改动(未跟踪的文件不算)、本地另有 PR 之外的提交时照原样用;拉取、快进出错也照原样用,提醒一声。目录必须是工作副本的根,否则不动(git -C会找到外面的仓库上去)。 - 没有就拉 PR 的分支(同上的
refs/pull/<n>/head)建一个:和实施的 worktree 同一个目录模板(插件设置里的 worktree 目录,$feature_name换成review-<工单号>),停在 PR 最新的提交上、不挂分支(git worktree add --detach),不和实施用的feature/…分支抢名字。不写进工单的本机记录:看板上删 worktree 会连带删工单的 feature 分支,审查建的这个不该走那条路。
- 本机有这张工单的工作副本就用它:看板记着的 worktree(
- 审查时的树:根只有一个,是工作副本,名字照写工作区文件夹的;下面只有 PR 改过的文件和通往它们的目录,目录层级照留、全部展开,每层排法同普通的树。节点照旧用
resourceUri(工作副本里的文件),点文件和平时一样打开(受支持的语言用预览),打开的就是 PR 最新的代码。 - 改动的颜色:
FileDecorationProvider挂在这些文件的 Uri 上,颜色用 VS Code 源代码管理的主题色——修改gitDecoration.modifiedResourceForeground徽标M,新增gitDecoration.addedResourceForeground徽标A,删除gitDecoration.deletedResourceForeground徽标D;目录不上色。Gitea 的状态added→ A,deleted→ D,别的(changed、改名的renamed在新路径上、copied……)→ M。编辑器标签页上打开的这些文件也带着,退出审查就清掉。 - 删掉的文件:列出来但打不开(没有命令),悬停提示
PR 里删掉了。原型把名字划掉,VS Code 的树行没有删除线,这是和原型的有意差异,只靠红色和D。 - 标题栏:视图
description写审查 #<工单号>(标题后面淡色),message写#<工单号> <标题> · PR #<PR 号> · 改了 N 个文件;上下文键superpowers.codeReader.reviewing为真时出「显示全部」($(close),navigation@2,命令superpowers.codeReader.showAllFiles,命令面板里也只在审查时有),点了回到工作区文件树。真实的视图没有新建按钮,原型里被「显示全部」换掉的那两个不用管。 - 行上的按钮:「复制」照旧(相对路径相对工作副本的根,就是仓库里的路径);注释按钮审查时不出(
when带!superpowers.codeReader.reviewing):注释记在工作区文件夹里,工作副本不是工作区文件夹。 - 刷新:审查时重读工单和 PR 改过的文件,工作副本照上面的规则更新;工单两列都离开了(「审查」↔「进行中」之间挪动不算)、PR 不能看了就提示原因、回到工作区文件树。
- 记住:选中的工单号存
workspaceState(superpowers.codeReader.reviewIssue),重开窗口后视图第一次露出来时接着审查(没打开侧栏不去拉);工单两列都离开了、没了 PR、PR 没了 / 合并了 / 关了,悄悄回到工作区文件树并忘掉;读的时候出错,报错后同样回去。 - 代码:
review/。纯函数(改动状态、改动的文件树、能不能审查、标题的字、弹框的各项)在changedFiles.ts、reviewRules.ts、pickList.ts;看板数据在reviewSource.ts(看板上摆着的工单由看板给:panel/kanbanSnapshot.ts的boardIssues;仓库和 token 与看板「改动」tab 共用gitea/repoContext.ts);工作副本在reviewWorktree.ts(git 操作在git/worktree.ts);着色在changeDecorations.ts;弹框、进出审查、记住在reviewMode.ts。文件树只多一个「换成这些文件」的入口(showChanges)。
编辑器右上角切换
- 受支持语言的文件:普通编辑器里标题栏显示「预览」,预览里显示「编辑」。点一下在同一个标签页位置换成另一种:用 VS Code 内部的
reopenActiveEditorWith(「重新打开编辑器的方式」背后的机制)原位替换。不能用「另开再关原标签」:文件有未保存改动时关原标签会弹保存框,选「不保存」会丢改动。 - 用
editor/title的navigation组、不带图标的命令,让按钮显示文字。若实测 VS Code 不渲染文字,改成带图标($(open-preview)/$(go-to-file)),标题保持「预览」「编辑」。VS Code 标题栏做不出原型里的分段控件,这是和原型唯一的有意差异。 - 受支持语言 = 当前注册的语言插件覆盖的
languageId,通过setContext('superpowers.codeReader.languageIds', [...])提供给 when 子句(resourceLangId in superpowers.codeReader.languageIds)。
预览(代码块树)
-
每一句是一块:导入、函数、类、枚举、异常、类型、字段、常量、变量、条件、否则、循环、尝试、捕获、最终、返回、语句、注释;Shell 脚本另有解释器、选项、分情况、命令、管道(见「Shell 脚本」);Dockerfile 另有解析指令、阶段、执行、复制、设置、标签、健康检查、启动(见「Dockerfile」)。
-
嵌套的块放在父块下面、往右缩进;每一层左边一条竖线(颜色
--vscode-editorIndentGuide-background)。 -
块的组成(从左到右):折叠箭头(只有有子块的块才显示,其余占位对齐)、种类图标、内容、右上角一排图标按钮。
-
种类图标(原型
kindBadge、画板「Prototype / 块种类图标」):块左边只画图标、不带字,和一行字等高(20px 的框,图形 16px,lucide 的线宽 2.2),颜色是种类色;种类的名字(翻译插件给kind值起的,见「筛选」;库规则的种类是规则给的badge.label)写在悬停提示(见下「悬停提示」,同块右上角按钮)和aria-label里,role="img"。和折叠箭头、内容同一行顶对齐。每种内置种类由插件定图标、颜色、名字:图标从插件定义的一组图标里取(见下),内置种类 → 图标只在 webview 的一处(kindIcons.tsx的KIND_ICONS);颜色在codeReader.css的--cr-kind-*(见「颜色」);名字由翻译插件给。同一类语义共用一个颜色,图标区分具体是哪种:种类 图标 种类 图标 种类 图标 import 导入 importfield 字段 tagtry 尝试 shieldfunction 函数 square-functionconst 常量 lockcatch 捕获 shield-alertclass 类 boxvariable 变量 variablefinally 最终 shield-checktype 类型 bracesif 条件 splitreturn 返回 corner-down-leftenum 枚举 list-orderedelse 否则 corner-down-rightstatement 语句 codeexception 异常 triangle-alertloop 循环 repeatcomment 注释 message-square-textinterpreter 解释器 terminaloptions 选项 sliders-horizontalcase 分情况 signpostcommand 命令 square-terminalpipeline 管道 square-terminaldirective 解析指令 terminalstage 阶段 layersrun 执行 square-terminalcopy 复制 copysetting 设置 sliders-horizontallabel 标签 taghealth 健康检查 activitystart 启动 play库规则的种类用规则给的图标(
KindBadge.icon):SQLAlchemy 数据表database、数据类package、Celery 应用server-cog、定时任务calendar-clock、FastAPI 接口globe、FastAPI 路由器network。这组图标由插件自己定义,是一个封闭的集合:契约里的
KIND_ICON_NAMES(上表 26 个——命令、管道、执行共用一个,解释器、解析指令共用一个,选项、设置共用一个,字段、标签共用一个——加库规则用的 6 个,lucide 的 kebab-case 名字),类型KindIconName是它们的联合,KindBadge.icon只能写其中一个,写别的是类型错误。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 不认识任何语言:
- 语言插件给每块报和语言无关的属性「属性 = 值」,都是稳定的英文 id,不放显示文字。通用词表由契约定(
COMMON_ATTRIBUTES):kind(块的种类)、access(public/private/protected,语言可以加自己的值)、async、abstract、typeOnly('true'/'false')。通用属性由宿主从块的 kind 和 facts 推出来(FunctionFacts.access/async、ClassFacts.access/abstract、EnumFacts.access、ExceptionFacts.access、ImportFacts.typeOnly、VariableFacts.access),只有这一条路,语言插件不用另报;facts 里没填的(语言没有这个概念,例如 JavaScript 没有抽象类、仅类型导入)块就没有这个属性,不是填「否」。语言自己的属性(Rust 的unsafe = 'true')、facts 表达不了的通用属性值(Kotlin 的access = 'internal')填在块的attributes里,和推出来的合在一起,同一属性以它为准;kind总是块的种类。 - 翻译插件给属性和值起名(
Translator.attributeNames:access→ 访问级别、public→ 公共、kind的function→ 函数、布尔值 → 是 / 否),和属性表里的属性名、值是同一套叫法(中文直译里属性表的「访问级别」「异步」「抽象」「仅类型」行和筛选共用一份表);种类图标的悬停提示也取它。库规则的卡片自己报的属性(RuleCard.attributes,FastAPI 接口的httpMethod)由规则起名(LibraryRule.attributeNames,用词归规则、按翻译插件的显示语言挑):宿主先取翻译插件给的名字,没给的再取规则给的(attributes.ts的mergeAttributeNames)。没起名的显示 id。 - webview 只按属性 id / 值 id 筛,显示翻译插件给的名字。
- 语言插件给每块报和语言无关的属性「属性 = 值」,都是稳定的英文 id,不放显示文字。通用词表由契约定(
- 下拉框:这份文件里有两种以上值的属性各一个(只有一种值的不给——筛它等于按种类筛);只有一种值、但这个值正被去掉的也给,不然藏起来的块在栏上找不到原因。排列:通用属性按契约的顺序(种类、访问级别、异步、抽象、仅类型),语言自己的属性按在文件里第一次出现的先后排在后面。下拉菜单顶上一行「全选 · 取消全选」,分隔线下面是勾选列表,列出这份文件里出现的值(按第一次出现的先后)和各有几块(所有层的块都算,例如「私有 3 / 公共 3」)。
- 全选 / 取消全选:把这份文件里这个属性出现的值一起勾上 / 去掉,和逐个勾一样记下来;别的文件才有的值记着的去掉状态不动(筛选整个工作区共用)。取消全选后有这个属性的块全藏起来,没有这个属性的照常显示;菜单不关,可以接着勾。
- 规则:同一属性里勾着的值满足一个就显示,不同属性要同时满足;块没有某个属性就不受那个属性的筛选影响(条件、返回块没有访问级别,按访问级别筛时照常显示);块被筛掉,连同它的子块一起藏,不留空位,层级竖线跟着变短;子块都被筛掉的块不给折叠箭头。
- 汇总:什么都没筛掉时右边写「共 N 块」(N 是整份文件所有层的块数);筛掉了写「已隐藏 N 块 · 清除」,N 只数直接被筛掉的块,跟着藏起来的子块不算;点「清除」整份筛选清空。页面上一块都不剩时写「所有块都被筛选隐藏了。」
- 下拉框上的字:没去掉值时是属性名;去掉了(边框换
--vscode-focusBorder)勾着一种写「访问级别:公共」,几种写「种类:已选 11 种」,一种都不勾写「种类:未选」。 - 记住:记的是每个属性去掉的值(
{ 属性 id: 值 id[] }),所以文件里第一次出现的值默认显示。整个工作区共用一份(不按文件),存在扩展端的workspaceState(superpowers.codeReader.filter),重新加载窗口后还在;一个预览改了,所有开着的预览跟着变。某个属性在这份文件里没出现就没有它的下拉框,记着的去掉的值照样对它出现的文件起作用。 - 跳到定义到被筛掉的块:临时显示目标和通向它的路径(被筛掉的祖先块下面只露出这条路径,目标自己的子块照常按筛选显示,函数照样能展开看主体),本来被筛掉的块右边带小提示「已被筛选隐藏,临时显示」,照常展开、滚过去、闪。筛选一变,或文档刷新(正在编辑的块和它的祖先除外,不能打着字就消失)就回到筛选状态。
- 折叠、跳到定义、属性行 id、滚动位置恢复照旧;被筛掉的块不在页面上,按行滚动(从文本编辑器切过来)落到它前面最近的一块。视口顶部按筛选栏下沿算:记位置、恢复、滚过去都让开筛选栏。
- 样子:筛选栏钉在预览顶部(
position: sticky,块从它下面滚过去),横贯编辑区、下边一条--vscode-panel-border、底色--vscode-editor-background,里面的内容和代码块同宽对齐:「≡ 筛选」(宽 52px)、每个属性一个下拉框(宽 150px、高 24px,--vscode-dropdown-background/--vscode-dropdown-border/--vscode-dropdown-foreground)、右边汇总(descriptionForeground,「清除」--vscode-textLink-foreground)、再右边翻译下拉框;放不下就换行。下拉菜单盖在代码块上面:宽 200px,--vscode-editorWidget-background、--vscode-editorWidget-border、--vscode-widget-shadow阴影;顶上「全选」「取消全选」两个链接(--vscode-textLink-foreground,间距 12px),下面 1px 分隔线(--vscode-menu-separatorBackground),列表太长时只滚列表、这一行不动;每项勾选框(勾着是--vscode-focusBorder底)、值、右边块数(--vscode-editorLineNumber-foreground);悬停、键盘高亮--vscode-list-hoverBackground。 - 键盘和读屏:下拉框按钮
aria-haspopup="listbox"、aria-expanded;菜单role="listbox"、aria-multiselectable,每项role="option"、aria-selected表示勾着。按钮上 ↓ / 回车 / 空格打开(↑ 打开并停在最后一项),焦点落在列表上;列表里 ↑ ↓ Home End 移动,空格 / 回车勾选;Shift+Tab 回到「取消全选」「全选」(普通按钮,回车 / 空格触发),Tab 再回到列表;Esc 关掉并回到按钮;焦点离开下拉框(Tab 出去、点外面)就关。鼠标点选项、全选 / 取消全选时焦点留在列表上,方向键接着能用。
代码差异
审 AI 写的代码时,侧栏只列改动的文件(「未提交的更改」「选工单」审查),预览的卡片标出这个文件相比改之前加了什么、改了什么、删了什么(原型插件菜单「代码差异」:画板「代码差异 · 全部 / 只看改动 / 标记说明」)。普通文件树下预览和平时一样,不标。
- 对比基准(
diff/diffBase.ts的DiffBase,由开着的侧栏筛选给:TreeFilter.diffBase,TreeFilters转给预览):- 「未提交的更改」开着、文件在工作区的仓库里(
workspaceOrder,审查的工作副本不算):对比上一次提交(HEAD),文案「对比:上一次提交」。 - 审查工单、文件在这张工单的工作副本里:对比 PR 目标分支与 PR 头的合并基(Gitea PR 的
merge_base,没给时退回目标分支的提交,同看板「改动」tab)上的版本,改名的取改名前的路径(reviewBaseFile);文案「对比:PR 目标分支 <base 分支名>」(PR 的base.ref)。合并基是 PR 头的祖先,拉 PR 的提交时已经在本机,直接在工作副本里取。 - 取旧内容:
git/fileAt.ts的fileAtRef(git ls-tree看有没有这个文件、git show <提交>:<路径>取内容,走runGit);基准里没有这个文件(未跟踪、新加的、PR 里新加的、仓库还没有提交)不逐张标绿,顶部一条横幅「新文件,全部是新增」,后面淡色写原因(「还没被 git 跟踪,上一次提交里没有这个文件」「上一次提交里没有这个文件」「PR 目标分支 master 里没有这个文件」「仓库里还没有提交」)。
- 「未提交的更改」开着、文件在工作区的仓库里(
- 什么时候比(
diff/previewDiff.ts的PreviewDiff,每个预览一个):每发出一份文档(编辑、保存、块编辑写回、换翻译)之后按眼下的基准重新比;换了筛选、基准挪了(「未提交的更改」盯着的 git 状态变了,比如提交后 HEAD 动了;审查进 / 重读工单)打开着的预览也重新比。预览先照常画文档,差异算好了再补发(diff消息),打开不变慢。取过的旧内容按基准的 key(提交 + 路径)记着,编辑时不用每次问 git;旧版本每次用同一个语言插件、翻译插件、库规则重新排成视图数据,规则查定义、调用的种类用这份文档眼下查到的答案,两边同样的写法翻得一样。取不到旧内容、旧版本解析失败时记日志、不标。 - 块怎么对上(
diff/blockDiff.ts,纯函数):同一父块下的兄弟块对齐——有名字的块(defines:函数名、类名、只声明一个变量时的变量名、规则卡片讲的名字)按「种类 + 名字」配,同名的照先后,挪了位置也配得上;没名字的块(语句、调用、注释……)以此为锚,最长公共子序列找出种类、源码都没变的,夹在相邻两个锚中间的按先后找种类相同、内容最像(按词比,相似度到一半)的配成修改。配不上的旧有新无算删除、旧无新有算新增;改名算一删一增。配上的块整段源码(去共同缩进)一样算没变,否则算修改,接着比子块;自己的内容改了、或源码变了子块却都没变的,是「自己改了」(上一处 / 下一处、总览条停在它上面)。 - 卡片里标什么(
diff/viewDiff.ts,纯函数):修改的卡片在渲染用的视图数据这一层比(翻译好的属性表、一行译文、摊开实参的表、说明),所有语言、库规则的卡片走同一条路:- 属性表按属性名配行;配上的行值变了:着色的词按词(一段着色的词一个)对齐,大部分没变时逐截标(新加的一截绿底、删掉的一截红底划掉),没变的不到四成时整段写成旧值(红底划掉)紧跟新值(绿底);说明这类 Markdown 按词比,中文按字、标点各算一个(
diffWordsWithSpace)。带子行的行子行改了时只标子行,值(「3 个」「字典 2 项」这类汇总)跟着变、不另标——原型里参数多了一行时「输入参数」的个数也没标。 - 子行(参数表、导入项、字典的各项、摊开实参的表)按第一列的字配对:多出来的(新参数)整行淡绿底、左边 +,没了的放回原来的位置、整行淡红底划掉、左边 −;配上的逐格比。有加行、删行的表格每行第一格前多一个记号位(+ / −,没变的空着),列照样对齐。
- 多出来的属性行、没了的属性行同样整行淡绿 / 淡红底,+ / − 画在行左边外侧。删掉的内容取自旧版本,词去掉源码下标(不能跳定义、不参与高亮)。
- 属性表按属性名配行;配上的行值变了:着色的词按词(一段着色的词一个)对齐,大部分没变时逐截标(新加的一截绿底、删掉的一截红底划掉),没变的不到四成时整段写成旧值(红底划掉)紧跟新值(绿底);说明这类 Markdown 按词比,中文按字、标点各算一个(
- 删掉的块:放回原来的位置——旧版本里它前面最近一个配上的兄弟所配的新块后面,再越过紧跟着的新增块(新增的在前、删掉的在后,原型里删掉的
_is_owner在新增的renew和release之间);排在最前面的在这一层最前面。子块里删掉的挂在父块下面,整块删掉的子块不再单列。 - 统计「新增 N · 修改 N · 删除 N」只数最外层带标记的块:新增块的子块、修改块里改动的子块不再数(原型的 2 / 3 / 1)。
- 页面(原型组件
Reader / DiffBar、Reader / UnchangedStrip、Reader / OverviewRuler、Reader / NewFileBanner、Diff / 已删除):- 卡片左边外侧 3px 的改动色条(和卡片等高,间隔 5px):新增绿、修改蓝、删除红,没变的留一条透明的,同一层照样对齐;子卡片各带各的。悬停色条提示「新增 / 修改 / 删除」。
- 新增的卡片连同子卡片底色淡绿(10%),最外面那张左上角压着「新增」小标(绿底白字 10px)。修改的卡片只有蓝条,里面按上面标。
- 删除卡片:红虚线边、淡红底、半透明,没有编辑、问 AI 按钮;默认收成一行「已删除 · <种类> <名字>」(名字是属性表第一行的值,或译文的第一行;种类名取旧版本的筛选属性名),点箭头或这一行展开,下面是旧内容、整张划掉;子块不画。按块筛选同样藏。
- 标了改动的块默认展开(函数、类本来默认收起子块),用户点过的照他的。
- 筛选栏第二行(高 30px,只有有差异数据时才有):比较图标、「对比:<基准>」、「·」、统计(各按改动色,没有的那种不写,都没有写「没有改动」),右边「只看改动」开关、上一处 / 下一处(↑ ↓,悬停提示、
aria-label)。基准里没有这个文件时只写基准。 - 只看改动:每一层连着没变的块折成一条「… N 块没变 点开」(虚线框),被筛掉的块不算也不打断,删掉的块在哪儿就在哪儿断开;改过的块照常,它下面没变的子块同样折;点开一段就恢复这一段(筛选一变重新折)。开关按工作区记住(
BlockFilter,workspaceState的superpowers.codeReader.onlyChanges),所有预览共用;跳到定义落在折起来的块上时临时露出来。 - 上一处 / 下一处:在各处改动(新增的最外面那块、自己改了的修改块、删掉的块,照页面先后,被筛掉的不算)之间跳,展开折住它的块,滚到筛选栏下面、闪几下;跳完没滚过页面就接着刚跳到的那处走,自己滚过了按视口顶上那块的行找;到头绕回另一头。
- 总览条:预览右边缘 14px 宽、从筛选栏下面贴到底,每处改动按它在新文件里的行画一段刻度(修改块只画它自己那几行,删掉的块一条 2px 细线),点刻度跳过去(同上一处 / 下一处)。
- 文档刚改、差异还没重算时:块按记号认,没动过的块照样带色条;卡片里的标记按下标认,只在页面上正是差异算的那一版时画。
- 颜色:取 VS Code 编辑器行号旁的改动色带
--vscode-editorGutter-addedBackground/modifiedBackground/deletedBackground(主题没给时用 Dark Modern 的默认值),行内增删的字用--vscode-diffEditor-insertedTextBackground/removedTextBackground;整行底色是改动色的 14%,新增卡片底色 10%。
属性表(函数、类、枚举、异常、导入、常量、变量块)
卡片外观(折叠箭头、种类图标、底色、悬停高亮、「编辑 / 预览」切换)和其他块一样,只是内容换成一张属性表:左列属性名(界面字体、descriptionForeground 色,宽 76px,这张表里有更长的属性名时整列按最长的那个放宽,值照样对齐在同一处,属性名不折行:shell 的「管道里任一步失败都算失败」),右列是值(等宽字体,按词性着色,长了换行),两列间距 12px,行高 20px。「输入参数」「导入项」和值是字典 / 对象字面量的「值」「初始值」下面展开子行,属性名前带折叠箭头(图标、大小、颜色同块的折叠箭头;点箭头或属性名切换,默认展开,折叠后值「N 个」「字典 N 项」照常显示,只收起子行);子行往右缩进 5px,左边一条 1px 竖线(editorIndentGuide-background1);输入参数例外,是一张表格(见下)。字典 / 对象的子行分键 / 值两列,键列宽 150px(编辑区窄时按 28% 收),值列占剩下的。子行下面还能再展开一层(嵌套的字典 / 对象):带下一层的子行第一列前面有折叠箭头(点箭头或第一列切换,默认展开),同一层里其余子行在同样的位置留空、第一列对齐;下一层同样往右缩进 5px、左边一条竖线,竖线对着上一层的箭头,每层自己的两列对齐。同一层里没有带下一层的子行时不留箭头的位置(和导入项的子行一样)。
输入参数的表格(原型 paramRows):往右缩进 5px,第一行是表头「名称 | 类型 | 默认值」,下面每个参数一行。列宽按整列的内容定,webview 不认是哪种表格(库规则的字段表同样排,见 tableTemplate):整张表格一个网格、各行借用它的列,前几列宽裕时放到整列不换行,最后一列(默认值、说明)分剩下的、至少留 min(20%, 8em);放不下时先让最后一列换行,编辑区再窄前几列平分剩下的宽度、在格子里换行(长名字从中间断开),不撑出卡片、不藏掉哪一列。格子左右留 8px、上下 2px,长类型、长默认值在格子里换行,不挤掉别的列。外框 1px、圆角 4px,格子之间、行之间 1px 分隔线,颜色都是主题的表格分隔色(--vscode-tree-tableColumnsBorder,主题没给时退到 --vscode-editorWidget-border);表头垫主题的表头底色(--vscode-keybindingTable-headerBackground,比编辑区深一档或浅一档),列名界面字体 12px、descriptionForeground 色。没有默认值的格子留空。折叠「输入参数」那一行时整张表收起;子行 id、跳到定义闪「它那个参数」、高亮名字的各处出现都和列表一样,闪的是表格里那一行(行边框和相邻行共用,改在行里面描一圈聚焦色)。哪些子行排成表格由翻译插件定:给了列名(PropertyRow.columns)的排成表格,没给的(导入项、字典 / 对象的各项)照旧是列表,webview 只看有没有列名。
| 种类 | 行 |
|---|---|
| 函数 | 名称(函数色;匿名函数写「匿名」)/ 说明(有说明才显示:JSDoc 去掉 /**、*/ 和每行开头的 *,docstring 去掉引号;再去掉共同缩进和首尾空行,@param 这类标签行照抄;得到的原文按 Markdown 渲染,见「注释的 Markdown 排版」;里面的名字不能点)/ 访问级别 / 异步(是 / 否)/ 装饰器(有才显示,源码原样照抄、按源码着色,多个换行)/ 输入参数(N 个 或「无」,展开成表格,列名 名称 | 类型 | 默认值,每个参数一行:名称 | 类型(没标注写「未标注」)| 默认值(译文,没有留空))/ 输出参数(返回类型的译文,没标注写「未标注」) |
| 类 | 名称(类型色;匿名类 export default class {} 写「匿名」;赋给变量、类字段的类表达式写变量名、字段名)/ 说明(有说明才显示,规则同函数)/ 访问级别 / 抽象(是 / 否,只有 TypeScript / TSX 有,看 abstract class,TypeScript 的类都有这一行;JavaScript / JSX 没有)/ 装饰器(同函数)/ 类型参数(有才显示:TypeScript <K, T extends X = Y> 每个参数的译文,逗号隔开,如 K, T 限定为 X = Y;Python 3.12 的 class G[T] 同样)/ 父类(有才显示:TypeScript extends 后面的表达式连着泛型实参的译文,如 Base<字符串>;Python 类名后面括号里的参数列表照写的顺序逐项翻,关键字参数、*bases 也在这一行,如 Base, metaclass=ABCMeta;逗号隔开)/ 实现的接口(有才显示:TypeScript implements 后面每个类型的译文,逗号隔开) |
| 枚举 | 名称(类型色)/ 说明(有说明才显示,规则同函数)/ 访问级别(同类)/ 装饰器(有才显示,同函数)/ 继承(有才显示: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)三个都是)。
- Python:父类名(
- 按命名习惯认的部分和它的限制:父类从别的文件导入时语言插件看不到那个文件,只能按名字认(PEP 8 要求异常类名以 Error 结尾,
AppException这种以 Exception 结尾的也常见)。所以父类名不合习惯(从别处导入的NotFound、Forbidden当父类)的认不出来,照常是类块;名字合习惯却不是异常的父类(普通类起名RenderError)会被当成异常。同一文件里按名字认,不看作用域(同名的两个类当成一个)。 - 类属性(进「属性」表):Python 是类主体里给一个名字赋值的语句(
x = 1、x: int = 1、只写注解的x: int),连着赋值(a = b = 1)、解构赋值不算;TypeScript / JavaScript 是类字段(public_field_definition/field_definition,含 static;值是带语句块主体的函数、类表达式的照常是方法、类块)。说明同枚举成员:紧挨在上面的注释块、这一行行尾的注释、Python 紧跟在后面的属性 docstring,原文用空格连起来。类 docstring 里谷歌写法的Attributes:一节照原文留在「说明」里,不拆到各属性的说明格子。 - 构造参数:Python 是
__init__的参数去掉第一个(self;第一个是*args这类时不去),TypeScript / JavaScript 是constructor(...)的参数(参数属性public code?: string也是一个参数);写了几个取最后一个(重载的实现写在最后)。*、/分隔符同函数的输入参数,不算参数。类里没写构造函数(继承父类的)时没有这一行。 - 并进属性表的子块:各个类属性连同紧挨在它前面的注释块;构造函数、方法、别的语句、讲方法的注释、最后面的注释照常是子块(不管写在哪),默认收起。异常块的头部(编辑范围)是整个类:从说明、装饰器开始,到类连同各子块的最后一行,「编辑」时整段一起改,盖住留下的子块(同库规则的卡片)。
- 语言插件报的已经是异常块,库规则只认类块,所以异常类不交给库规则:
@dataclass装饰的异常类照样是异常卡片,@dataclass写在「装饰器」一行,注解的字段在「属性」表里。 - 跳到定义:类名是块自己的名字(闪整块),属性名只闪「属性」表里那一行,构造函数的参数只闪「构造参数」表里那一行(构造函数自己的「输入参数」也标了同一个参数,页面上先找到的是卡片里这一行);中文名(
Annotated[…, Comment("…")]、行尾# @Comment …)照常换属性格子里的名字,行尾记号不进说明。
样例(fixtures/appExceptions.py 是 luffy core/exceptions/schemas.py 的快照,测试里逐字核对;str | None 译成「字符串 | 空」):
名称: AppException
说明: 应用异常基类. …(docstring 原文,含 Attributes: 一节)
访问级别: 公共
继承: Exception
属性: 3 个
status_code | 整数 | 500
error_code | 字符串 | "INTERNAL_ERROR"
message | 字符串 | "服务器内部错误"
构造参数: 3 个
message | 字符串 | 空 | 空
detail | Any | 空
error_code | 字符串 | 空 | 空
名称: NotFound
说明: 资源不存在 (404).
访问级别: 公共
继承: AppException
属性: 3 个
status_code | 404
error_code | "NOT_FOUND"
message | "资源不存在"
AppException 的 __init__、to_dict 是卡片的子块。fixtures/commerceExceptions.py(luffy commerce/exceptions.py 的快照)里 16 个类都是异常,只有说明的是名称 / 说明 / 访问级别 / 继承四行,写了 __init__ 的多一行构造参数。
推导式(写在表达式里的遍历)
原型画板「遍历 · 推导式」。Python 的列表 / 集合 / 字典推导式、生成器表达式(含 async for、几节 for、if、套着的推导式)不管出现在哪(一行译文、属性表的值 / 初始值 / 默认值、字典项的值),都原地换成一个小框:
- 框:嵌在前后文字里(
display: inline-block),框头是「循环」块的种类图标(KIND_ICONS.loop,luciderepeat,12px)和结果类型,后面是重排后的译文。边框、底色、框头字的颜色都取「循环」块的种类色(--cr-kind-loop:边框 45%、底色 14%,圆角 4px,左右留 5px);框头界面字体 11px、500 字重。上下边框算在 20px 行高里,框和前后文字基线对齐、不把一行撑高;框里套的框行高再减 2px。整个框放不下时挪到下一行,比一行还长时占满一行、在框里换行。 - 结果类型:生成器 / 列表 / 集合 / 字典(
[…]列表、{…}集合、{k: v …}字典、(…)生成器)。 - 框里按读的顺序重排:每节
for写「遍历 <来源> 的每个 <变量>」,紧挨着的几节for用顿号接着写(「遍历 xss 的每个 xs、xs 的每个 x」),async for写「异步遍历」(同步、异步换了就另起一句「,异步遍历 …」),if在它所在的位置写「,仅当 <条件>」(后面再有for就另起一句「,遍历 …」),最后「,取 <值>」,字典是「<键>: <值>」。来源、变量、条件、值照常直译,里面还有推导式就是框里的框。「遍历」「的每个」「仅当」「取」「异步遍历」按关键字着色,逗号、顿号、空格按普通文字。 - 括号:框代替了推导式自己的方括号 / 花括号 / 圆括号,不再写出来;Python 调用唯一的参数是生成器时只写一对括号(
f(x for x in y)),这对括号属于调用,照常写在框外,紧贴着框:"".join(框)。生成器是几个参数之一时(f(a, (x for x in y)))括号是它自己的,不写。 - 框里的名字照样带源码下标:⌘ / Ctrl + 点击跳到定义、单击高亮各处出现都照旧;跳到定义的目标只显示在框里(推导式的变量)时,目标是框所在的块。
- 样例(必须逐字一致;框写成
⟦类型|框里的文字⟧):
| 源码 | 译文 |
|---|---|
return "".join(secrets.choice(ALPHABET) for _ in range(length)) |
返回 "".join(⟦生成器|遍历 range(length) 的每个 _,取 secrets.choice(ALPHABET)⟧) |
return [u.id for u in users if u.is_active] |
返回 ⟦列表|遍历 users 的每个 u,仅当 u.is_active,取 u.id⟧ |
return {u.id: u for u in users} |
返回 ⟦字典|遍历 users 的每个 u,取 u.id: u⟧ |
- TypeScript 没有这种写法,不涉及。几种读不成遍历的写法(语法树认、Python 3 不认的
for x in a, b,写到一半缺了变量 / 来源 / 条件 / 值的)照常逐词直译。
调用与构造
原型画板「调用 · 构造」(code.js 的「调用与构造」一节,嵌套见 nestedArgRow、NESTED_CALL_SAMPLE)。实参多的调用一般跨几行写,挤成一行译文读不下去;把实参摊成一张表,实参的值又是这样的调用时在格子里接着摊。
- 哪些调用:实参列表(括号连同里面,
CallFacts.argumentList)跨了几行的调用,而且整个落在要翻的这一段里(块头部;TypeScriptdescribe('x', () => { … })这种主体是子块的调用不算)。一行写完的调用照旧一行译文,不问定义。链式调用跨了几行、每段的括号都写在一行里的(items\n .filter(…)\n .map(…))也不算;式子跨了几行、里面的调用各自写在一行里的(bool(x)\n and Path(x).is_file())是式子,也不算。 - 一段排一个:一行译文的块、常量 / 变量的值、实参表里每个值各是一段,各自排自己里面遇到的第一个(先外后里)这样的调用;同一段里后面再有的照常一行译文(挤成一行时圆括号贴着里面的内容:
f(\n x\n)写成f(x),方括号、花括号照旧空一格)。被调用的表达式里的调用(get_driver(\n m\n).query(\n …\n)排的是外面的 query)照常一行。 - 一行:
[等待 ]新建 / 调用 被调用的名字 · N 个参数。「等待」就是await照常的译法(在调用前面),「新建」「调用」按声明关键字着色;被调用的名字照常翻、带源码下标(⌘ / Ctrl + 点击、单击高亮、中文名都照旧),前面几节(对象、模块)按变量着色,最后那个名字新建时按类型着色、调用时按函数着色;「· N 个参数」按说明词着色,N 数的是表里的行。 - 埋在式子里:调用后面还紧跟着同一个式子的别的部分(
.model_dump()、[0]、or x;后面是右括号、逗号、分号、冒号或什么都没有的不算),这一行整个括起来:「(新建 X · 3 个参数).model_dump()」「(调用 make · 1 个参数) 或者 fallback」,表照样排在这一段下面。前面有东西的(fallback or make(\n…\n))不括。 - 新建还是调用:写法说明了的看写法(
CallFacts.construct:TypeScript 的new X(…)是新建,不带 new 的是调用,都不问定义);Python 新建对象和调用函数写法一样,翻译插件问SourceDocument.calleeKind:名字定义成类(含异常、枚举)是「新建」,函数、别的、查不到、还没查到都是「调用」。两种语言都用「新建」这个词。 - 查定义(宿主,
calleeKinds.ts+editor/definitions.ts的definitionKind):排视图数据是同步的,先按已经查到的排(第一次都按调用画),记下问了哪些调用;发出预览后问语言扩展被调用的名字(CallFacts.name)定义在哪(和 ⌘ + 点击同一套),打开定义所在的文件、用它那种语言的语言插件分块(readDefinitionKind:自己的名字盖住定义处的最里层那块),几处定义取第一处认得出的。查到的和已知的不一样就重排、重发。按被调用的表达式原文记(user_service.update_user),同一份文档里写法一样的按一处查;文档改了先用上一版查到的,每个版本查一次,语言扩展还没起来(没答)的照 RETRY_DELAYS 隔一会儿再查(和库规则查名字的定义同一套DeferredAnswers)。被调用的不是名字(handlers[k](…))的不查,按调用算。 - 表:两列「参数 | 值」,每个实参一行、照源码顺序。按名字传的(Python
name=value)写名字,按变量着色、带源码下标;按位置传的写「第 N 个」(说明词,只数按位置传的);*args、...args写「展开」,**kwargs写「展开关键字」(说明词)。值照常一行译文(名字带下标);唯一的实参是生成器时(sum(\n x for x in xs\n)),共用的那对括号属于调用,格子里只有框。表的样子同输入参数表(PropertyTable:表头、圆角外框、格子之间分隔线,第一列按内容定宽)。 - 嵌套:值(一段)里有跨几行写的调用时,值那一格写它的一行,它的实参表排在同一格里、文字下面(
PropertyItem.table),一层层往下套,多深都一样;套着的表不能折叠,总是展开。每一层都和最外层一样:先按「调用」画、查到定义再刷新成「新建」,名字带源码下标,跳到定义、高亮各处出现、中文名照旧。排版:每套一层往右缩 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,只多认几种块的种类。 - 块(图标、颜色见「预览(代码块树)」「颜色」):
- 解释器(interpreter,属性表):首行
#!。「解释器」写程序名(bash),「怎么找」写按 PATH 去找(/usr/bin/env bash),写死路径的写写死的路径(/bin/sh -e)。 - 选项(options,属性表):一条
set -euo pipefail一块,每个开关一行:行名是开关的意思(出错就停、用到没设的变量就报错、管道里任一步失败都算失败、打印执行的每条命令……),值是「是 / 否」,后面淡淡写源码里的写法(-e、-o pipefail、+x)。set -- a b这种不是开关,照常是命令。 - 变量、常量(variable / const,属性表,同别的语言的变量块):赋值和
local/export/readonly/declare;local url="$1" sha="$2" out一句几个是一块,没给值的写「无」。常量是readonly的,和脚本顶层名字全大写、值里没有展开的。 - 分情况(case):每个情况都短(最多 3 句、每句一行写完、没有注释和嵌套的语句块)时整条一块,一行「按 x 分情况」,下面一张表「情况 | 做什么」,做的几句在格子里一句一行;否则头部是「按 x 分情况」,每个情况一块子块「情况:以 -apple-darwin 结尾」,要做的各句是它的子块。
- 函数(function,属性表):紧贴在上面的注释(中间没空行)是「说明」,不单独成块。shell 的函数没有参数表,参数按位置取:「输入参数」写「N 个,按位置」,表「位置 | 函数里叫」,函数里用
local x="$N"(或x=$N)起了名字的写名字,没起的写$N。「输出」是函数自己打印到输出的那几句(echo/printf,重定向到别处的不算;进 if、循环、分情况,不进$(…)、管道、嵌套的函数),同一文件里有$(函数名 …)时后面淡淡写「(调用处用 $(fetch …) 拿到)」。「返回」:写了return N的写那几个值,什么都不打印、也没写 return 的写「最后一句成不成功」。主体各句是子块,同别的语言的函数,默认收起。 - 命令(command):一条命令一行,由命令规则读(「删除 dest(连同里面的东西、不提示)」「下载 url 到 out.part(失败就报错、跟着跳转走、最多重试 3 次)」);没有规则的命令照抄命令名,后面的词照常翻(引号去掉,展开读成中文)。重定向统一读(不归命令规则):
>&2→「(打印到错误输出)」,2>/dev/null→「(丢掉错误输出)」,> f→「(写到 f)」,>> f→「(追加到 f)」,2>&1→「(错误输出并进输出)」,< f→「(从 f 读)」。用&&连起来的几条命令读成「a 然后 b」,a || b读成「a,失败就 b」。tree-sitter-bash 把a && b > f读成整串带着重定向,其实重定向只属于 b:语言插件把它算在 b 上(「a 然后 b(写到 f)」)。 - 管道(pipeline):
a | b | c读成「a → b → c」;最后一段是循环(find … | while read -r link; do … done)时读成「找出 dest 下面所有符号链接 → 逐行读进 link」,循环的主体是子块。 - 条件、遍历(if / else / loop):
if/elif/else同 Python(elif/else展平成兄弟块,读成「否则如果」「否则」)。条件:[ -f x ]→ 文件 x 存在,-d目录 x 存在,-e存在,-L是符号链接,-z/-n为空 / 不为空,-r/-w/-x可读 / 可写 / 可执行,-eq/-ne/-lt/-gt/-le/-ge等于 / 不等于 / 小于 / 大于 / 不大于 / 不小于;=/==/!=等于 / 不等于,[[ … ]]里右边是通配时读成「以 .sh 结尾」「不以 .sh 结尾」;!→ 不,&&/||→ 并且 / 或者。一句[ … ](或一串全是条件)自己就是一句语句时是条件块,读成「成立:…」;条件后面跟一句的([ -f x ] && rm x、cond || continue)读成「如果 文件 x 存在,删除 x」「如果不是〔cond〕,跳过这一个」。遍历:for f in "$gc"/git-*→ 遍历 gc/git-* 匹配到的每个文件 f,for x in a b→ 遍历 a、b 中的每个 x,for a in "$@"→ 遍历 全部参数 的每个 a;while cond→ 只要 cond 就一直重复,until cond→ 直到 cond 之前一直重复,while read -r x→ 逐行读进 x。
- 解释器(interpreter,属性表):首行
- 值和展开(一行译文、变量的值、表格里都一样):
$1→ 第 1 个参数,$0→ 脚本路径,$@/$*→ 全部参数,$#→ 参数个数,$?→ 上一条命令的退出码,$$→ 当前进程号;${x:-d}→ x(没设就用 d),第几个参数写「没给」:${1:-}→ 第 1 个参数(没给就是空);${x:=d}→ x(没设就设成 d),${x:?m}→ x(没设就报错:m),${x:+y}→ y(x 设了才有),${#x}→ x 的长度,${x#p}/${x##p}→ x(去掉开头的 / 开头最长的 p),${x%s}/${x%%s}→ x(去掉结尾的 / 结尾最长的 s),${x/a/b}→ x(其中第一个 a 换成 b),${x//a/b}→ x(其中 a 全换成 b),${x:1:2}→ x(从第 1 个字符起,取 2 个)。整个值是$(cmd)时读成「执行〔…〕的输出」,写在别的字中间的读成「〔…〕」;命令规则读出来是一个东西的直接写它:$(dirname "$0")→ 脚本所在目录。双引号字符串去掉引号,写死的部分按字符串色,展开的变量按变量色;全大写的名字(PY_VERSION、HOME)按常量色。 - 通配(分情况的情况、
[[ … ]]的右边):*.zip→ 以 .zip 结尾,*-x→ 以 -x 结尾,x*→ 以 x 开头,*x*→ 含有 x,别的写「匹配 …」;a | b→ a 或 b;分情况里单独一个*→ 其他。 - 照旧能用的:按种类筛选、单击名字高亮各处出现、⌘ / Ctrl + 点击跳到定义、中文名(行尾
# @Comment 安装目录)、文件注释、问 AI。VS Code 自己没有 shell 的定义、高亮提供者,所以同一文件里的名字由语言插件认(LanguageAdapter.symbols):函数的定义是函数名,用到的是同名的命令;变量的定义是赋值、local/export声明、for的变量、read读进的名字,用到的是$x、${x…};第几个参数、$?这类不算。跳到用到的那处之前最近的一处定义(同一个变量前后赋过几次值),前面没有就跳到第一处;高亮时定义处算写入、别处算读取。装了给 shell 提供定义、高亮的扩展时,先问它,它答不上才用语言插件认的。 - 样例(
fixtures/fetchRuntime.sh,translators/bashZh.test.ts的FETCH_READING逐行核对):
| 源码 | 预览 |
|---|---|
#!/usr/bin/env bash |
解释器 bash / 怎么找 按 PATH 去找(/usr/bin/env bash) |
set -euo pipefail |
出错就停 是 -e / 用到没设的变量就报错 是 -u / 管道里任一步失败都算失败 是 -o pipefail |
target="${1:-}" |
名称 target / 初始值 第 1 个参数(没给就是空) |
here="$(cd "$(dirname "$0")/.." && pwd)" |
名称 here / 初始值 执行 〔进入 脚本所在目录/.. 然后 打印当前目录〕 的输出 |
cache="${XDG_CACHE_HOME:-$HOME/.cache}/luffy-desktop-runtime" |
名称 cache / 初始值 XDG_CACHE_HOME(没设就用 HOME/.cache)/luffy-desktop-runtime |
PY_RELEASE=20260924 |
常量:名称 PY_RELEASE / 值 20260924 |
case "$target" in x86_64-pc-windows-msvc) … ;; *) echo "用法: …" >&2; exit 2 ;; esac |
按 target 分情况,表「情况 | 做什么」:x86_64-pc-windows-msvc | GIT_URL = …⏎GIT_SHA256 = …⏎PY_SHA256 = …、其他 | 打印 "用法: 脚本路径 <…>"(打印到错误输出)⏎退出,退出码 2 |
sha_ok() { [ -f "$1" ] && [ "$(shasum -a 256 "$1" | cut -d' ' -f1)" = "$2" ]; } |
名称 sha_ok / 输入参数 2 个,按位置:第 1 个 | $1、第 2 个 | $2 / 返回 最后一句成不成功;子块「成立:文件 第 1 个参数 存在 并且 〔算 第 1 个参数 的 sha256 → 按空格切,取第 1 段〕 等于 第 2 个参数」 |
# 下载 …⏎fetch() { local url="$1" sha="$2" out … echo "$out"; } |
名称 fetch / 说明 下载 (已缓存且校验通过就不重下), 回显本地文件路径 / 输入参数 2 个,按位置:第 1 个 | url、第 2 个 | sha / 输出 打印 out(调用处用 $(fetch …) 拿到) |
out="$cache/$(basename "${url//%2B/+}")" |
名称 out / 初始值 cache/〔取文件名 url(其中 %2B 全换成 +)〕 |
if ! sha_ok "$out" "$sha"; then |
如果 不 sha_ok out sha |
curl -fL --retry 3 -o "$out.part" "$url" |
下载 url 到 out.part(失败就报错、跟着跳转走、最多重试 3 次) |
case "$git_pkg" in *.zip) unzip -q … ;; *) tar -xzf … ;; esac |
按 git_pkg 分情况:以 .zip 结尾 | 解压 zip git_pkg 到 dest/git(不打印过程)、其他 | 解压 tar.gz git_pkg 到 dest/git |
*-apple-darwin)(情况里有注释、好几句) |
子块「情况:以 -apple-darwin 结尾」 |
find "$gc" -mindepth 1 -maxdepth 1 -type d ! -name mergetools -exec rm -rf {} + |
找出 gc 下一层的目录,除了 mergetools,逐个 删除(连同里面的东西、不提示) |
for f in "$gc"/git-*; do |
遍历 gc/git-* 匹配到的每个文件 f |
[ -L "$f" ] && [ "$(readlink "$f")" = git ] || continue |
如果不是 〔f 是符号链接 并且 〔读出 f 指向的路径〕 等于 git〕,跳过这一个 |
find "$dest" -type l | while read -r link; do |
找出 dest 下面所有符号链接 → 逐行读进 link(循环主体是子块) |
du -sh "$dest/git" "$dest/python" >&2 |
统计占用空间 dest/git、dest/python(按人读的单位、只看总数)(打印到错误输出) |
- 和原型不一样的地方(插件生成的,比手写的更一致):函数里的变量也是属性表块(同别的语言,不写成一行「局部 …」);
$N一律写「第 N 个参数」;选项的意思之间一律用顿号(「连同里面的东西、不提示」);[ "$(readlink "$f")" = git ]照结构读成「〔读出 f 指向的路径〕 等于 git」,不合写成「f 指向 git」。
Dockerfile
原型画板「Dockerfile」(code.js 的「Dockerfile」一节:DOCKER_KINDS、NEXT_DOCKERFILE、BACKEND_DOCKERFILE;README「Dockerfile」)。样例是 luffy 的 frontend/client-nextjs/Dockerfile 和 backend/docker/Dockerfile.backend(复制成 fixtures/clientNextjs.Dockerfile、fixtures/backend.Dockerfile);原型里的中文是手写的目标,下面的译文都由插件生成,逐行核对见 translators/dockerfileZh.test.ts 的 NEXT_READING、BACKEND_READING,原型略去的几段也照样读。
- 哪些文件:VS Code 认成
dockerfile的文件:Dockerfile、Dockerfile.*、*.dockerfile、Containerfile。语法包是 tree-sitter-dockerfile,@vscode/tree-sitter-wasm里没有,取自 npm 包tree-sitter-wasm(预编译、每个 wasm 都签过名、上游提交钉死),打包时和别的语法包一起复制进dist/tree-sitter/(见「解析运行时」)。 - 分工同别的语言:语言插件(
languages/dockerfile.ts)报块和 facts;翻译插件(translators/dockerfileZh.ts)定用词;RUN、shell 写法的 CMD / ENTRYPOINT、HEALTHCHECK 的检查命令是嵌着的 shell(EmbeddedCode),宿主交给 shell 的语言插件、翻译插件和命令规则读(见下「嵌着的代码」),Dockerfile 这边不再读一遍 shell;数组写法的命令(["node", "server.js"])不经过 shell,由 Dockerfile 语言插件报成命令(LanguageAdapter.command),同样交给命令规则。webview 不认识 Dockerfile,只多认几种块的种类。 - 块(图标、颜色见「预览(代码块树)」「颜色」):
- 解析指令(directive,属性表):文件最开头连着的
# syntax=…、# escape=…、# check=…,一条一行:「语法 docker/dockerfile:1.7(BuildKit 按这个版本读这份文件)」「转义字符 …」「检查 …」。别的注释、空行、指令之后的不算。 - 阶段(stage,属性表):每个 FROM 开一个阶段,到下一个 FROM 之前的指令、注释都是它的子块;第一个 FROM 之前的(全局的 ARG)照常是顶层的块。卡片:名称(
AS后面的名字;没起名的写「没起名,按序号 N 引用」)/ 说明(紧贴在 FROM 上面、中间没空行的几行注释,算在阶段的头部里)/ 基础镜像(「node,标签 22-alpine」,写了摘要的加「,摘要 sha256:…」,接着前面某个阶段建的写「阶段 builder」,写成构建参数的读成「BASE(构建参数)」)/ 平台(写了--platform才有)。阶段名能点:COPY --from=deps、RUN --mount=…,from=deps、FROM deps里的 deps ⌘ / Ctrl + 点击跳到那个阶段,单击高亮各处出现。阶段默认展开。 - 执行(run):RUN 里的 shell 由 shell 的语言插件分步(
LanguageAdapter.steps):只有一条命令时一行,就是那条命令读成的(「pnpm 装依赖(严格按 lock 文件、不跑安装脚本)」),一条循环、条件的子块归这个 RUN;只用&&连着的一串拆开,一条一步,头一行写「执行」,3 条以上加「(N 条命令,一条失败就停)」(原型 2 条时只写「执行」,10 条时带条数);分几句写的(heredoc 里一行一句)是「(N 条命令)」。续行的\去掉、各段拼起来读(RUN apt-get install -y \⏎ curl git);RUN <<EOT的 heredoc 正文当一段脚本读;管道照 Shell 写「a → b」;a && echo x > f && b这种整串带重定向的(tree-sitter-bash 这样读),重定向只算在 echo 那一步上。RUN 的选项写在括号里:--mount=type=cache,target=X→ 缓存目录 X 跨构建保留,type=secret,id=X→ 临时挂上密钥 X,type=ssh→ 转发 SSH 密钥,type=tmpfs,target=X→ X 用内存临时目录,type=bind(默认)→ 临时挂上 [阶段 F 的] 源 到 目标,--network=none / host / default→ 不联网 / 用宿主机的网络 / 用默认网络,--security=insecure / sandbox→ 不隔离权限 / 隔离权限,别的照抄。 - 复制(copy):「[从阶段 X | 从镜像 Y] 复制 源 → 目标(属主 u:g)」;源是
.写「整个构建目录」,目标是.写「./」;--chmod→ 权限,--parents→ 保留目录层级,--exclude→ 跳过 X,--link→ 单独成层。ADD:源全是网址写「下载」,全是本地的压缩包(.tar、.tar.gz、.tgz……,ADD 会自动解开)写「解压」,git 仓库写「拉取仓库」,--checksum→ 校验;COPY <<EOF /f写「写入 /f(内容写在下面)」。 - 变量(variable,属性表):ARG 是「构建参数 X / 默认值 …」,没写默认值的写「没写,构建时传」;ENV 是「环境变量 X / 值 …」。值里的
$X、${X}读成变量(带源码下标,全大写的按常量色),在它之前(同一阶段里,FROM 那一行看文件开头的)用 ARG 声明过的后面注「(构建参数)」,BuildKit 自动给的(TARGETPLATFORM、TARGETARCH……)注它的意思,${X:-d}注「没设就用 d」,${X:+d}注「设了就是 d」。 - 并块:连着写的同种指令(紧挨着下一行,中间没空行、没注释、没别的指令)并成一块。ENV、ARG 并成一个变量块:一行「环境变量 4 条连着写」(一条 ENV 写了几个的是「3 个」),下面一张「名称 | 值」(构建参数是「名称 | 默认值」)表,每行讲的定义是那个变量;COPY 从同一个地方复制的(都从构建目录,或
--from一样)并成一行「复制(N 条连着写,属主都是 u:g)」,下面一张「从 | 源 | 到」表(都从构建目录复制的没有「从」这一列,从阶段写「阶段 X」、从镜像写「镜像 名字:标签」),属主都一样才写在头一行,别的选项写在「到」那一格里;LABEL 并成一张「名称 | 值」表。ADD 不并(各条做的事不一样),带 heredoc 的 COPY 不并。 - 设置(setting):WORKDIR「工作目录 /app」,USER「之后以 nextjs 身份运行」(
u:g照写),EXPOSE「开放端口 80(udp)、443」,VOLUME「声明数据卷 /data、/logs」,STOPSIGNAL「停止容器时发 SIGTERM 信号」,SHELL「之后的 RUN、CMD、ENTRYPOINT 用 /bin/bash -o pipefail -c 执行」,MAINTAINER「维护者 …」,各一行。ONBUILD 是一行「以它为基础镜像构建时再执行」,包着的那条指令是它的子块。 - 标签(label):「标签」,下面一张「名称 | 值」表(一条 LABEL 写几个、连着写的几条都在表里)。
- 健康检查(health,属性表):间隔 / 超时 / 启动后宽限 / 宽限期里的间隔(时长写成「1 分 30 秒」「500 毫秒」)/ 失败几次算不健康(「3 次」),写了才有;检查命令照 Shell 读(shell 写法)或交给命令规则(数组写法)。
HEALTHCHECK NONE只有一行「检查 不做健康检查(基础镜像里设的也关掉)」。 - 启动(start,属性表):CMD 是「命令」,ENTRYPOINT 是「入口命令」,读法同检查命令;「写法」是「数组,不经过 shell」或「经过 shell」。
- 解析指令(directive,属性表):文件最开头连着的
- 选项多的命令(
uv sync带一长串选项):命令规则认出的选项有 4 个以上(同一个选项写几遍算一个),或同一个选项写了几遍时,这一行后面写「· N 个选项」,意思排成一张「选项 | 意思」表,写了几遍的并成一行、选项后面写「× 11」(--no-install-package × 11 | 不装这些包:agent-dsl、…);3 个以内、都只写一遍的照旧写在括号里。表的样子同调用的实参表(见「调用与构造」)。 - 照旧能用的:按种类筛选、单击名字高亮各处出现、⌘ / Ctrl + 点击跳到定义(阶段名、ARG / ENV 的变量,同一文件里由语言插件认,同 Shell;RUN 里 shell 自己的变量由 shell 的语言插件认)、中文名(紧挨在 ARG、ENV(一个变量的)、
FROM … AS上面一行、只写了# @Comment 名字的注释,这行注释算在那条指令的块里、不单独成块)、文件注释、问 AI。 - 和原型不一样的地方(插件生成的,比手写的更一致):
- 并成一块的 ENV 是属性表的一行「环境变量 4 条连着写」加表,不写成一行字「环境变量(4 条连着写)」。它还是变量块,每行能跳到定义。
uv sync是一行「uv 按 lock 同步依赖 · 4 个选项」加表,不写成「命令 / 选项」两行(同调用的「调用 X · N 个参数」)。curl没写-o时写「请求 url」(原型的 RUN 里写「下载」);. /etc/os-release写「读入 … 里的变量和函数」;echo "…" > f写「打印 "…"(写到 f)」(原型「写入 … 到 f(覆盖)」,重定向统一由翻译插件接上);echo 里的[signed-by=…]照源码写全。- 检查命令
curl -f … || exit 1照 Shell 读成「请求 …(失败就报错),失败就 退出,退出码 1」(原型「或者」)。 - 原型第 12、13 行(
COPY --from=deps …、COPY . .)分开画,所以 COPY 只在从同一个地方复制时才并。 - 后端文件开头三行注释紧贴着 FROM,读成这个阶段的「说明」(原型没画这三行)。
- RUN 拆开的各步是 shell 的块,种类照 shell 的(命令、管道、变量……),不都叫「执行」。
- 嵌着的代码(通用的做法,见「通用契约」的
EmbeddedCode):语言插件报出这份源码里嵌着的别的语言的代码(LanguageAdapter.embedded:装着它的节点、语言、源码里属于它的几段,去掉了外层的记号)。宿主按语言找注册的语言插件、选中的翻译插件和命令规则,只解析这几段(includedRanges,节点下标照旧是整份源码的),按LanguageAdapter.steps分步:只有一步时这一步的子块归嵌着它的块,几步时各步是它的子块,都用那种语言的翻译插件翻、那种语言的词性着色(外层的源码着色上面叠一层);翻译插件翻嵌着它的块、属性表的行时问SourceDocument.embedded,拿到各步读成的一行,连词、「N 条命令」由它自己定。那种语言没注册时照抄、没有子块。同一文件里的名字把嵌着的代码里认出的一并交给宿主。 - 样例(
fixtures/clientNextjs.Dockerfile、fixtures/backend.Dockerfile):
| 源码 | 预览 |
|---|---|
# syntax=docker/dockerfile:1.7 |
语法 docker/dockerfile:1.7(BuildKit 按这个版本读这份文件) |
FROM node:22-alpine AS deps |
名称 deps / 基础镜像 node,标签 22-alpine |
RUN corepack enable && corepack prepare pnpm@latest --activate |
执行,子块「开启 corepack」「准备 pnpm latest(并设为当前用的版本)」 |
COPY package.json pnpm-lock.yaml ./ |
复制 package.json、pnpm-lock.yaml → ./ |
RUN pnpm install --frozen-lockfile --ignore-scripts |
pnpm 装依赖(严格按 lock 文件、不跑安装脚本) |
COPY --from=deps /app/node_modules ./node_modules |
从阶段 deps 复制 /app/node_modules → ./node_modules |
COPY . . |
复制 整个构建目录 → ./ |
ARG DEPLOYMENT_ID ⏎ ENV NEXT_DEPLOYMENT_ID=$DEPLOYMENT_ID |
构建参数 DEPLOYMENT_ID / 默认值 没写,构建时传;环境变量 NEXT_DEPLOYMENT_ID / 值 DEPLOYMENT_ID(构建参数) |
RUN pnpm build |
pnpm 构建 |
ENV NODE_ENV=production ⏎ …(4 条) |
环境变量 4 条连着写,表 NODE_ENV | production、NEXT_TELEMETRY_DISABLED | 1、PORT | 3000、HOSTNAME | 0.0.0.0 |
RUN addgroup --system --gid 1001 nodejs && \ ⏎ adduser --system --uid 1001 nextjs |
执行,子块「新建系统用户组 nodejs(gid 1001)」「新建系统用户 nextjs(uid 1001)」 |
COPY --from=builder --chown=nextjs:nodejs …(2 条) |
复制(2 条连着写,属主都是 nextjs:nodejs),表 阶段 builder | /app/.next/standalone | ./ |
USER nextjs、EXPOSE 3000 |
之后以 nextjs 身份运行、开放端口 3000 |
CMD ["node", "server.js"] |
命令 node server.js / 写法 数组,不经过 shell |
LABEL maintainer="Cruldra" \ ⏎ description="…" |
标签,表 maintainer | Cruldra、description | luffy-agent-platform - … |
RUN apt-get update && \ ⏎ …(10 条) |
执行(10 条命令,一条失败就停),子块「更新软件包列表」「安装 ca-certificates、curl、gnupg、postgresql-client、git(不装推荐的包、不用确认)」「建目录 /usr/share/keyrings」「请求 https://…/ACCC4CF8.asc(不打印进度、失败就报错、跟着跳转走) → 转成二进制密钥 写到 /usr/share/keyrings/postgresql.gpg」「读入 /etc/os-release 里的变量和函数」「打印 "deb …"(写到 /etc/apt/sources.list.d/pgdg.list)」…「打印 pg_repack 的版本」「删除 /var/lib/apt/lists/*(连同里面的东西、不提示)」 |
COPY --from=node:22-slim …(2 条) |
复制(2 条连着写),表 镜像 node:22-slim | /usr/local/bin/node | /usr/local/bin/node |
COPY --chown=appuser:appuser …(12 条) |
复制(12 条连着写,属主都是 appuser:appuser),表「源 | 到」12 行 |
RUN uv sync --frozen --no-dev --no-install-project \ ⏎ --no-install-package agent-dsl \ …(11 个) |
uv 按 lock 同步依赖 · 4 个选项,表 --frozen | 严格按 lock 文件,不改它、--no-dev | 不装开发依赖、--no-install-project | 不装本项目、--no-install-package × 11 | 不装这些包:agent-dsl、… |
HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \ ⏎ CMD curl -f http://localhost:8000/health || exit 1 |
间隔 30 秒 / 超时 5 秒 / 启动后宽限 10 秒 / 失败几次算不健康 3 次 / 检查命令 请求 http://localhost:8000/health(失败就报错),失败就 退出,退出码 1 |
CMD ["uv", "run", "uvicorn", "luffy_agent.main:app", "--host", "0.0.0.0", "--port", "8000"] |
命令 用 uv 运行 uvicorn luffy_agent.main:app,监听 0.0.0.0:8000 / 写法 数组,不经过 shell |
注释的 Markdown 排版
- 用在:注释块(
#、//、/* */、不挂在函数、类上的 JSDoc、Python 模块开头的 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:、相对路径)照原文显示。点链接由 VS Code 的 webview 自己拦下、交给宿主在外部打开,不用另发消息。图片不渲染(webview 的 CSP 不放行外部图片),显示成!加链接。 - 样子:正文用界面字体(
--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为假(默认)时先把当前预览标签固定住,免得被目标替掉。
- 闪哪一处:页面上就是这处定义的最具体的那一处,只按源码位置判断(定义提供者给的定义处下标,对上块和属性行带的源码范围),不看属性名文字:
- 定义是块自己的名字(函数名、类名;只声明一个变量时的变量名,解构模式整个算)→ 整块。块的这段范围由扩展端从 facts 取(
BlockView.defines),和翻译插件无关。 - 定义是属性表里某一行 / 子行讲的东西(参数、导入项、一条语句声明几个变量时的某一个、类的泛型参数、对象字面量一项的键——TypeScript 对象的属性名就是定义,嵌套几层都算)→ 只闪这一行 / 子行。范围是翻译插件从 facts 取的节点(
PropertyRow.defines、PropertyItem.defines;字典 / 对象的项标的是键,展开项不标),定义处落在范围里就算(*args的定义在args上、解构参数里的名字都对得上)。 - 其余(定义在一行译文里,例如循环变量;翻译插件没填
defines)→ 显示着定义处那个名字的块,名字在属性行的子行里时展开那一行(在嵌套的子行里时连它上面各层一起展开);页面上找不到这个名字就取包含定义那一行的块。
- 同一文件原地跳、跳进另一个文件的预览,规则一样。
- 定义是块自己的名字(函数名、类名;只声明一个变量时的变量名,解构模式整个算)→ 整块。块的这段范围由扩展端从 facts 取(
- 滚过去:目标所在块的祖先块折叠着就展开,目标在折叠的属性行子行里(参数、导入项、字典 / 对象的项)就展开那一行,在嵌套的子行里就连它上面折叠着的各层子行一起展开。目标已在视口里不滚,否则滚到视口正中(比视口高就顶部对齐)。然后目标的边框和底色往聚焦色(
--vscode-focusBorder)闪三下,共约 1.5 秒后复原;同一处再跳一次从头闪;系统开了「减少动态效果」时改成静止高亮 1.5 秒。目标是属性行 / 子行但块正在编辑(页面上没有属性表)时闪整块。 - VS Code 里没有这种语言的定义提供者(shell):语言扩展答不上时,同一文件里的名字由语言插件认(
LanguageAdapter.symbols,见「Shell 脚本」),跳到用到的那处之前最近的定义。 - 不按修饰键单击名字是高亮它的各处出现(见下一节);⌘ / Ctrl + 点击名字不会触发块的编辑切换、折叠,也不动高亮。
高亮名字的各处出现
- 能点的名字和「跳到定义」一样:带源码下标的段(属性表、一行译文、字典 / 对象各层子行的键和值,点分模块名每一节各算各的)。「编辑」状态的块里的源码、Markdown 里的名字不算,既不能点也不被高亮。
- 单击名字:问扩展端(
vscode.executeDocumentHighlights,文档 uri + 名字在源码里的位置),由语言扩展(Pylance、TypeScript 语言服务……)按语义答,同名的别的东西(例如同名的参数)不算。页面上源码范围和回答里任何一处重叠的段都高亮,一个名字被拆成几段(luffy_agent.core.db)时每段各按自己的范围认。 - 讲别处名字的段(
Segment.refers):写在字符串里、语言扩展不认得的名字,库规则标出它讲的是源码里哪个名字(FastAPI 路径"/{user_id}"里的user_id讲的是函数的参数user_id)。页面把这段当那个名字:单击问那个名字的各处出现,⌘ / Ctrl + 点击跳到那个名字的定义(参数本身),那个名字的某处出现盖住refers时这段也高亮(单击参数user_id时 URL、路由器接口表里的{user_id}一起亮)。只有一处认这件事:webview 的occurrences.ts的sourceOf(段讲的是源码里哪一段名字:有refers取它,否则是照抄的位置),data-offset、高亮、当前那一处都按它;扩展端不用管。 - 颜色和编辑器里一样:写入(定义、赋值,
DocumentHighlightKind.Write)用--vscode-editor-wordHighlightStrongBackground,读取和按文字认出的用--vscode-editor-wordHighlightBackground;主题给了wordHighlightStrongBorder/wordHighlightBorder(高对比主题)才描边,描边画在字框里面不挤动排版。同一段碰上两种按写入画。 - 很多主题没配单词高亮色(例如 Gerry Oceanic),默认的灰底很淡,所以再用主题的
--vscode-focusBorder补一笔(换什么主题都跟着主题走):当前那一处(点中的、用键盘跳到的)在底色之外描一圈 1px 框;别的各处在底色下面画一条 1px 细线(内阴影,不占text-decoration,⌘ 指着名字时的链接下划线照旧)。写入的那处照样是强高亮底色。框和线都画在字框里面,不挤动排版。 - 看不见的出现(在折叠着的块、属性行、子行里,或块被筛掉)点的时候不自动展开;筛选栏右边、块数前面写「名字 · 2/5」(当前那一处是照源码先后排的第几处 / 回答里共几处,看不见的也算),名字取点中的那一段的文字;点中的名字不在回答里(没有当前那一处)时写「名字 · N 处」。之后自己展开,里面的出现照样亮着。
- 键盘跳:两条命令「代码阅读:跳到下一处高亮」(
superpowers.codeReader.nextOccurrence)、「代码阅读:跳到上一处高亮」(superpowers.codeReader.previousOccurrence),默认键 mac 上 ⌘↓ / ⌘↑、别的系统 Ctrl+↓ / Ctrl+↑,when是activeCustomEditorId == superpowers.codeReader.preview && superpowers.codeReader.hasOccurrences;用户可以在「键盘快捷方式」里改绑,webview 里不写死处理哪个键。命令面板里只在代码阅读预览是活动编辑器时出现。- 上下文键
superpowers.codeReader.hasOccurrences:webview 报「眼下能不能用键盘跳」(有高亮、页面有焦点、焦点不在输入框 / 块的「编辑」里),变了才报;扩展端有一个预览能跳就设成真(同一时刻只有一个页面有焦点,几个预览的消息先后不定也对),页面失去焦点、高亮清掉、页面被销毁(标签页切走)、预览关掉都变假。 - 命令发给报了能跳的那个预览;从命令面板运行时页面没焦点,发给活动的预览。
- 跳:照源码先后,从当前那一处往后 / 往前找下一处页面上找得到的出现(显示着和它重叠的段),到头了从另一头接着找;在被筛掉的、正在编辑的、被正在编辑的祖先盖住的块里的跳过,Markdown 里的不算,筛选栏上的总数照旧。点中的名字不在回答里时从点中的地方数起。当前那一处换成它,展开把它折叠住的块、属性行和子行(同一处在页面上显示几次取页面上的第一处),再把它滚进视口(已经整个看得见就不滚,否则放到视口正中,同「跳到定义」)。
- 快捷键只写在
package.json里,扩展、页面的代码里都不写死哪个键,页面不拦任何键:VS Code 把 webview 里按的每个键都原样转给工作台的快捷键系统(webview 外壳pre/index.html的handleInnerKeydown发did-keydown),焦点在预览里时快捷键照常生效;用户改绑后原来的 ⌘↓ / ⌘↑ 照常是浏览器的滚到底 / 顶。 - 浏览器对触发命令的那个键自己的动作(⌘↓ 滚到底)照常发生,由跳转盖掉(
occurrences.ts的KeyScroll):页面在按下任何键时记下滚在哪;命令发来时,按键后 1 秒内的(KEY_TO_COMMAND_MS)先回到按键那一刻的位置,再把目标滚进视口;按键后 0.5 秒内(KEY_SCROLL_MS)浏览器又滚走了就拉回跳到的位置。按键后点了鼠标、滚了滚轮、又按了键,或从命令面板运行(没有刚按的键),都不回、不守。 - 实测(隔离的真 VS Code,逐帧录屏):命令十几毫秒就到页面,浏览器的滚到底动画再过一帧才开始,而且在合成线程上播、页面忙着重画时照样播;不管的话页面最后停在底部(目标看不见)。只在滚动时拉回,重画期间仍有约 100 毫秒来回跳的画面;滚到原地浏览器当没滚、不打断已排上的动画,所以回位时先挪一个像素再回来(同一段脚本里连着两下,中间不画),动画在开始前就被打断,录屏里一帧都没有被滚走。
- 上下文键
- 和编辑器一样每回点都现问,回答不留到下次用(语言扩展刚启动时答的「没有」或按文字认的结果不会一直留着);只认最近一次询问的回答,迟到的丢掉。点另一个名字:回答到之前旧高亮留着,到了就换成新的;回答是空的(没装语言扩展、没答)就什么都不亮,不按文字自己找。
- 语言扩展答不上(shell 没有高亮提供者)时用语言插件认的同一文件里的名字(
LanguageAdapter.symbols):定义处算写入、别处算读取;中文名落到哪些名字上也走这条路。 - 清掉(连同筛选栏上那句):点空白处或不是名字的字、按 Esc(输入框、下拉列表自己用掉的 Esc 不算)、文档换了版本、换了翻译。
- 不抢别的点击:⌘ / Ctrl(以及 Alt、Shift)+ 点击、双击的第二下、拖着选字都不动高亮(双击的第一下照样算一次单击,和编辑器一样);点在按钮、链接、输入框、下拉列表、筛选栏、属性名上照旧,高亮不动——折叠箭头、「编辑」、Markdown 里的链接都照旧;名字本身在按钮里(带下一层的子行的键)时照旧折叠,同时高亮。
中文名
代码几乎都是英文名字,读的人要一个个在脑子里翻。代码里给定义起了中文名(显示名)的,预览里直接写中文名:if current_user.role != UserRole.ADMIN 读成「如果 当前用户.role 不等于 UserRole.管理员」。
从哪来(语言插件认,契约里只有一种事实:定义处的名字 + 中文名,DisplayName):
| 语言 | 写法 | 能起名的定义 |
|---|---|---|
| Python | 注解 Annotated[T, …, Comment("当前用户")](typing.Annotated 也算;调用的名字最后一节是 Comment 就算,不管从哪导入、是不是本地定义的,参数是普通字符串) |
参数、变量、类字段 |
| Python | 装饰器 @Comment("…")(项目里自己定义的什么都不做的函数) |
函数、类 |
| Python | 定义那一行行尾的注释 # @Comment 当前用户(记号从 @Comment 到行尾,前面可以还有别的字) |
参数(多行参数表里每行一个)、变量、类字段、枚举成员、self.x = … 的 x |
| Python | 兜底:函数 docstring 里这个参数的说明(谷歌写法 Args: 一节的 name (type): 说明,Sphinx 写法 :param name: / :param type name:),取第一小句(到第一个 ,。,.;;:: 为止),去掉首尾空白后不超过 12 个字才算,否则没有中文名 |
参数 |
| TypeScript / JavaScript | 装饰器 @Comment("…")(名字最后一节是 Comment) |
类(@d export class 也算)、方法、字段、方法和构造函数的参数 |
| TypeScript / JavaScript | 定义那一行行尾的 // @Comment 当前用户(块注释不算) |
参数、变量、枚举成员、类字段、接口属性 |
| TypeScript / JavaScript | 兜底:函数的 JSDoc 里 @param [{类型}] 名字 [-] 说明([名字]、[名字=默认] 也认,opts.x 这种属性不认),同上取第一小句、不超过 12 个字 |
参数(函数声明、方法、值是函数的变量和类字段) |
| Shell | 赋值、local / export / readonly 声明那一行行尾的 # @Comment 安装目录 |
变量 |
| Dockerfile | 紧挨在指令上面一行、只写了 # @Comment 部署 ID 的注释(Dockerfile 的注释只能单独成行) |
ARG、ENV(只有一个变量的)、FROM … AS 的阶段名 |
- 行尾记号给那一行第一个能带行尾记号的定义(上表第三行、第六行那些;函数、类用装饰器),单独成行的注释、那一行没有这种定义的不算。
- 同一个定义几处都写了:写在代码里的记号(注解、装饰器)优先,其次行尾记号,最后才是说明文字。
- 行尾记号不是说明文字:语言插件把记号的源码范围也交出来(
DisplayNameFacts.markers,不管这个定义最后取没取它),宿主放进SourceDocument.markers,翻译插件翻注释、说明时把它去掉(# @Comment 管理员这样只有记号的注释就没有说明,枚举成员表里没有这一列)。docstring、JSDoc 照原样显示。
落到哪些名字上(用到它的各处由语言扩展按语义认,同名的别的东西不算;没有语言扩展时一个都不换,和高亮、跳到定义一样只走这一条路):
- 本文件定义的:每个起了中文名的定义在定义处问一次
vscode.executeDocumentHighlights,回答里落在视图数据里带下标的名字(段的下标、长度都对得上)上的都换成中文名。 - 别的文件定义的(
UserRole.ADMIN,ADMIN 在 models.py):宿主给每种语言建一份工作区中文名索引(同库规则的WorkspaceIndex,找文件、排除、嵌套 checkout、忽略文件、盯改动、第一次查才建都是同一套;文件是语言插件给的DisplayNameReader.files,预筛是mentions),键是原名;这份文档里原名在索引里(别的文件)有的名字才问vscode.executeDefinitionProvider,定义处正好是索引里那个定义(同一文件、同一行列)才换。 - 按文档版本查一次,不挡第一次画:文档先发,查到本文件的先发一次,别的文件的查完再发(
displayNames消息);页面重新加载(标签页切回来)时随文档带上已经查到的。语言扩展刚启动时答不上来(定义处自己都查不到出现,或要问的定义一个都没答),隔 1、2、4、8、16 秒再查,还答不上就算了。 - 改文档时不闪:已经查到的中文名跟着改动挪位置(改动在前面的往后挪,碰到的丢掉),随新版本的文档先带过去;新版本查完一起换(手上有挪过来的就不先发只有本文件的那一半)。查到一半文档又改了,查到的丢掉。
怎么显示(webview 不认识任何语言,只按下标和长度换字):
- 预览里所有照抄源码名字的地方(一行译文、属性表、表格、枚举和库规则的卡片、推导式的框):名字写成中文名,悬停提示「原名 current_user」;下标不变,⌘ / Ctrl + 点击跳到定义、单击高亮各处出现、⌘↓ 跳都照旧,筛选栏上的名字写中文名。
- 定义处本身(输入参数表格的名称格子、名称行、枚举成员格子这些):中文名后面淡淡写原名(
descriptionForeground、字小一号,.cr-original),例如「当前用户 current_user」。 - 只换名字那一段:
UserRole.ADMIN读成「UserRole.管理员」,前面的类名照写。 - 「编辑」状态的源码、Markdown 说明里的名字不换。
问 AI
原型画板「问 AI」、组件 Reader / AskPanel(State=history | asking)。读代码时对一块有疑问,直接在块下面问 Claude Code,答完存起来,下次打开还在,理解了就删。
- 入口:块右上角那排图标按钮里的「问 AI」(每种块都有:普通块、属性表块、库规则的卡片)。点一下在块下面展开问答区,再点(或在输入框里按 Esc)收起。文件不在任何工作区文件夹里时问答没地方存,不给这个按钮。
- 问答区紧跟在块下面、子块前面,和块同宽,左边一条 2px 的链接色竖线(
--vscode-textLink-foreground50%)。从上到下:- 灰字的历史:这块以前问过的,每条一行问题(
descriptionForeground,一行放不下截断)、右边时间(「刚刚」「5 分钟前」「3 小时前」「昨天」「3 天前」,一个月以前写日期,悬停看完整时间)跟着用哪个 profile 答的(「3 天前 · sonnet」;选「默认」答的、这功能以前存的只写时间)和垃圾桶「已理解,删除」;问的时候贴了图的,问题下面一排缩略图(见「贴图」,灰字历史里淡一点、悬停变实);下面是回答的前两行(去掉 Markdown 记号的纯文字,更淡)。点问题或这两行展开整段回答,照样灰字,按 Markdown 排(同注释的 Markdown 渲染),再点收起。 - 当前这一轮(展开以来问的,和历史之间一条细分隔线):问题照常颜色、加粗,贴了图的问题下面一排缩略图,回答照常颜色、按 Markdown 排,逐字长出来,末尾一个闪的光标;下面一行灰字写正在做的事(「读取 exceptions.py」「搜索 get_user」,还没开口时「正在想…」)。出错时这一问下面一个红框写原因(找不到 claude、退出码非零带它的原话、没登录时提示在终端里运行 claude 登录、超时);叫停的写「已停止,这次没存。」
- 输入框:没问过时提示「问这块代码… Enter 发送,Esc 收起」,问过了「继续问… Enter 发送,Esc 收起」;Enter 发送,Shift+Enter 换行,输入法选字时的 Enter 不算;Esc 收起(不清掉名字高亮);展开时光标就在输入框里。右边先是 profile 下拉,再是发送按钮(没字时不能点),正在答时换成「停止」。
- 贴图:在输入框里粘贴(⌘V / Ctrl+V)剪贴板里的图片(截图、复制的图片),图片加到输入框上面一排缩略图里,随下一个问题一起发;剪贴板里同时有的文字照常粘进输入框。图片文件拖到输入框上也一样(VS Code 里要按住 Shift 拖,不然它自己接走打开文件)。认 PNG、JPEG、GIF、WebP,每张最多 5 MB,一个问题最多 6 张;不认的、太大的、多出来的不加,缩略图下面一行提示原因(「只能贴 PNG、JPEG、GIF、WebP 图片;每张图片不能超过 5 MB;一个问题最多带 6 张图片,有 N 个没加上。」,警告色 11.5px),下次贴图或发送时清掉。问题还是要有字才能发(只贴图不写字发不出去);发出去后缩略图清空。
- 缩略图:40px 见方、圆角 4px、1px 边框,图按比例裁满;历史和当前这一轮的和回答一样往右缩到问题下面,贴了还没发的和输入框左边对齐、每张右上角一个小圆「✕」(「去掉这张图片」,去掉后光标回到输入框)。点缩略图看大图:盖住整个预览的深色底,图按比例放到放得下为止,右上角「关闭」;打开时焦点在「关闭」上,点哪儿或按 Esc 关上(只关大图,不收起问答区、不清名字高亮),焦点回到点开它的缩略图。缩略图用
data:地址(还没发的、当前这一轮的)或webview.asWebviewUri给的地址(存下的),页面安全策略本来就允许这两种图片,不用改。 - profile 下拉(同筛选栏的下拉
SelectMenu,ghost样子):只写选中的 profile 名和一个小箭头,灰字 12px、高 20px,最宽 140px,长名字截断,输入框还是主角;悬停--vscode-toolbar-hoverBackground,打开时--vscode-toolbar-activeBackground,键盘聚焦描 1px--vscode-focusBorder。点开(或聚焦后按 ↓ / ↑ / Enter / 空格)列出能用的 profile,↑ ↓ 挑、Enter 选、Esc 关;profile 多了弹层顶上有搜索框。悬停提示「用哪个 Claude Code 配置(profile)回答,下一个问题起用它」。 - 选项是插件的 profile 目录(同看板、会话:
cc/profiles的listClaudeProfiles,名字是文件名去掉.json)。插件的默认 profile(getDefaultProfilePath,offical.json)在里面时,没选过就选它;不在(一个 profile 都没有,或没有offical.json)时最前面多一项「默认」:不带--settings,claude 用~/.claude的登录,和以前一样照问,没选过就选它。 - 选了从下一个问题起用它(正在答的不受影响)。所有预览、所有块共用一份,记在扩展的
globalState(superpowers.codeReader.askProfile,记 profile 名,「默认」记'')里:换个预览、重启 VS Code 都还是它;记着的那个没了(删了、目录换了)就回到默认那项。
- 灰字的历史:这块以前问过的,每条一行问题(
- 没展开、但问过的块:块下面只露一行灰字「问过 N 次」,读的时候一眼知道这里问过;点开看灰字历史(能展开、能删),没有输入框。
- 收起就结束这一轮:这一轮答完的都回到灰字历史里(再问照样接着会话,见「追问」);还没答完的接着答,答完照样存。同一块同时只答一个问题(正在答时不能再发),不同块可以同时问,全插件同时最多 3 个,再多就在这一问下面说「同时最多问 3 个问题,等前面的答完再问」。
- 追问:这块存过问答,再问就接着最近一次(按问的时间)存下的那条的会话(
--resume <会话 id>,会话 id 取 stream-json 的 init 消息,和问答一起存在code-qa.json里),只发问题和「还是第几行那块」;收起再展开、预览重新加载、重启 VS Code 都一样接得上,前面问过的照旧灰字显示。换了 profile 也照样接着那次会话问(--resume和这次选的--settings一起带,本机 claude 2.1 试过:换 profile 后会话 id 不变、记得前面说过的),线程里不另外提示。那条没记会话 id、或会话找不到了(过期、被清理)就按第一次那样带上代码上下文重新开会话。删掉最近那条,就接着剩下里最近的;一块下面删光了就是没问过。 - 删除:点垃圾桶马上删,不用确认;一块下面删光了这块就不挂了,一份文件删光了文件里就没有这份文件。那条带的图片文件一起删。
- 找不到原来位置的问答:记号(见下)对不上这份文件里任何一块的问答不显示,也不动它,留在
code-qa.json里;代码改回来、记号又对上了,就又挂回那块下面。 - 预览重新加载(标签页切走再切回来)后,扩展端还在答的问题接上(展开它那块、接着长),答完照样显示。同一份文件开在几处,问答、正在答的都一样。
怎么问(扩展端 qa/):
- 用 Claude Code 命令行:和插件别处的
claude -p一样找 claude(resolveClaudeBin)、清环境变量(ANTHROPIC_*、嵌套会话标记)、带问的时候选的 profile(--settings <它的路径>,选「默认」不带;每次问都现读 profile 目录,见上面的 profile 下拉),在这份文件所在的工作区文件夹里跑,它能自己读项目里别的文件。 - 只读,靠三层:
--tools Read,Grep,Glob(可用的内置工具只剩读、搜,编辑、写文件、跑命令的工具根本没有),--allowedTools Read,Grep,Glob(这几个不用再批),--permission-mode dontAsk(别的一律拒、不等人批,盖过 profile 里的 bypassPermissions),外加--strict-mcp-config且不给--mcp-config(不加载任何 MCP 服务,免得 MCP 工具能改东西)。不带--dangerously-skip-permissions。 - 问题从 stdin 发:
-p --input-format stream-json,写一条用户消息(一行 JSON,同cc/spawnClaude.ts带图时的buildStreamJsonLine:先是问题的文字,后面按贴的先后跟着各张图片,{ type: 'image', source: { type: 'base64', media_type, data } })后关上 stdin。没贴图也走这一条路(只有文字),问题不上命令行。追问(--resume)和会话找不到时重新问都带上这一问的图片。本机 claude 2.1 试过:带图第一次问、接着会话再带一张图追问,都认得出图、记得上一张;约 6 MB 的 base64 图片照样能答。 - 输出
--output-format stream-json --verbose --include-partial-messages:init 消息给会话 id;逐字的text_delta拼成回答;工具调用换成「读取 X」「搜索 Y」;新的一条回复(读完文件后开口)一开口就换掉前面那句(「我先看看 X」);答完以 result 行的最终回答为准,is_error当失败。超时 5 分钟;「停止」给进程发 SIGTERM,这次不存。 - 第一次问的话:要求用中文、大白话、简洁,只读,提到文件写相对项目根目录的路径(带行号);带上文件的相对路径和语言、外面套着的各层块的开头(每层最多 6 行,带行号,例如类和它里面的函数)、这块代码(块的完整范围,同「编辑」,带行号,超过 120 行只贴开头并说剩下的在哪几行)、问题。追问只发问题和「还是 路径 第 a–b 行那块代码,代码可能刚改过,需要时重新读文件」。这些都只用视图数据的行号、记号和树形,不认识任何语言。
块的位置记号(BlockView.anchor,qa/anchor.ts):
- 扩展端生成视图数据时按源码先后逐块给:有名字的块(
defines的来源:函数名、类名、枚举名、单个变量名,库规则卡片取规则给的定义范围)是外层各个有名字的块加自己的「种类 名字」,例如class ReferralRelation > function display;没名字的块(条件、语句、返回、注释、导入、多变量声明……)是外层最近的有名字的块加「种类 @源码哈希」,哈希是这块头部那几行(库规则认领的块是整条语句)去掉所有空白后的 SHA-1 前 8 位,改它的子块不影响它的记号,例如function display > if @3fa1c2d0。同一层里记号相同的(同名的两个函数、一模一样的两行语句)按源码先后在后面补[1]、[2]。 - 所以:上面插几行、改缩进空白、改别的函数的名字,记号都不变;有名字的块改了主体、签名还认得;没名字的块自己的源码改了,记号就变了,旧问答成了「找不到原来位置的」(不显示)。种类用视图数据的种类(库规则的卡片是规则的种类 id)。
- webview 只拿记号比相等,不拆开看。
存法:
- 每个工作区文件夹一份
<工作区文件夹>/.spx/code-qa.json,和.spx/code-notes.json同一套写法(pathMap.ts、folderStore.ts两者共用):{ "相对路径": [{ "anchor": 记号, "entries": [{ "id", "question", "answer", "createdAt", "sessionId", "profile"?, "images"? }] }] },回答是 Markdown 原文,createdAt是 ISO 时间,profile是用哪个 profile 答的(profile 名;选「默认」答的没有这一项),images是问题带的图片的文件名(见下;没带图的没有这一项);文件缺失或解析失败当作没有,认不出的块、问答跳过;写时按需建.spx,两格缩进、文件按路径排、块按记号排、问答按时间排、末尾换行;存第一条前不生成文件,删光后留{}。 - 问题带的图片(
qa/qaImages.ts):答成了才写,每张一个文件,放在同一个工作区文件夹的.spx/code-qa/里,名字是<问答 id>-<第几张>.<扩展名>(png/jpg/gif/webp),内容是原图;这个目录第一张图存下时才建。没答成(失败、叫停)不留文件;图片写不进去时这一问算失败(「回答没存上:图片写不进 .spx/code-qa(原因)」),不存这条。images只认这个样子的名字,手改成别的路径(../x)的跳过,删的时候也碰不到别处。 - 图片跟着条目走(
PathMapFile.attachments,folderStore.ts每次改表写好后删掉不再有条目带着的文件):删一次问答、删源文件(问答跟着删)时图片一起删;改名、挪位置按问答 id 起名,文件不用动;另一条还带着的不删;外部改了code-qa.json(git pull、手改)不删文件。挪到另一个工作区文件夹时问答跟过去,图片留在原来那个文件夹、跟着删掉,过去的那条缩略图就没了。 - 预览要能读到
.spx/code-qa/:打开预览时把这份文件所在工作区文件夹的这个目录加进 webview 的localResourceRoots,发给预览的问答里图片换成webview.asWebviewUri的地址。 - 在 VS Code 里改名、挪位置、删除文件 / 目录时问答跟着走(同文件注释);外部改了
code-qa.json(git pull、手改)文件监听重读,开着的预览跟着变。
通用契约
放在 vscode/src/codeReader/contract.ts,语法树类型用 @vscode/tree-sitter-wasm 自带的 web-tree-sitter 类型。
export type BlockKind =
| 'import' | 'function' | 'class' | 'enum' | 'exception' | 'type' | 'field' | 'const' | 'variable'
| 'if' | 'else' | 'loop' | 'try' | 'catch' | 'finally' | 'return' | 'statement' | 'comment'
| 'interpreter' | 'options' | 'case' | 'command' | 'pipeline' // shell:解释器、选项(属性表),分情况、命令、管道(一行译文)
| 'directive' | 'stage' | 'run' | 'copy' | 'setting' | 'label' | 'health' | 'start' // Dockerfile:解析指令、阶段、健康检查、启动(属性表),执行、复制、设置、标签(一行译文)
/** 源码里一段的下标(源码字符串下标),end 为开区间。 */
export interface SourceRange { startIndex: number, endIndex: number }
/** 源码里的一段,带起止行号(从 0 开始)。 */
export interface SourceSpan extends SourceRange { startRow: number, endRow: number }
// ── 属性表块的结构化信息(facts):语言插件从语法树读出,节点都指向源码 ──
export type Access = 'public' | 'private' | 'protected'
export interface ParameterFacts {
name: Node // 解构参数是整个模式,...rest / *args / **kwargs 带前缀
optional: boolean // TypeScript 的 x?
type?: Node
default?: Node
}
export interface FunctionFacts {
name?: Node // 匿名函数没有
doc?: Node // 函数的说明:紧贴在前面的 JSDoc 注释节点(TS)、主体开头的 docstring 字符串节点(Python)
access?: Access // 嵌套在语句块里的函数没有
async?: boolean // 没有这个概念的语言不填
decorators: Node[]
parameters: ParameterFacts[]
returnType?: Node
positional?: PositionalParameter[] // 不写参数表、按位置取参数的语言(shell 的 $1):用到的第几个参数,照位置排;填了 parameters 就是空的
prints?: Node[] // 函数打印到输出的那几句(shell 的 echo / printf,没重定向到别处)
captured?: boolean // 函数的输出被调用处拿去用了(shell 同一文件里有 $(函数名 …))
returns?: Node[] // 只有成不成功的语言(shell):写了的 return 语句;空数组是「最后一句成不成功」。别的语言不填
}
/** 按位置取的一个参数:第几个(1 起),函数里给它起的名字(`local url="$1"`)。 */
export interface PositionalParameter { position: number, name?: Node }
/** 第一行 `#!` 写的解释器:程序名(bash),交给 env 按 PATH 找还是写死的路径,`#!` 后面的整句。 */
export interface InterpreterFacts { program: string, lookup: 'env' | 'path', command: string }
/** 打开 / 关掉解释器的一个开关(`set -e`、`set -o pipefail`):id 是语言自己的叫法(bash 的长名字 errexit……),flag 是源码里的写法。 */
export interface SwitchFacts { id: string, on: boolean, flag: string }
export interface OptionsFacts { switches: SwitchFacts[] }
/** 文件开头给构建工具看的解析指令(`# syntax=docker/dockerfile:1.7`),一行一条。 */
export interface ParserDirective { name: string, value: string }
export interface DirectiveFacts { directives: ParserDirective[] }
/** 指令后面的一个选项(`--from=deps`):值在源码里从哪开始(值里的阶段名、变量能跳到定义)。 */
export interface InstructionFlag { node: Node, name: string, value: string, valueStart: number }
/** 一个构建阶段(FROM):阶段名、第几个(没起名的按它引用)、紧贴在上面的注释、镜像名、是不是前面的阶段、标签 / 摘要(带 : / @)、选项。 */
export interface StageFacts { name?: Node, index: number, doc?: Node[], image: Node, stage: boolean, tag?: Node, digest?: Node, flags: InstructionFlag[] }
/** 要执行的命令的两种写法:数组(不经过 shell,node 是整个数组)、交给 shell 的一段嵌着的代码。 */
export type CommandForm = { form: 'exec', node: Node } | { form: 'shell', code: EmbeddedCode }
/** 健康检查:none 是 `HEALTHCHECK NONE`,否则选项和检查命令。 */
export interface HealthFacts { none: boolean, flags: InstructionFlag[], command?: CommandForm }
/** 容器启动时执行的命令:CMD(默认命令)或 ENTRYPOINT(入口)。 */
export interface StartFacts { instruction: 'cmd' | 'entrypoint', command: CommandForm }
/** 写在这门语言里的另一种语言的代码:外层语法树里装着它的节点、语言、源码里属于它的几段(去掉了外层的记号,拼起来是一份那种语言的代码)。 */
export interface EmbeddedCode { node: Node, languageId: string, ranges: SourceRange[] }
/** 嵌着的代码被当成一串步骤执行时的分块:一步一块,chained 表示前一步失败就不往下走(shell 的 &&)。 */
export interface StepBlocks { blocks: CodeBlock[], chained: boolean }
/** 宿主读嵌着的代码读出来的:各步读成的一行(注释不算一步)、是不是一步失败就停、整段是不是就一步(这一步的子块归嵌着它的块)。 */
export interface EmbeddedReading { steps: { text: Inline[], table?: PreviewTable }[], chained: boolean, whole: boolean }
/** 父类的一项:TS `extends Base<T>` 的 Base(表达式)和它后面的泛型实参 <T>;Python 类参数列表的一项(含 metaclass=…、*bases)。 */
export interface BaseFacts { value: Node, typeArguments?: Node }
export interface ClassFacts {
name?: Node // 匿名类没有;赋给变量、类字段的类表达式取变量名、字段名
doc?: Node // 同 FunctionFacts.doc
access?: Access // 嵌套在语句块里的类没有
abstract?: boolean // 只有 TypeScript 填(JavaScript 不填)
decorators: Node[]
typeParameters: Node[] // 每个泛型参数一项
bases: BaseFacts[]
implements: Node[] // TypeScript 的 implements,每个接口一项
}
/** 枚举的一个成员。 */
export interface EnumMemberFacts {
name: Node
value?: Node // 源码里写的值;没写(TS 自增)、交给库自动取(Python auto())时没有
computed?: number // 没写值、语言算得出来时的值(TS 数值枚举自增 0、1、2…)
docs: Node[] // 说明节点,照源码顺序:紧挨在上面单独成行的注释 / JSDoc、这一行行尾的注释、Python 的属性 docstring
}
/** 枚举:TS 的 enum 由语言插件报;Python 自带 enum 库的类由库规则认成这种块(RuleFacts)。 */
export interface EnumFacts {
name: Node
doc?: Node // 同 ClassFacts.doc
access?: Access // 同 ClassFacts.access
decorators: Node[]
bases: BaseFacts[] // 照写的父类(Python 的 str, enum.Enum);TS 没有
members: EnumMemberFacts[]
}
/** 异常类的一个类属性:Python 类主体里给一个名字赋值的语句,TS 的类字段(含 static)。 */
export interface FieldFacts {
name: Node
type?: Node
value?: Node
docs: Node[] // 同 EnumMemberFacts.docs
}
/** 异常类:Python、TS 都由语言插件报(见「异常」)。 */
export interface ExceptionFacts {
name?: Node // 同 ClassFacts.name
doc?: Node // 同 ClassFacts.doc
access?: Access // 同 ClassFacts.access
decorators: Node[]
bases: BaseFacts[] // 同 ClassFacts.bases
fields: FieldFacts[] // 类属性,照源码顺序
parameters?: ParameterFacts[] // 构造函数的参数(Python 的 __init__ 去掉 self,TS 的 constructor);没写构造函数时没有
}
export type ImportItem =
| { kind: 'named', name: Node, alias?: Node } // import { a as b }、from m import a as b
| { kind: 'default', alias: Node } // import x from 'm'
| { kind: 'namespace', alias?: Node } // import * as x、from m import *
| { kind: 'module', module: Node, alias?: Node } // import a.b as c、import x = require('m')
export interface ImportFacts {
sources: Node[] // 来源模块的写法;Python 的 import a, b 有多个
typeOnly?: boolean // 只有 TypeScript 填(JavaScript 不填)
items: ImportItem[]
}
/** 字典 / 对象字面量里的一项,照源码顺序;注释不算。 */
export type EntryFacts =
| { kind: 'pair', key: Node, value: Node, 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<string, string>
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<B, 'kind' | 'facts'> : never : never
export type TextKind = Exclude<BlockKind, FactBlock['kind']>
/** 预览成一行译文的块(注释块是 Markdown 说明)。 */
export interface TextBlock extends BlockShape {
kind: TextKind
doc?: Node // 同 VariableFacts.doc:Python 字段后面紧跟的属性 docstring;预览在译文下面按 Markdown 显示(Translator.doc)
group?: Node[] // 几条连着写的同种语句并成的一块(Dockerfile 连着的 COPY、LABEL):各条,node 是第一条;翻译插件排成一行字加一张表
embedded?: EmbeddedCode // 这块执行的是嵌着的另一种语言的代码(RUN):宿主分步,步是它的子块(只有一步时是那一步的子块)
}
export type CodeBlock = TextBlock | FactBlock
/** 着色用的词性,源码着色和译文着色共用。 */
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' 是写在表达式里的遍历,图标、颜色同「循环」块。 */
export type GroupStyle = 'iteration'
/** 框起来的一组词:预览画成嵌在前后文字里的小框,框头是图标和 label(翻译插件给);框里的词照样带下标,还能再套框。 */
export interface SegmentGroup { style: GroupStyle, label: string, segments: Inline[] }
/** 译文里的一段:着色的词,或框起来的一组词。 */
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
}
/** 代码里给一个定义起的中文名:name 是定义处那个名字,text 是中文名。 */
export interface DisplayName { name: SourceRange, text: string }
export interface DisplayNameFacts {
names: DisplayName[] // 起了中文名的定义,照源码顺序;一个定义几处都写了时语言插件定取哪个
markers: SourceRange[] // 写在注释里给代码阅读看的记号(`@Comment 当前用户`):翻注释、说明时去掉
}
export interface DisplayNameReader {
files: Record<string, string> // languageId → 工作区里这种语言的文件(glob):宿主建中文名索引
mentions: (source: string) => boolean // 便宜的文字预筛
find: (root: Node, source: string, languageId: string) => DisplayNameFacts
}
/** 翻译插件翻一块时拿到的这份源码:源码字符串,解析它的语言插件,和要从注释、说明里去掉的记号。 */
export interface SourceDocument {
source: string
language: LanguageAdapter
markers: SourceRange[]
/**
* 调用的名字(CallFacts.name)的定义是哪种块:宿主问语言扩展定义在哪,用那个文件的语言插件分块认出来。
* 同步地只交出已经查到的;还没查到、查不到、宿主不查都是 undefined,查到了宿主重排、重发(见「调用与构造」)。
*/
calleeKind?: (call: CallFacts) => BlockKind | undefined
/** 这个命令名的命令规则(这种语言注册的,先注册的算);没有时命令照抄。规则出错由宿主记下、当它没读。 */
commandRule?: (name: string) => CommandRule | undefined
/** 嵌着的代码(EmbeddedCode.node 是 node 的那段)读出来的样子:宿主用那种语言的插件读好了交出来;那种语言没注册时没有。 */
embedded?: (node: Node) => EmbeddedReading | undefined
}
/** 一份源码里语言插件自己认得出的一个名字:定义处,和用到它的各处(含定义处)。没有定义、高亮提供者的语言(shell)靠它跳转、高亮。 */
export interface LocalSymbol { definitions: SourceRange[], occurrences: SourceRange[] }
/** 筛选属性和值的显示名;没给名字的显示 id。 */
export interface AttributeNames {
attributes: Record<string, string> // access → 访问级别
values: Record<string, Record<string, string>> // access → public → 公共、async → true → 是、kind → function → 函数
}
/** 翻译插件:代码块 → 一行译文或属性表。 */
export interface Translator {
id: string // 'typescript.zh-template'
label: string // '中文直译'
languageIds: string[]
displayLanguage: string // 译文用的语言('zh'):库规则按它挑自己的词表
translate: (block: TextBlock, document: SourceDocument) => TextTranslation // 只翻 header 那一段;注释块给 Markdown 原文;跨几行写的调用带实参表
describe: (block: FactBlock, document: SourceDocument) => PropertyRow[] // 函数、类、枚举、异常、导入、常量、变量块 → 属性表
expression: (node: Node, document: SourceDocument) => Inline[] // 一个表达式 / 类型节点整个翻成一行译文,译法同属性表的值;库规则借它
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'] as const
export type KindIconName = (typeof KIND_ICON_NAMES)[number]
/** 库规则的种类:块左边只画图标(种类色),label 是悬停提示和读屏念的名字,筛选栏里也叫它;icon 只能从 KindIconName 里挑。 */
export interface KindBadge { label: string, color: ThemeColor, icon: KindIconName }
/** 库规则认领一块后给出的替身,下面挂着原块留下的子块:规则自己的卡片,或认成的现成属性表块。 */
export type RuleBlock = RuleCard | RuleFacts
/** 规则自己的卡片:一张属性表,种类的图标、颜色、名字和每一行都由规则给。 */
export interface RuleCard {
kind: string // 种类 id,「规则 id.名字」(sqlalchemy.table):筛选的 kind 值,要稳定
badge: KindBadge // 种类的名字、颜色、图标;筛选栏里这个种类也叫这个名字
rows: PropertyRow[]
defines?: SourceRange // 块自己的名字(类名):跳到的定义是它时闪整块
attributes?: BlockAttributes // 规则自己的筛选属性,叠在宿主从原块推出的上面;kind 填了不算;名字由 LibraryRule.attributeNames 给
absorbs: CodeBlock[] // 并进来的子块(原块 children 里的对象本身),不再单独成块;没并的照源码顺序留作子块,编辑范围同别的块,是原块整条语句
}
/** 把这块认成契约里现成的一种属性表块(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<RuleLocation[]> // 第一次查时建,照文件、行排好
}
export interface LibraryRule {
id: string // 'sqlalchemy'
label: string // 'SQLAlchemy 数据表'
languageIds: string[]
libraries: string[] // 关心的库:导入来源等于它或是它的子模块才启用
claim: (block: CodeBlock, context: RuleContext) => RuleBlock | undefined
resolve?: (key: string, workspace: RuleWorkspace) => Promise<RuleLocation[]> // 查自己标的引用;没给就不能标引用
attributeNames?: (displayLanguage: string | undefined) => AttributeNames // 自己报的筛选属性和值叫什么(同 Translator.attributeNames,用词归规则);翻译插件给了名字的以翻译插件为准
}
export interface CodeReaderApi {
registerLanguage: (adapter: LanguageAdapter) => Disposable
registerTranslator: (translator: Translator) => Disposable
registerLibraryRule: (rule: LibraryRule) => Disposable
registerCommandRule: (rule: CommandRule) => Disposable
}
// ── 命令规则(见「命令规则」一节)──
/** 重定向:输出(1)、错误输出(2)或两样写到 / 追加到文件,并进另一路(`>&2`、`2>&1`),从文件读。 */
export type Redirect =
| { kind: 'write' | 'append', stream: 'output' | 'error' | 'both', target: Node }
| { kind: 'merge', stream: 'output' | 'error', into: 'output' | 'error' }
| { kind: 'read', source: Node }
/** 命令的一个词:节点,和它写死的文字(去掉引号、转义;有展开的没有)。 */
export interface CommandWord { node: Node, literal?: string }
/** 一条命令:命令名、后面的各个词(选项、选项的值、要处理的东西,规则自己认)、重定向。 */
export interface CommandFacts { name: CommandWord, arguments: CommandWord[], redirects: Redirect[] }
/** 规则读出来的一行;value:读出来的是一个东西(`dirname "$0"` 是脚本所在目录),不是一个动作。 */
export interface CommandReading { text: Inline[], value?: boolean, table?: PreviewTable } // table:选项写得多的命令的「选项 | 意思」表
export interface CommandContext {
displayLanguage: string // 当前翻译插件的译文语言,规则按它挑词表
translate: (word: Node) => Inline[] // 一个词照常翻(名字带源码下标)
command: (name: CommandWord, args: CommandWord[]) => CommandReading | undefined // 读写在实参里的另一条命令(find -exec)
}
export interface CommandRule {
id: string // 'shell.rm'
label: string // 'rm'
languageIds: string[]
commands: string[] // 管哪些命令名;几条规则管同一个名字时先注册的算;'*' 管没有自己的规则的命令
read: (command: CommandFacts, context: CommandContext) => CommandReading | undefined // 读不了返回 undefined,命令照抄
}
activate()返回{ codeReader: CodeReaderApi };其他扩展声明extensionDependencies后用extensions.getExtension('clurdra.superpowers-vscode-clurdra')!.exports.codeReader注册。- 内置的 TypeScript、Python、Shell、Dockerfile 语言插件、四个中文直译翻译插件、shell / 装系统 / 装软件包的命令规则和 SQLAlchemy、Celery、enum、dataclasses、FastAPI 库规则在
activate()里通过同一个 API 注册(规则按这个顺序),没有旁路。 - 注册表变化时更新
superpowers.codeReader.languageIds上下文,并让已打开的预览刷新。 - 分工:语言插件只从语法树读出和语言无关的事实(facts,引用源码节点,不做正则匹配文字);翻译插件决定属性名、枚举词(公共 / 是 / 默认导出)和值怎么译,也给筛选属性和值起名(
attributeNames)。筛选属性的通用部分由宿主从 facts 推出(attributes.ts的blockAttributes),语言插件只在BlockShape.attributes里补自己语言特有的。第三方语言插件给函数、类、枚举、异常、导入、常量、变量块填 facts 就能用上任何翻译插件的属性表;自己语言的字典 / 映射字面量填进DeclaratorFacts.entries就能展开成子行(中文模板直译按字面量的节点类型查叫法,见ZhTable.mapLiterals)。写在表达式里的遍历(推导式这类)由语言插件的iteration给结构(节点 →IterationFacts),翻译插件排成一个SegmentGroup(中文直译是样式iteration、框头是结果类型的译名);webview 只认框的样式 id,不认语言。调用由语言插件的call给结构(节点 →CallFacts),跨几行写的由翻译插件排成一行字加一张表(PreviewTable,见「调用与构造」);webview 只画表。 - 语言插件的信息只经契约到翻译插件:宿主翻每一块时把
{ source, language }(SourceDocument,language 是解析这份源码的语言插件)交给translate/describe,翻译插件要词性、遍历结构都从language取,不 import 任何语言插件的模块(lint 规则挡住translators/下的非测试文件 importlanguages/)。所以第三方翻译插件(英文、AI 翻译)拿得到和内置翻译插件一样的信息,第三方语言插件也能配上任何翻译插件。 - 第三方语言插件要给的:
id、languageIds、grammarPath、toBlocks(块的种类、头部、子块;函数、类、枚举、异常、导入、常量、变量块带 facts)、tokenRole。可以给的:facts 里语言有才填的项(async、abstract、typeOnly、access、doc、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)两种,按有没有as分:- 认成现成的属性表块(
RuleFacts):as是种类和 facts(BlockFacts,例如{ kind: 'enum', facts: EnumFacts }),absorbs同卡片。宿主把原块换成这种块(节点不变,头部是原块整条语句,子块是留下的),此后和语言插件报的这种块完全一样:翻译插件的describe排属性表、种类名取翻译插件的、颜色取这个种类的、筛选属性从 facts 推、块自己的名字从 facts 取。规则不给用词,也不给标签和颜色。只要检查并进来的是不是这块的子块。 - 规则自己的卡片(
RuleCard),下面几条都说的是它。
- 认成现成的属性表块(
- 卡片必须给:
kind(写成「规则 id.名字」,宿主核对前缀;筛选记住的就是它,要稳定)、badge(种类的名字、暗 / 亮两种颜色和 lucide 图标名)、rows(用现成的PropertyRow:属性名 + 着色的值或 Markdown,表格就是带columns的子行)、absorbs(并进来的子块)。可以给:defines(块自己的名字,跳到定义是它时闪整块)、attributes(自己的筛选属性);表格子行填PropertyItem.defines(字段名),跳到这个定义时只闪那一行。 - 只经契约:规则拿到的是块(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(去掉引号),有一个等于规则的某个库、或以「库.」「库/」开头(子模块),规则才对这份源码启用。规则不用自己读导入语句。 - 认领:宿主从顶层往下每块问一遍启用的规则,按注册顺序,第一个认领的算(先注册的优先);被认领的块留下的子块照样问。
- 出错:规则抛错、或替身不合约定(种类 id 没带规则 id 前缀、并进来的不是这块的子块、属性表里的引用标的不是自己 / 规则没给
resolve/ 引用的段没带下标)时,宿主记一条警告日志(输出面板「Superpowers」),当它没认、接着问下一个规则;都没认这块照常显示。预览不会因为规则出错而失败。 - 并进来的子块:不再单独成块,由规则挑(
absorbs),宿主不替规则多并。没并进来的子块(方法、规则不讲的注释)不管写在哪(字段中间也算),都照源码顺序是替身的子块,预览里照常看得见。替身的编辑范围同别的块,是原块整条语句(从头部开始,到原块连同各子块的最后一行,类就是整个类),盖住留下的子块的行:替身编辑时预览不画这些子块,同一行不会同时在两处能改(见「预览(代码块树)」)。内置规则并成员时连同紧挨在它前面的注释块一起并(讲的是这个成员),讲方法的注释、最后面的注释照常留下。 - 卡片在预览里:种类图标、颜色取
badge(icon;颜色暗色、亮色主题各取一个),悬停提示是badge.label,内容是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),和类块一样默认收起子块。
- 认哪些类:装饰器是
@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__ 照常是卡片的子块。
内置: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 的规则,按下面的先后,第一条对上的算:
- 写了
Depends(...)/Security(...)(默认值= Depends(x),或Annotated[T, Depends(x)]):依赖,不是请求带的。 - 名字在路径里(
{user_id}、{file_path:path},前缀和路径都算):路径参数。 - 写了
Path/Query/Header/Cookie/Body/Form/File(同上两种写法):照写的,Header进「请求头」、Cookie进「Cookie」,Body/Form/File进「请求体」。 - 类型是别处写好的类型别名(
Int4Query = Annotated[int \| None, Query(...)]、FilterQuery = Annotated[FilterList, Depends(...)]):经工作区索引查到它的标记(见下),照 1、3 放。 Request、Response、WebSocket、HTTPConnection、BackgroundTasks、SecurityScopes:FastAPI 自己填的,不列。- 上传文件(
UploadFile、list[UploadFile]):请求体。 - 简单类型:查询参数。简单类型是 int、float、str、bool、bytes、complex、UUID、datetime、date、time、timedelta、Decimal、Any、EmailStr、HttpUrl、AnyUrl、SecretStr、
None、Literal[...]、枚举类(别处定义的经索引查),和只包着它们的X | None、Optional/Union/list/set/tuple/Sequence这些;没标注类型的也是。 - 别的(Pydantic 模型、字典……):请求体。
*args、**kwargs 不算。
响应表(同一个状态码的几处写在一行里:内容「、」隔开、去重,说明「;」隔开、去重):
- 成功那一行:装饰器的
status_code(整数,或status.HTTP_201_CREATED这种常量),没写是 200;内容是response_model,没写就用返回类型标注(response_model=None、-> None没有内容);说明是response_description,没写是「成功」(comment 色)。 - 路由对象和装饰器的
responses={...}:每一项一行,键是状态码(整数、"404"、常量;"5XX"、"default"照写);值是字典时"model"是内容、"description"是说明,值不是字典时整个是内容;**x不列。 - 函数体里直接抛的异常(嵌套的函数、类、lambda 里的不算,只写
raise的不算,raise X from e算,调别的函数间接抛的不算)按状态码归:HTTPException(从 fastapi 或 starlette 导入)的状态码是status_code=或第一个位置参数,消息是detail=或第二个位置参数;别的类的状态码是类上的status_code = N,经工作区索引查(下面),消息是第一个位置参数(或message=、detail=)。内容是异常类名(type 色,带下标,⌘ + 点击到类的定义),说明是消息:字符串照写(comment 色),别的写法交给翻译插件翻。查不到状态码的异常(没查完、不是 HTTP 异常)不列。 - 有参数或请求体(路径 / 查询 / 请求头 / Cookie / 请求体里有一项)时,422 那一行加上「参数校验失败 | FastAPI 自动返回」(hint 色,淡)。
- 状态码的颜色:小于 400 的绿色(暗 #2ea043 / 亮 #1a7f37,段的
color),别的 string 色;不是三位数字的照写、string 色。
别的文件里的定义(索引 fastapi.names,见「规则认领时查别的文件里的定义」):看工作区里所有 **/*.py,预筛有 status_code、Annotated 或 Enum 才解析;从模块顶层认出:类上写了 status_code = N(带不带注解、status.HTTP_403_FORBIDDEN 这种常量也算)的类 → 值是状态码;父类是 enum 的枚举类型(Enum、IntEnum、StrEnum、Flag、IntFlag)的类 → Enum;X = Annotated[T, Query(...)] 这种类型别名 → 标记名(Query、Path、Depends……)。父类继承下来的 status_code(class AccountFrozen(Forbidden) 没自己写)不认。
样例(fixtures/routes.py 的 update_user,Forbidden 定义在 luffy 的 core/exceptions/schemas.py(fixtures/appExceptions.py),status_code = 403;测试里逐字核对):
名称: update_user
说明: 更新用户信息。…
路由器: router
标签: 用户管理
异步: 是
依赖: get_db(db)、get_current_user(current_user)
请求: PUT /users/{user_id}
URL | /users/{user_id}
方法 | PUT
路径参数 | user_id: Int4Path
请求体 | UserUpdateRequest
响应: 200 / 403 / 422
200 | UserDetailResponse | 成功
403 | Forbidden | 无权更新此用户信息;无权修改角色;无权修改启用状态;无权修改密码
422 | 参数校验失败 | FastAPI 自动返回
skill_hub/routes.py 的 list_skills:查询参数 | query: 字符串 | 空, tag: 字符串 | 空, skip: 整数, limit: 整数,依赖 get_db(db)、get_current_user(参数名是 _);user/console/routes.py 的 create_user:成功那一行 201 | UserDetailResponse | 成功,请求体 UserCreateRequest。
配色:名称 function、标签 string、依赖名 function、「(参数名)」hint、表格第一列(项)hint、URL 同上、参数名 variable、类型按翻译插件、异常类名 type、消息和说明 comment、自动带的 422 hint。
命令规则
shell 脚本大半是一条条命令,一条命令读成什么取决于命令本身(rm -rf 是「删除(连同里面的东西、不提示)」,tar -xzf 是「解压 tar.gz」)。这些既不是语法(语言插件不管),也不是某种语言的措辞(翻译插件不该认识几十个命令)。命令规则是第五种插件:按命令名注册,把一条命令连同它的选项读成一句译文,像库规则按库认写法一样。
- 契约:
CommandRule(id、label、languageIds、commands、read,见「通用契约」)。语言插件认出一条命令(LanguageAdapter.command→CommandFacts:命令名、后面的各个词连写死的文字、重定向),翻译插件读到命令时按命令名问SourceDocument.commandRule,交给规则的read,拿回一行译文(CommandReading)。规则读选项、选项的值用词的literal(去掉引号、转义的文字),要翻的词(变量、字符串、展开)交回CommandContext.translate,名字照样带源码下标;find … -exec rm -rf {} +里的命令用CommandContext.command读。 - 注册:
CodeReaderApi.registerCommandRule(rule),返回Disposable;内置规则走同一个 API。宿主按语言取注册着的规则(registry.commandRulesFor),按命令名排好(commandRules.ts的commandRuleLookup):几条规则管同一个名字时先注册的算。 - 读不了:规则返回 undefined(不认识的写法、不是这种译文语言)或读的时候出错(宿主记下「命令规则 X 读「rm」时出错,照抄命令:…」、当它没读),命令照抄命令名、后面的词照常翻。没有规则的命令一样(
foo -x "$y"→foo -x y)。 - 规则不认识的选项照抄在译文后面(
rm -rf --one-file-system x→ 删除 x(连同里面的东西、不提示) --one-file-system)。 - 重定向不归规则:翻译插件读完命令再统一接上(见「Shell 脚本」),所以每条规则都不用管
>&2、2>/dev/null。 - 译文语言:内置规则只有中文词表,
CommandContext.displayLanguage不是zh时不读(照抄)。 - 管哪些语言:内置规则的
languageIds是 shellscript 和 dockerfile(Dockerfile 数组写法的命令不经过 shell,执行的还是同一些程序)。 - 没有自己的规则的命令交给管
'*'的规则:内置的只认只写了一个--version(-V、version)的命令,读成「打印 pg_repack 的版本」,别的照抄。 - 选项多的:规则把认出的选项连同意思交给
commands/reading.ts的explained(选项在源码里的写法、写了几遍都由parseOptions记下),4 个以上(同一个写几遍算一个)或有写了几遍的,这一行后面写「· N 个选项」,意思排成「选项 | 意思」表(CommandReading.table,翻译插件排在这一行下面,同调用的实参表),写了几遍的并成一行「--no-install-package × 11 | 不装这些包:…」;别的写在括号里。 - 内置规则(文件、下载、打包、文本处理和 shell 自带的在
commands/shellCommands.ts,建用户、密钥、装文件、改属主和--version在commands/systemCommands.ts,装软件包、装依赖、跑程序的在commands/packageCommands.ts;认选项的共用办法在commands/reading.ts:-rf连写拆开,-o 值/-o值/--retry 3/--retry=3都认,--之后都不是选项):
| 命令 | 读成 |
|---|---|
rm -rf dest |
删除 dest(连同里面的东西、不提示) |
mkdir -p d |
建目录 d(连同上级目录) |
curl -fL --retry 3 -o out url、curl -fsSL url |
下载 url 到 out(失败就报错、跟着跳转走、最多重试 3 次)、请求 url(不打印进度、失败就报错、跟着跳转走;-sS 一起写只写「不打印进度」) |
tar -xzf f -C d、unzip -q f -d d |
解压 tar.gz f 到 d、解压 zip f 到 d(不打印过程) |
find d -mindepth 1 -maxdepth 1 -type d ! -name m -exec rm -rf {} + |
找出 d 下一层的目录,除了 m,逐个 删除(连同里面的东西、不提示);find d -maxdepth 1 -name '*.dll' -delete → 找出 d 这一层的 *.dll,逐个 删除;find d -type l → 找出 d 下面所有符号链接 |
cp -L a b、mv a b、mv a b d |
复制 a 指向的实体文件 到 b、改名 a → b、移动 a、b 到 d |
echo "下载 $url"、echo "$out"、printf '%s\n' "$x" |
打印 "下载 url"(有写死的文字时连引号)、打印 out、按格式打印 "%s\n",填入 x |
exit 1、return 1 |
退出,退出码 1、返回 1 |
cd d、pwd |
进入 d、打印当前目录 |
basename x、dirname x、dirname "$0" |
取文件名 x、取所在目录 x、脚本所在目录(读出来是一个东西,$(…) 直接写它) |
readlink x、readlink -f x |
读出 x 指向的路径、取 x 的真实路径 |
shasum -a 256 f、sha256sum f、md5sum f |
算 f 的 sha256、算 f 的 md5 |
cut -d' ' -f1、cut -c1-3 f |
按空格切,取第 1 段、取 f 每一行的第 1 到 3 个字符 |
du -sh a b |
统计占用空间 a、b(按人读的单位、只看总数) |
cat f、grep -qi error log、sed -i 's/a/b/g' conf |
打印 f 的内容、在 log 里找含 error 的行(只看找没找到、不分大小写)、把 conf 里的 a 全换成 b(直接改文件) |
chmod +x f、ln -sf t link、touch f |
给 f 加上可执行权限、建符号链接 link → t(已有就替换)、更新修改时间 f(没有就建空文件) |
source f / . f、read -r x、sleep 1 |
读入 f 里的变量和函数、读一行进 x(不认转义)、等 1 秒 |
continue、break、true / :、false |
跳过这一个、跳出循环、什么也不做、失败 |
apt-get update、apt-get install -y --no-install-recommends a b、apt-get purge -y x、apt-get clean |
更新软件包列表、安装 a、b(不装推荐的包、不用确认)、卸载 x(连配置一起删、不用确认)、清掉下载的安装包 |
apk add --no-cache --virtual .deps gcc、apk del .deps |
安装 gcc(不留索引缓存、记成一组 .deps)、卸载 .deps |
pip install --no-cache-dir -r requirements.txt、pip3 install -U pip |
pip 安装 requirements.txt 里列的包(不留缓存)、pip 安装 pip(升级到最新) |
npm ci --omit=dev、npm run build、npm run migrate |
npm 按 lock 文件装依赖(不装开发依赖)、npm 构建、npm 运行脚本 migrate(build / test / dev / start / lint 这些常见的脚本名写成要做的事) |
pnpm install --frozen-lockfile --ignore-scripts、pnpm build、yarn install --immutable |
pnpm 装依赖(严格按 lock 文件、不跑安装脚本)、pnpm 构建、yarn 装依赖(严格按 lock 文件) |
corepack enable、corepack prepare pnpm@latest --activate |
开启 corepack、准备 pnpm latest(并设为当前用的版本) |
uv sync --frozen --no-dev、uv pip install --system -r req.txt、uv run --no-sync uvicorn app:app --host 0.0.0.0 --port 8000 |
uv 按 lock 同步依赖(严格按 lock 文件,不改它、不装开发依赖)、uv 安装 req.txt 里列的包(装进系统的 Python)、用 uv 运行(不先同步依赖) uvicorn app:app,监听 0.0.0.0:8000(uv run 后面的命令照它自己的规则读) |
useradd -m -s /bin/false appuser、adduser --system --uid 1001 nextjs、addgroup --system --gid 1001 nodejs、adduser web www |
新建用户 appuser(建主目录、不能登录)、新建系统用户 nextjs(uid 1001)、新建系统用户组 nodejs(gid 1001)、把 web 加进组 www(Debian 和 Alpine / BusyBox 的写法都认) |
gpg --dearmor -o k.gpg、install -d /d、install -m 644 a /etc/a、chown -R u:g /x |
转成二进制密钥 写到 k.gpg、建目录 /d、安装 a 到 /etc/a(权限 644)、把 /x 的属主改成 u:g(连同里面的东西) |
pg_repack --version |
打印 pg_repack 的版本(管 '*' 的规则) |
解析运行时
- 依赖
@vscode/tree-sitter-wasm(VS Code 团队维护,包含 web-tree-sitter 运行时和 TypeScript / TSX / JavaScript / Python / Bash 语法包,版本配套)。它没有的语法包(Dockerfile)取自开发依赖tree-sitter-wasm(Crysthamus/tree-sitter-wasm:各家语法包在 GitHub Actions 里编译,上游提交钉死,每个 wasm 都用 cosign 签名、发 npm 带来源证明;tree-sitter-dockerfile 是 ABI 15,和@vscode/tree-sitter-wasm的运行时配得上),esbuild.mjs用它的getWasmPath取出来,和别的一起复制进dist/tree-sitter/;单元测试直接用包里的(testSupport.ts的DOCKERFILE_WASM_DIR)。 - 打包:
esbuild.mjs构建时把tree-sitter.wasm和用到的语法包复制到dist/tree-sitter/(package.json的files已包含dist/**/*)。 - 运行时初始化一次
Parser.init({ locateFile })指向dist/tree-sitter/tree-sitter.wasm;语法包按路径懒加载并缓存。 - 文件扩展名到语法包:
.ts→ typescript;.tsx→ tsx;.js/.jsx/.mjs/.cjs→ javascript(tree-sitter-javascript 支持 JSX);.py→ python;shellscript(.sh/.bash/.zsh、首行写着 shell 解释器的文件)→ bash;dockerfile(Dockerfile、Dockerfile.*、*.dockerfile、Containerfile)→ dockerfile。 - 嵌着的代码只解析源码里的几段(
TreeSitterRuntime.parse的ranges,交给 web-tree-sitter 的includedRanges),节点下标、行列照旧是整份源码的。以languageId为准。
代码块树规则
TypeScript / JavaScript
- 顶层与语句块内的每条语句一块;连续的行注释合成一块注释。
- JSDoc(以
/**开头的块注释,/**/不算)和下一条语句之间只有空白(空行也算,中间隔着装饰器也算)、那条语句是函数、类或枚举块时,它是这个函数、类、枚举的说明:进facts.doc,头部往上延到注释开头,不单独成块。其余情况(后面不是函数、类、枚举块,中间隔着别的注释,在上一条语句的行尾)照旧是注释块。//行注释和普通/* */块注释不变。 import_statement→ import。export_statement包着声明 → 按声明定种类,节点取整个export_statement(头部包含export)。function_declaration/generator_function_declaration/method_definition→ function,子块来自主体语句。- 变量声明的唯一声明值是带语句块主体的箭头函数 / 函数表达式 → function(React 组件、回调常见写法),子块来自主体;是类表达式(
const A = class {})→ class。 lexical_declaration/variable_declaration:模块顶层的const→ const,其余 → variable。class_declaration/abstract_class_declaration、export default class {}→ class,子块:方法 → function,字段 → field(值是带语句块主体的函数 → function,值是类表达式 → class),注释 → comment。- 类(含类表达式)的父类是异常类时(见「异常」)→ exception:头部是整个类,字段连同前面的注释并进属性表,方法(含 constructor)、别的成员照常是子块。整棵树建好后按整份文件认(同一文件里的异常类写在后面也算)。
interface_declaration→ type,子块:属性 / 方法签名 → field;type_alias_declaration→ type;enum_declaration(含const enum、declare enum的ambient_declaration、export包着的)→ enum,头部是整条声明,成员在属性表里讲、不成子块。if_statement→ if,子块来自 consequence;else/else if展平成紧跟其后的兄弟块 else(else if的头部是else if (...),子块来自它的 consequence)。for/for…in/for…of/while/do→ loop。try_statement→ try;catch_clause→ 兄弟块 catch;finally_clause→ 兄弟块 finally。switch_statement→ if,每个case/default→ 子块 if。return_statement→ return;其余语句(含throw、单独一个字符串的语句:'use strict'这类指令,TypeScript / JavaScript 没有拿字符串当说明的写法)→ statement。- 表达式语句的参数里带语句块主体的函数(
useEffect(() => { … })、describe('x', () => { … }))→ statement,子块来自那个函数主体,头部到主体开始为止。
Python
import_statement/import_from_statement/future_import_statement→ import。function_definition→ function;decorated_definition按内部定义定种类,头部包含装饰器。class_definition→ class;类体里的带注解赋值 → field。- 父类是异常类的类(见「异常」)→ exception:头部是整个类,类属性连同前面的注释并进属性表,方法(含
__init__)、别的语句照常是子块。整棵树建好后按整份文件认(同一文件里的异常类写在后面也算)。 - 字符串语句:单独一个普通字符串字面量的表达式语句(
string,或隐式拼接的concatenated_string,每段的前缀只有 r / u 或没有;f-string、t-string、字节串不算,照旧是 statement)。 - 函数、类主体的第一条语句(注释不算)是字符串语句时,它是这个函数、类的 docstring:进
facts.doc,头部往下延到字符串结尾(它和冒号之间的注释也在头部里),子块从它后面开始,不单独成块。 - 属性 docstring:模块、类主体里(条件、函数等语句块里的不算)紧跟在一条赋值后面的字符串语句是这个常量、变量、字段的说明:常量、变量进
facts.doc,字段进块的doc,头部往下延到字符串结尾,不单独成块。「紧跟」同 JSDoc:中间可以隔空行和赋值的行尾注释,隔着别的注释、语句就不算;只挂一个,后面再来的字符串照下一条规则。 - 其余字符串语句(模块开头的 docstring、写在语句中间当注释用的字符串)和
comment→ comment。 - 赋值:模块顶层且变量名全大写 → const,其余 → variable。
if_statement→ if;elif_clause/else_clause→ 兄弟块 else;for/while→ loop(循环的else→ 兄弟块 else)。try_statement→ try;except_clause→ catch;finally_clause→ finally。with_statement→ statement,子块来自主体;match_statement→ if,case→ 子块 if。return_statement→ return;其余 → statement。- 复合语句头部到主体开始为止(包含行尾冒号,翻译时去掉)。
Shell
- 首行
#!→ interpreter;别的comment连着的几行合成一块注释;紧贴在函数定义上面(中间没空行)的注释是函数的说明,进facts.doc、头部往上延,不单独成块。 set后面全是开关(-euo pipefail、+x、-o nounset)→ options;有不是开关的词(set -- a b)→ command。variable_assignment、declaration_command(local / export / readonly / declare 声明了变量的)→ variable;readonly 的、脚本顶层名字全大写且值里没有展开的 → const。一句声明几个变量是一块(DeclaratorFacts各一项)。function_definition→ function,子块来自主体各句。if_statement→ if,子块是 then 后面的各句;elif_clause/else_clause展平成紧跟的兄弟块 else。for_statement/c_style_for_statement/while_statement(until 也是)→ loop,头部到 do 为止,子块是 do … done 里的各句。case_statement→ case:每个情况都短(最多 3 句,都是一行写完的命令、赋值、声明,没有注释)时整条一块;否则头部到 in 为止,每个case_item一块子块 case(头部是它的模式,到)为止),子块是要做的各句。pipeline→ pipeline;最后一段是循环时头部到 do 为止,循环主体是子块。[ … ]、[[ … ]](前面可以带!),和用&&/||连起来、全是条件或前面全是条件最后跟一句的一串 → if。return命令 → return;别的命令(带不带重定向)、&&/||连起来的一串命令 → command;其余语句 → statement。
Dockerfile
- 文件最开头连着的
# syntax=/# escape=/# check=(从第一行起、中间没空行)→ directive。 - 每个
from_instruction开一个 stage,头部是 FROM 那一行,紧贴在上面(中间没空行)的几行注释是它的说明、头部往上延;到下一个阶段(连同它上面的说明)之前的指令、注释是子块;第一个 FROM 之前的照常是顶层的块。 - 相邻行的注释合成一块注释;只写了中文名记号、紧挨着下一条指令的注释算在那条指令的块里。
run_instruction→ run(整条指令,shell 写法的带embedded);copy_instruction、add_instruction→ copy;label_instruction→ label;env_instruction、arg_instruction→ variable(facts 的 scope 分构建参数、环境变量);WORKDIR / USER / EXPOSE / VOLUME / STOPSIGNAL / SHELL / MAINTAINER → setting;healthcheck_instruction→ health;cmd_instruction、entrypoint_instruction→ start;onbuild_instruction→ setting,包着的指令照这些规则是它的子块;别的(语法错误)→ statement。- 连着写的同种指令(紧挨着下一行)并成一块:ENV、ARG 并成一个变量块(declarators 是各条的全部),LABEL 和从同一个地方复制的 COPY 并成一块(
TextBlock.group,头部盖住全部);ADD、带 heredoc 的 COPY 不并。
属性表信息(facts)从哪来
- TypeScript 函数:
function_declaration/generator_function_declaration/function_signature、类里的method_definition/ 方法签名、值是带语句块主体函数的变量和类字段、export default的函数值。名称取声明名 / 变量名 / 字段名;说明取紧贴在前面的 JSDoc 注释节点(见上面代码块树规则);参数来自formal_parameters(required_parameter/optional_parameter的 pattern、type、value,JavaScript 的assignment_pattern),不带括号的单参数箭头函数取parameter;返回类型取return_type里的类型;异步看有没有async;类成员的装饰器是类主体里排在它前面的decorator兄弟节点。 - TypeScript 类:
class_declaration/abstract_class_declaration、类表达式class(export default class {}、变量的值、类字段的值)。名称取类名 / 变量名 / 字段名;说明同函数;访问级别同函数(类字段按类成员);抽象看节点类型是不是abstract_class_declaration(只有 languageId 是 typescript / typescriptreact 时填,JavaScript 没有这个概念,不填);装饰器是export_statement和类声明自己的decorator子节点(@d export class、export @d class、@d class都算),类字段的值是类表达式时还有类主体里排在它前面的decorator;泛型参数是type_parameters里每个type_parameter;父类、实现的接口在class_heritage里:extends_clause的每个 value 一项,跟在它后面的type_arguments归它,implements_clause里每个类型一项;JavaScript 的class_heritage直接是extends后面的表达式。 - TypeScript 导入:
import_statement的source(import x = require()取import_require_clause里的);import_clause里的标识符是默认导入,namespace_import是* as,named_imports里每个import_specifier的 name / alias;语句本身带type是仅类型(同抽象,只有 TypeScript 填)。 - TypeScript 变量:每个
variable_declarator的 name / type / value;value 是object时 entries 是它的具名子节点(注释除外):pair的 key / value(value 又是object时连它的 entries)、shorthand_property_identifier(键和值都是它)、spread_element里的表达式、method_definition的 name 和头部(到 body 之前)。 - Python 函数:
function_definition的 name / parameters / return_type,async子节点;说明是主体开头 docstring 的字符串节点(string/concatenated_string);装饰器是decorated_definition里的decorator;参数identifier、typed_parameter(名字是第一个具名子节点)、default_parameter、typed_default_parameter、*args/**kwargs,*和/分隔符不算参数。 - Python 类:
class_definition的 name;说明是主体开头 docstring 的字符串节点;装饰器同函数;泛型参数是 Python 3.12type_parameters里的每一项;父类是superclasses(argument_list)里的每个具名子节点,照写的顺序,关键字参数(metaclass=ABCMeta)、*bases、**kw也算;没有抽象、实现的接口。Python 没有枚举语法:enum 库的类由库规则认成枚举块(见「内置:enum(Python)」)。 - 异常(TypeScript、Python 一样):名称、说明、访问级别、装饰器、父类取自原来的类(同上两条),TypeScript 的抽象、泛型参数、实现的接口不带;类属性、构造参数见「异常」。
- TypeScript 枚举:
enum_declaration的 name、body;说明、访问级别同类(export了公共、顶层没export私有、嵌套的没有);没有装饰器、父类。成员是 body 里的property_identifier(没写值)和enum_assignment(name、value),照源码顺序;没写值的接着上一个数往上加(第一个 0),上一个的值是数字字面量(可带正负号,0x10这类也算)或算出来的数才算得出,否则没有computed。说明:从上一个成员之后起单独成行的注释(//和 JSDoc),和写在它这一行行尾的注释;同一行上一个成员后面的 JSDoc 讲的是下一个成员;最后一个成员后面的注释不算。 - Python 导入:
import_statement每个 name 一个来源、导入项是整个模块;import_from_statement的 module_name 和各 name(aliased_import拆出 alias),wildcard_import是全部;future_import_statement的来源是__future__。 - Shell:解释器取
#!后面的整句,第一个词是env的是按 PATH 找、程序名是它后面那个词,否则是写死的路径、程序名是路径最后一节。选项:每个开关一项,单字母换成 bash 的长名字(e → errexit、u → nounset、x → xtrace……),-o 名字照写,-开、+关。变量:variable_assignment的 name / value,声明里没给值的只有 name。函数:name 是函数名,doc 是紧贴在上面的注释;positional是主体里(不进嵌套的函数)用到的$N/${N}/${N:-…},照位置排,local x="$N"、x=$N给它起名字;prints是没重定向到别处的echo/printf(进 if、循环、分情况,不进$(…)、管道、嵌套的函数);captured看同一文件里有没有$(函数名 …);returns是主体里的return命令。 - Dockerfile:解析指令是每行注释
# 名字=值的名字(小写)和值;阶段取image_spec的 name / tag / digest 字段、as字段(image_alias),stage 看镜像名是不是前面某个阶段的名字(不分大小写),选项是param、mount_param节点的原文--名字=值(值在语法树里没有单独的节点,按原文拆,记下值从哪开始);变量是env_pair的 name / value、arg_instruction的 name / default;健康检查的 none 看有没有 NONE,检查命令是里面的cmd_instruction;数组写法取json_string_array,shell 写法取shell_command里各个shell_fragment(line_continuation不算),只有一个<<EOT的取它下面heredoc_block从第一行到最后一行。 - Python 变量:
assignment的 left / type / right,说明是紧跟在后面的属性 docstring 的字符串节点(见上面代码块树规则);right 是dictionary时 entries 是它的具名子节点(注释除外):pair的 key / value(value 又是dictionary时连它的 entries)、dictionary_splat里的表达式。
中文模板直译
结构照抄源码(按节点模板重排的语法结构除外,见下文),只替换三类东西,其余(标识符、属性名、字面量、标点)原样保留并按词性着色:
- 关键字:只认语法树里的关键字节点(匿名节点),所以属性名
i.number、对象键不会被误翻。 - 内置类型:只在类型位置翻(TS 的
predefined_type、Python 注解里的内置类型名)。 - 运算符:
===全等、!==不全等、==等于、!=不等于、&&并且、||或者、!非(Python 的not/and/or本身是关键字)。
同一套表和节点模板也用来翻属性表里的值:引擎能翻任意一个节点(只取它落在块头部里的那一段),参数类型、默认值、返回类型、变量类型和初始值、字典 / 对象各项的键和值都走这条路。
其他规则:空数组字面量 → 空数组、空对象字面量 → 空对象(Python 空列表 → 空列表、空字典 → 空字典);复合语句头部去掉结尾的 { 或 :;多行合成一行(连续空白压成一个空格,右括号前的尾逗号去掉);注释块给去掉注释符号的 Markdown 原文(见「注释的 Markdown 排版」)。关键字与前后内容之间保留源码里的空格(! 翻成「非」后补一个空格)。
关键字按所在语法节点选择译法:同一个关键字在不同语法结构里意思不同时,表里写一个默认译法,再加若干「语境 → 译法」。语境是从关键字节点往上的节点类型链,外层在前:raise_statement 指父节点是 raise_statement,for_statement > else_clause 指父节点是 else_clause、祖父节点是 for_statement(关键字包在一层子句节点里时要看到祖父节点)。几条语境同时匹配时取最长的,都不匹配用默认译法。节点类型以 tree-sitter 语法为准。下面两张表里写成「(语境)译法 / (语境)译法」的关键字就是按这条规则区分的,排第一个的是默认译法。
节点模板:照抄源码结构读不通的语法结构,按语法节点类型给一个模板,把译文重排成中文语序。模板是一串项,每项要么是插入的译词,要么是子节点槽位:槽位按 tree-sitter 字段名引用子节点;语法没给字段名的,按具名子节点序号(0 起,注释不占序号)引用。命中模板的节点只输出模板里的项,节点自己的关键字和标点(if / else、? / :)不再输出;插入的译词一律按关键字(keyword)着色,模板各项之间空一格;槽位里的子节点照常递归翻译,里面的关键字、运算符、嵌套的同类结构照样处理。两种情况不重排、照常翻:模板引用的子节点不存在;节点没整个落在要翻的这一段里。目前的模板:
| 语言 | 节点类型 | 模板 |
|---|---|---|
| TypeScript | ternary_expression(c ? a : b) |
如果 〈condition〉 则 〈consequence〉 否则 〈alternative〉 |
| Python | conditional_expression(a if c else b,没有字段名,具名子节点依次是 a、c、b) |
如果 〈1 号〉 则 〈0 号〉 否则 〈2 号〉 |
写在表达式里的遍历(推导式)不走节点模板:它的节数不定、要换成一个框,由语言插件的 iteration(翻译时从 SourceDocument.language 拿到)认出来,引擎按「推导式」一节的规则排进框里;和节点模板一样,没整个落在要翻的这一段里就不重排、照常翻。跨几行写的调用也不走节点模板:由语言插件的 call 认出来,引擎只在翻一行译文(translate)、变量 / 常量的值(declaredValue)和实参表里的值时按「调用与构造」一节排(每段一个 TableSlot,见 expandedText、emitCall、argumentTable),属性表别的值、库规则借的 expression 照常一行。
节点模板也排不了的写法(shell 的 ${x:-d}、[ -f x ]、case、for f in glob)由翻译插件的节点读法排(ZhTable.readers:节点类型 → 读法,读法拿到 ReaderContext:照常翻子节点的 say、名字照抄带下标的 name、读一条命令的 command,能给一张排在这一段下面的表,分情况的「情况 | 做什么」就是它;返回 undefined 照常翻)。一条命令由语言插件的 command 认出、按命令名交给 SourceDocument.commandRule(见「命令规则」),引擎接上重定向,规则给了选项表就排在这一段下面;没有规则时命令名照抄(按函数着色)、各个词照常翻。解释器开关的叫法在 ZhTable.switches(errexit → 出错就停),解析指令的叫法在 ZhTable.directives(syntax → 语法),并成一块的几条(TextBlock.group)按第一条的节点类型查 ZhTable.groups 读成一行字加一张表。阶段、健康检查、启动、分构建参数 / 环境变量的变量这些属性表块的行由引擎按 facts 排;要执行的命令(CommandForm)数组写法照常翻(交给命令规则),shell 写法问 SourceDocument.embedded,几步用「然后」(一步失败就停的)或分号连起来。
TypeScript 关键字表(流程 = keyword,声明 = storage):
| 流程 | 声明 / 字面量 |
|---|---|
import 导入、from 来自、as(导入 / 导出别名)作为 / (类型断言 x as T、as const)断言为、export 公共、default(export default)默认 / (switch 里的 default:)默认情况、if 如果、else 否则、for 遍历、of 取自、in(二元运算 'k' in obj)在 / (for…in 头部)取自、while 当、do 执行、switch 分支、case 情况、break 跳出、continue 继续、return 返回、throw 抛出、try 尝试、catch 捕获、finally 最终、await 等待、yield 产出 |
const 常量、let 变量、var 变量、function 函数、async 异步、class 类、extends(类 / 接口继承)继承 / (泛型约束 T extends X、infer U extends X)限定为 / (条件类型 A extends B ? … : …)符合、implements 实现、interface 接口、type 类型、enum 枚举、new 新建、instanceof 属于、typeof 类型为、keyof 键名、readonly 只读、public 公共、private 私有、protected 受保护、static 静态、abstract 抽象、delete 删除、true 真、false 假、null 空、undefined 未定义 |
TypeScript 内置类型:string 字符串、number 数值、boolean 布尔、any 任意、unknown 未知、never 永不、void 无、object 对象、bigint 大整数、symbol 符号。Promise 等库类型不翻。
Python 关键字:import 导入、from(导入)从 / (raise … from)源自 / (yield from)自、as(导入别名、with … as、except … as)作为、def 函数、async 异步、await 等待、class 类、return 返回、if 如果、elif 否则如果、else(if 后)否则 / (for / while 后,循环没被 break 才执行)未中断时 / (try 后,没出错才执行)未出错时、for 遍历、in(比较里)在 / (for 头部)取自(推导式里的 for / in 不逐词翻,整个重排,见「推导式」)、while 当、try 尝试、except 捕获、finally 最终、raise 抛出、with 使用、yield 产出、lambda 匿名函数、pass 跳过、break 跳出、continue 继续、global 全局、nonlocal 非局部、del 删除、assert 断言、not 非、and 并且、or 或者、is 是、not in 不在、is not 不是、None 空、True 真、False 假、match 匹配、case 情况。
Python 内置类型(仅注解里):str 字符串、int 整数、float 浮点数、bool 布尔、list 列表、dict 字典、set 集合、tuple 元组、bytes 字节串。
一行译文样例(必须逐字一致):
| 源码 | 译文 |
|---|---|
if (!folder) |
如果 (非 folder) |
return [] |
返回 空数组 |
for (const f of files) { |
遍历 (常量 f 取自 files) |
failed: { path: string, reason: string }[] |
failed: { path: 字符串, reason: 字符串 }[] |
return {⏎ locked: true,⏎ … }(issueLock.ts 20–24 行) |
返回 { locked: 真, prerequisiteNumber: prereq.number, prerequisiteColumn: prereq.column } |
err instanceof Error ? err.message : String(err) |
如果 err 属于 Error 则 err.message 否则 String(err) |
属性表样例(必须逐字一致;子行各列用 | 隔开):
| 源码 | 属性表 |
|---|---|
export async function findEnvFiles(workspaceRoot: string): Promise<string[]> {(envLock.ts 18–23 行,连同前面的 JSDoc) |
名称 findEnvFiles / 说明 Scan the workspace folder rooted at …(三行原文照留,按 Markdown 排成一段)/ 访问级别 公共 / 异步 是 / 输入参数 1 个:workspaceRoot | 字符串 | / 输出参数 Promise<字符串[]> |
async function chmodAll(workspaceRoot: string, mode: number): Promise<EnvLockResult> { |
名称 chmodAll / 访问级别 私有 / 异步 是 / 输入参数 2 个:workspaceRoot | 字符串、mode | 数值 / 输出参数 Promise<EnvLockResult> |
import { promises as fsp } from 'node:fs' |
来源 'node:fs' / 仅类型 否 / 导入项 1 个:promises 作为 fsp |
const ENV_PATTERN = '**/.env*' |
名称 ENV_PATTERN / 访问级别 私有 / 值 '**/.env*' |
const issue = issues.find(i => i.number === issueNumber) |
名称 issue / 初始值 issues.find(i => i.number 全等 issueNumber) |
def find_env_files(root: str) -> list[str]:(sample.py,连同下一行的 docstring) |
名称 find_env_files / 说明 Return every .env file under root. / 访问级别 公共 / 异步 否 / 输入参数 1 个:root | 字符串 / 输出参数 列表[字符串] |
from .schemas import (⏎ UserAvatarUploadResponse, … )(routes.py 19–23 行) |
来源 .schemas / 导入项 3 个:UserAvatarUploadResponse、UserDetailResponse、UserUpdateRequest |
payload = await file.read()(routes.py,函数体里) |
名称 payload / 初始值 等待 file.read() |
_default_port = 5680 if _role == "beat" else 5679 |
名称 _default_port / 访问级别 私有 / 初始值 如果 _role 等于 "beat" 则 5680 否则 5679 |
_CONTEST_STATUS_KEYS = {⏎ "published": "contests:publish", "ended": "contests:end", "draft": "contests:update" } |
名称 _CONTEST_STATUS_KEYS / 访问级别 私有 / 初始值 字典 3 项:"published" | "contests:publish"、"ended" | "contests:end"、"draft" | "contests:update" |
CONFIG = {"db": {"host": host, "opts": {}}, **defaults, 1: await load()} |
名称 CONFIG / 访问级别 公共 / 值 字典 3 项:"db" | 字典 2 项(下一层 "host" | host、"opts" | 空字典)、展开 | defaults、1 | 等待 load() |
const result: EnvLockResult = { total: files.length, ok: [], failed: [] }(envLock.ts 40 行) |
名称 result / 类型 EnvLockResult / 初始值 对象 3 项:total | files.length、ok | 空数组、failed | 空数组 |
export const handlers = { total: files.length, 'x-y': await load(), [key]: !ok, files, ...rest, async load(id: string): Promise<void> { … }, nested: { a: [], b: { c: 1 } } } |
名称 handlers / 访问级别 公共 / 值 对象 7 项:total | files.length、'x-y' | 等待 load()、[key] | 非 ok、files | files、展开 | rest、load | 异步 load(id: 字符串): Promise<无>、nested | 对象 2 项(下一层 a | 空数组、b | 对象 1 项,再下一层 c | 1) |
export abstract class Repository<K, T extends Entity = Entity> extends Base<string> implements Loader<T>, Disposable {(classes.ts 3–9 行,连同前面的 JSDoc 和 @Injectable()) |
名称 Repository / 说明 仓库基类。… / 访问级别 公共 / 抽象 是 / 装饰器 @Injectable() / 类型参数 K, T 限定为 Entity = Entity / 父类 Base<字符串> / 实现的接口 Loader<T>, Disposable |
export default class { |
名称 匿名 / 访问级别 公共 / 抽象 否 |
class Meta(Base, metaclass=ABCMeta):(classes.py,带 @dataclass(frozen=True)) |
名称 Meta / 访问级别 公共 / 装饰器 @dataclass(frozen=True) / 父类 Base, metaclass=ABCMeta |
class NotFound(AppResponse):(classes.py,连同 docstring) |
名称 NotFound / 说明 找不到资源。 / 访问级别 公共 / 父类 AppResponse |
预览数据与消息
扩展端把代码块树转成可序列化的视图数据发给 webview:
interface BlockViewBase {
id: string // 在树里的位置,例如 '6.1.0'
anchor: string // 块的位置记号(见「问 AI」):问答挂在它上面,webview 只比相等
kind: string // BlockKind(库规则认成的现成块也是,如 enum),或库规则卡片的种类 id(sqlalchemy.table)
startLine: number // 编辑范围的起止行,0 起:块的完整范围,头部连同各子孙块、结尾(见 blockSpans);库规则认领的块是原块整条语句
endLine: number
headerEndLine: number // 块自己那部分(头部)的最后一行,库规则认领的块是整条语句;问 AI 时贴外层块的开头用,webview 不用
code: string // 编辑范围的源码,去掉共同缩进
codeTokens: Segment[] // 源码着色(拼起来等于 code)
defines?: SourceRange // 块自己的名字(函数名、类名、只声明一个变量时的变量名)在源码里的范围,扩展端从 facts 取(库规则的块取 RuleBlock.defines);跳到的定义是它时闪整块
attributes: Record<string, string> // 筛选属性(含 kind),见「筛选」
badge?: KindBadge // 库规则的卡片才有:种类的名字、颜色、图标;webview 按它画图标、上色、默认折叠
children: BlockView[]
}
/**
* 属性表块(有 facts)和库规则认领的块只带 properties,其余块只带 translation(注释块是 `{ markdown }`);没有翻译插件时是空数组。
* 一行译文里有跨几行写的调用时 table 是摊开实参的表(TextTranslation.table),块带说明(TextBlock.doc)时 doc 是翻译插件给的 Markdown 原文(Translator.doc),
* webview 依次排在译文下面。
*/
type BlockView = BlockViewBase & ({ translation: PreviewText, table?: PreviewTable, doc?: Markdown } | { properties: PropertyRow[] })
- 扩展 → webview:
{ type: 'document', version, languageId, blocks: BlockView[], attributes: FileAttribute[], translators: { id, label }[], translatorId, error?: string, reveal?: Reveal, names: ShownName[] },文档每次变化(防抖 150ms)重发。names是这个版本眼下已知的中文名({ offset, length, name, definition? }:源码下标 [offset, offset + length) 的名字显示成 name,definition 是定义处本身),见「中文名」。attributes是这份文件里出现过的筛选属性,按筛选栏的先后排:{ id, name, values: { id, name, count }[] },名字取翻译插件的attributeNames,没有的用 id。reveal = { line, definition? }只随新预览的第一份文档走:从文本编辑器切过来时是它顶部那一行(滚到包含这行的块、顶到顶部,折叠的不展开);跳到定义时还带definition(定义处名字在源码里的下标),按「跳到定义」一节展开、滚动、闪。{ type: 'reveal', line, definition? }:同上,发给已经开着的预览。页面正在重新加载(标签页切回来)时收不到,扩展端改成随它ready后的第一份文档带过去。{ type: 'definitionProbed', id, found }:回答第id次probeDefinition。{ type: 'occurrencesFound', id, occurrences: { start, end, kind: 'text' | 'read' | 'write' }[] }:回答第id次findOccurrences,[start, end)是源码下标,kind同DocumentHighlightKind;语言扩展没答就是空数组。{ type: 'moveOccurrence', step: 1 | -1 }:两条命令发来,跳到下一处 / 上一处出现。{ type: 'displayNames', version, names: ShownName[] }:version 这份文档里显示成中文名的名字,查到一步发一步;页面只收自己眼下这个版本的。{ type: 'filter', excluded: Record<属性 id, 值 id[]>, onlyChanges }:整个工作区共用的块筛选和「只看改动」开关(见「代码差异」)。webview 发ready后先于第一份文档发一次(第一份文档按筛选画),之后任何一个预览改了都发给所有开着的预览(包括改的那个)。{ type: 'diff', diff: DocumentDiff | null }:这份文件相比对比基准改了什么(见「代码差异」),每发一份文档之后算好了补发,换了筛选、基准挪了再发;null是不比了。DocumentDiff是{ base, version, newFile?, blocks: Record<块记号, { change: 'add' | 'mod', outermost?, own?, marks? }>, removed: { parent, after?, block, line, kindName }[], stats: { add, mod, del }, lineCount }:块按记号认;marks是卡片里要标的(属性表各行、子行、一行译文、摊开实参的表、说明的增删,行和子行按下标,只在页面上正是version这一版时画);删掉的块block是旧版本的视图数据(只带它自己的内容、词不带下标),parent/after是它挂在哪块下面、排在哪块后面(记号),line是它在新文件里的位置。{ type: 'qa', available, threads: { anchor, entries: { id, question, answer, createdAt, profile?, images? }[] }[], running: { askId, anchor, question, text, activity? }[] }:这份文件的问答(见「问 AI」)。ready后发一次,存的问答、正在答的问题变了再发给开着这份文件的所有预览;available为假时文件不在工作区文件夹里,不给「问 AI」;images是问题带的图片在预览里能直接用的地址(webview.asWebviewUri);running让重新加载的页面接上还在答的问题(不带图片,接上的这一问不显示缩略图,答完回到历史里照常有)。{ type: 'askProfiles', options: { id, label }[], selected }:「问 AI」的 profile 下拉(id是 profile 名,「默认」的id是''、label是「默认」)。ready后发一次(每次现读 profile 目录),任何一个预览选了都发给所有开着的预览(包括选的那个)。{ type: 'answer', askId, text, activity? }:askId那个问题的回答长了(text是到现在为止的全文)或换了在做的事。webview 攒着、最多每 80ms 画一次。{ type: 'answerEnd', askId, status: 'done' | 'failed' | 'stopped', text?, error?, entryId? }:答完了。done带最终回答和存下的那条的 id(历史里不再重复显示它),failed带给人看的原因,stopped是被叫停、没存。
- webview → 扩展:
{ type: 'probeDefinition', id, version, offset }:按住修饰键指到带offset的名字上时问有没有定义,id每问一次加一;version不是当前文档版本就不答。{ type: 'goToDefinition', version, offset }:修饰键 + 点击,按「跳到定义」一节跳。这两条都只带下标:扩展端按发出去的文档认出规则标了引用的下标交给规则查(见「规则自己查定义」),其余问语言扩展。{ type: 'findOccurrences', id, version, offset }:单击带offset的名字,问它在这份文档里的各处出现;id每问一次加一,version不是当前文档版本就不答。{ type: 'occurrenceNavigation', available }:眼下能不能用键盘在各处出现之间跳,页面加载后报一次、之后变了才报;扩展端按它设superpowers.codeReader.hasOccurrences。{ type: 'ready' }{ type: 'edit', startLine, endLine, text }:用WorkspaceEdit把这几行整行替换成text(每行补回原来第一行的缩进)。{ type: 'selectTranslator', id }:按languageId记到globalState,重发文档。{ type: 'setFilter', excluded }:改块筛选(整份替换);扩展端逐项核对后记进workspaceState,再发filter给所有预览。webview 不在本地先改,只按扩展端发来的显示。{ type: 'setOnlyChanges', on }:打开 / 关掉「只看改动」;扩展端记进workspaceState(superpowers.codeReader.onlyChanges),再发filter给所有预览。{ type: 'ask', askId, anchor, question, images: { mediaType, base64 }[] }:问anchor那块;askId由 webview 起;images是贴进输入框的图片(没贴是空的,base64不带data:前缀)。扩展端按最近一次发出去的块树和源码找这块、拼问题,这块存过问答就接着最近那条的会话追问(见「问 AI」的追问),用扩展端记着的 profile;找不到(代码刚改过)、图片格式不认或问不了(这块正在答、同时答的太多)时直接回answerEndfailed。{ type: 'stopAsk', askId }:叫停,结束 claude 进程,这次不存。{ type: 'deleteQa', anchor, entryId }:删掉一次问答。{ type: 'selectAskProfile', id }:「问 AI」换 profile;扩展端记进globalState后发askProfiles给所有预览。webview 不在本地先改,只按扩展端发来的显示。
- 块的编辑状态和用户的折叠选择在 webview 里按块 id 记(
vscode.setState):{ editing: string[], folds: Record<id, boolean>, scroll? }。folds只记用户点过的(id → 折叠着没有),属性行和带下一层的子行也记在里面:属性行 id 是<块 id>/p<行序号>(例如6.1.0/p3),子行 id 是上一层(行或子行)的 id 加/<子行序号>,一层层往后接(例如6.1.0/p3/1是第 3 行的第 1 个子行,6.1.0/p3/1/0是它下面的第 0 个);块 id 只有数字和点,只有行那一节带p、其余各节是纯数字,所以块、行、各层子行的 id 互不相撞。子行 id 也是跳到定义时在页面上找要闪的那一项用的。没记的按默认:函数、类、枚举、异常块和库规则的卡片折叠,其余块、属性行和子行展开。眼下折叠着哪些由「默认 + folds」算出来,界面、跳到定义的展开都只看这一份。跳到定义展开祖先块、属性行时也记进folds(记成展开)。读回来的状态逐项核对,旧版本存的、形状不对的项当没存过。文档刷新后保留;正在编辑且有焦点的块,刷新时不覆盖输入框内容。
颜色
词性(暗色取 VS Code Dark+,亮色取 Light+):
| 词性 | 暗色 | 亮色 |
|---|---|---|
| keyword | #c586c0 | #af00db |
| storage / operator | #569cd6 | #0000ff |
| variable | #9cdcfe | #001080 |
| function | #dcdcaa | #795e26 |
| type | #4ec9b0 | #267f99 |
| string | #ce9178 | #a31515 |
| number / literal | #b5cea8 | #098658 |
| comment | #6a9955 | #008000 |
| hint | --vscode-descriptionForeground |
同左 |
| constant | #4fc1ff | #0070c1 |
| plain | --vscode-editor-foreground |
同左 |
属性表里:说明按 comment,访问级别(公共 / 私有 / 受保护)和「作为」按 keyword,是 / 否按 storage,函数名称按 function,类名称按 type,未标注、无、N 个、字典 N 项、对象 N 项、展开、默认导出、全部、整个模块、匿名按 hint;输入参数表格的列名是界面文字(descriptionForeground),不是着色的词。推导式框里的「遍历」「的每个」「仅当」「取」「异步遍历」按 keyword,框头按「循环」块的种类色。父类名(TypeScript extends 后面、JavaScript class_heritage 里、Python 类名后面括号里直接写的名字)源码着色和属性表里都按 type。
带自己颜色的段(Segment.color,库规则按库的配色给,例如 FastAPI 的 HTTP 方法、成功的状态码):暗色、亮色两个写在元素的 style 上(--color-dark / --color-light),.cr-colored 按主题挑一个,盖过词性的颜色;词性照旧报(role)。
种类(种类图标的颜色 + 块底 7% + 边框 35%;亮色主题同色系;图标见「预览(代码块树)」):import 导入 #c586c0;function 函数 #dcdcaa;class 类、type 类型 #4ec9b0;enum 枚举 #b180d7(亮色 #652d90);exception 异常 #f14c4c(亮色 #cd3131,和尝试、捕获、最终同一色系,更红一些);field 字段、const 常量、variable 变量 #9cdcfe;if 条件、else 否则 #ce9178;loop 循环 #8f9cff;try 尝试、catch 捕获、finally 最终 #f48771;return 返回 #89d185;statement 语句 #9d9d9d;comment 注释 #6a9955(虚线框);Shell 的 interpreter 解释器同语句 #9d9d9d,options 选项 #e5c07b(亮色 #9a6a00),case 分情况同条件 #ce9178,command 命令、pipeline 管道 #d7ba7d(亮色 #7c5f12);Dockerfile 的 directive 解析指令同语句,stage 阶段 #56b6c2(亮色 #127f8c),run 执行同命令,copy 复制同导入,setting 设置同选项,label 标签同变量,health 健康检查同尝试 / 捕获,start 启动同返回(原型 DOCKER_KINDS 的颜色);库规则的种类用规则给的颜色(badge.color 的 dark / light,webview 写在块的 style 上按主题挑),内置 SQLAlchemy 数据表 #e8a860 / #a3590d,Celery 应用 #98c379 / #4d7f17,定时任务 #56b6c2 / #127f8c,数据类 #ef7fb0 / #b3306c,FastAPI 接口 #61afef / #1a6fb5,FastAPI 路由器 #7aa2f7 / #34548a。编辑状态:底色 --vscode-editor-background,边框 --vscode-focusBorder。
代码位置
vscode/src/codeReader/
contract.ts 通用契约
registry.ts 注册表 + CodeReaderApi
runtime.ts web-tree-sitter 初始化、语法包加载、解析
blockView.ts CodeBlock → BlockView(源码着色、去缩进、调用翻译);嵌着的代码:解析那几段、分步、换那种语言的翻译插件转子块、着色叠一层(EmbeddedPart)
attributes.ts 块的筛选属性(从 facts 推出通用属性 + 语言自己报的)、文件里出现过的属性和块数
libraryRules.ts 库规则的宿主部分:按导入启用、按注册顺序认领、出错退回、查规则标的引用
workspaceIndex.ts 工作区索引(库规则的 WorkspaceIndex,和宿主自己的中文名索引)的宿主部分:建、缓存、跟着文件改动更新,查到的项带着认的时候给的东西(不碰 VS Code)
nameResolution.ts 中文名落到预览里哪些名字上:本文件问各处出现、别的文件先查中文名索引再问定义,按文档版本查一次、没答上再查、跟着改动挪位置(不碰 VS Code)
workspaceFiles.ts 工作区索引在 VS Code 里怎么找文件、读文件、盯改动(排除规则)
workspaceScope.ts 工作区索引还要跳过的文件:嵌套的 checkout、忽略文件忽略的(不碰 VS Code)
rules/sqlalchemy.ts 内置 SQLAlchemy 库规则(只 import contract 和同目录的共用代码)
rules/celery.ts 内置 Celery 库规则:两张卡片、频率换算、任务名索引
rules/enum.ts 内置 enum 库规则(Python):枚举类认成枚举块,成员照 Python 的规则认
rules/dataclass.ts 内置 dataclasses 库规则(Python):数据类卡片,字段照 dataclasses 的规则认
rules/fastapi.ts 内置 FastAPI 库规则(Python):路由器卡片(接口表)、接口卡片(请求表、响应表),参数照 FastAPI 的规则放,异常类的状态码、类型别名、枚举经索引查
commandRules.ts 命令规则的宿主部分:命令名 → 规则(先注册的算),规则出错记下、当它没读
commands/reading.ts 命令规则共用的:认选项(连写拆开、带值的、长选项、--)、拼译文(动作、括号里的意思、照抄不认识的选项)
commands/systemCommands.ts 内置命令规则:新建用户 / 用户组(useradd、adduser、addgroup / groupadd)、gpg、install、chown,管 '*' 的只问版本的命令
commands/packageCommands.ts 内置命令规则:apt-get / apt、apk、pip / pip3、npm、pnpm、yarn、corepack、uv(sync、pip、run……)、uvicorn
commands/shellCommands.ts 内置 shell 命令规则(rm、mkdir、curl、tar、unzip、find、cp、mv、echo / printf、exit、cd、pwd、basename、dirname、readlink、shasum、cut、du、cat、grep、sed、chmod、ln、touch、source、read、sleep、continue / break……)
localSymbols.ts 语言插件认的同一文件里的名字怎么用:盖住位置的名字、跳到哪处定义、各处出现(不碰 VS Code)
ruleLookups.ts 库规则认领时查别的文件里的定义(RuleContext.lookup):索引 + 语言扩展查定义(不碰 VS Code)
calleeKinds.ts 调用的名字定义成哪种块(SourceDocument.calleeKind):按被调用的表达式原文记、问最后那个名字(不碰 VS Code)
deferredAnswers.ts 上面两样共用的:排视图数据时先交出已知的、记下问了什么,发出预览后按版本查一次、没答的隔一会儿再查,变了叫宿主重发(不碰 VS Code)
(blockView.ts 的 readDefinitionKind:定义处落在哪块自己的名字上;editor/definitions.ts 的 definitionKind:打开定义所在的文件交给它)
rules/pythonTree.ts Python 库规则共用的读语法树的办法(名字最后一节、字符串内容、调用的参数、别名、注解里的 X[...]、库里的名字)
rules/segments.ts 库规则共用的拼属性表文字的办法
rules/children.ts 库规则共用的挑子块的办法(成员连同紧挨在它前面的注释块、成员的说明节点)
languages/shared.ts 语言插件共用的建树工具;异常类(withExceptions:按整份文件认异常类,类块换成异常块,并类属性、取构造参数)
languages/typescript.ts (call:调用的结构,new 是新建;TYPESCRIPT_EXCEPTIONS:认异常类的父类名、类字段、constructor)
languages/python.ts (iteration:推导式的结构;call:调用的结构;PYTHON_EXCEPTIONS:自带的异常类、类属性、__init__)
languages/displayNames.ts 中文名的共用部分:第一小句、行尾记号、按优先级汇总
languages/pythonNames.ts Python 的中文名写法(Annotated、@Comment、行尾记号、docstring)
languages/bash.ts bash / sh / zsh(shellscript):代码块树、facts(解释器、选项、按位置的参数、输出、返回)、command(命令的结构、重定向)、symbols(同一文件里的函数、变量)
languages/bashNames.ts Shell 的中文名写法(行尾 # @Comment)
languages/dockerfile.ts Dockerfile:代码块树(阶段、并块)、facts(解析指令、阶段、健康检查、启动、分作用范围的变量)、embedded(RUN 这些 shell 在哪几段)、command(数组写法的命令)、symbols(阶段名、变量)、中文名
languages/typescriptNames.ts TypeScript / JavaScript 的中文名写法(@Comment 装饰器、行尾记号、JSDoc)
translators/zhTemplate.ts 模板直译引擎(按语言的表驱动,词性、遍历结构从 SourceDocument.language 取;推导式排进框见 iterationGroup)
translators/typescriptZh.ts TS 表
translators/pythonZh.ts Python 表(含注解位置的判断)
translators/bashZh.ts Shell 表:节点读法(展开、特殊变量、条件、通配、&& / ||、管道、遍历、分情况)、开关的叫法
translators/dockerfileZh.ts Dockerfile 表:各条指令的读法(RUN 问嵌着的 shell 读出来的样子)、并块的读法(COPY、LABEL)、解析指令的叫法
explorer/fileTree.ts 侧栏文件树(FileTreeDecorator:给行添信息的接口;showChanges:审查工单时只摆 PR 改过的文件)
explorer/copyText.ts 复制菜单的命令和复制出什么(纯函数)
explorer/copyCommands.ts 复制命令:写剪贴板、状态栏提示
review/changedFiles.ts 改动状态 → M / A / D、颜色,PR 改过的文件 → 按目录分好的树(纯函数)
review/reviewRules.ts 能审查的列(「审查」「进行中」)、弹框列哪些工单、PR 号、能不能审查、标题栏的字(纯函数)
review/pickList.ts 选工单弹框的各项:先摆出来、文件数读到一个换一个、还转不转圈、每项的字(纯函数)
review/reviewSource.ts 看板数据:弹框列的工单(看板上摆着的)、重读单张工单、PR、PR 改过的文件
(panel/kanbanSnapshot.ts 的 boardIssues:看板上现在摆着的工单——这次加载的,否则存下的快照;KanbanWebviewPanel.issuesOnBoard 接上面板和 workspaceState)
review/reviewWorktree.ts 本机这张工单的工作副本在哪、备好 PR 最新的代码(已有的快进、没有的建)
review/changeDecorations.ts FileDecorationProvider:审查时给改过的文件上色、挂徽标
review/reviewMode.ts ReviewMode:选工单弹框、进出审查、刷新、按工作区记住,审查时预览对比什么(diffBase)
uncommitted/uncommittedMode.ts UncommittedMode:「未提交的更改」进出、盯 git 状态重摆,开着时预览对比上一次提交(diffBase);纯函数在 uncommittedFiles.ts
explorer/treeFilters.ts TreeFilters:侧栏的几种筛选同时只开一种,开着的那种定预览对比什么,换了筛选、基准变了通知预览
diff/diffBase.ts DiffBase:对比基准(文案、旧内容怎么取、缓存用的 key、基准里没有这个文件时的说明)
diff/blockDiff.ts 两棵块树比:兄弟块对齐(名字 / 最长公共子序列 / 相似度)、新增修改删除、删掉的块放哪、统计(纯函数)
diff/viewDiff.ts 一块卡片里的内容比:属性表按属性名、子行按第一列、着色的词逐截、Markdown 逐词;删掉的块只留自己的内容、去掉源码下标(纯函数)
diff/previewDiff.ts PreviewDiff:一个预览的差异,发完文档再比、旧内容按 key 记着、只发最后一次的结果;DocumentDiff 消息的形状(不碰 VS Code)
(git/fileAt.ts 的 fileAtRef:某个提交上某个文件的内容,没有这个文件是 null)
(gitea/repoContext.ts 的 resolveRepoContext:当前工作区的 Gitea 仓库和 token,看板「改动」tab 共用;git/worktree.ts 的 fetchPullRequestHead / addDetachedWorktree / fastForwardWorktree:拉 PR 的提交、建不挂分支的 worktree、不丢东西地快进)
pathMap.ts 按文件 / 目录记东西的表(文件注释、问答共用):路径的键、改名 / 挪位置 / 删除时怎么改、.spx 里 JSON 的读写、条目带着的文件哪些不再有人带、删文件(不碰 VS Code)
folderStore.ts FolderStore:每个工作区文件夹一份表,内存里的一份、文件监听、跟着改名 / 删除走、读写排队、改表后删掉不再有条目带着的文件
notes/noteMap.ts 保存一条注释(纯函数)
notes/store.ts .spx/code-notes.json 叫什么、每条怎么认
notes/noteItem.ts 有注释的行怎么显示:描述、悬停提示、contextValue(纯函数)
notes/fileNotes.ts FileNotes:注释的 FolderStore、给树画行
notes/commands.ts 「添加注释 / 编辑注释」输入框
qa/anchor.ts 块的位置记号(BlockAnchors)(不碰 VS Code)
qa/qaMap.ts .spx/code-qa.json 的形状、每条怎么认、记一次 / 删一次、再问时接着哪个会话(纯函数)
qa/prompt.ts 问 AI 的话:找到那块、外层块的开头、带行号的代码、追问
qa/answerStream.ts stream-json 的每行 → 会话 id、逐字的回答、在做的事、最终回答 / 失败
qa/claudeAsk.ts 命令行参数(只读、选的 profile)、问一次(问题连同图片从 stdin 发,追问找不到会话时重新问)、失败给人看的话
qa/qaImages.ts 问题带的图片:认哪些格式、存到 .spx/code-qa/ 叫什么名字、写文件(不碰 VS Code)
qa/askProfiles.ts AskProfiles:问 AI 用哪个 profile,下拉的选项、记在 globalState、通知所有预览(不碰 VS Code)
qa/asks.ts QaAsks:正在答的问题,一块一个、最多 3 个、能叫停(不碰 VS Code)
qa/codeQa.ts CodeQa:问答的 FolderStore、开问(问过的块接着最近的会话,用选的 profile,带贴的图)、答完存(带 profile 名、图片)、删,发给预览的问答、图片目录
(cc/spawnClaude.ts 的 runClaudeLines:跑 claude、给了 input 就写进 stdin、stdout 逐行交出、超时、叫停,问 AI 用它;buildStreamJsonLine 拼 stream-json 的一条用户消息)
editor/previewEditor.ts CustomTextEditorProvider
editor/messages.ts 消息类型
editor/blockFilter.ts 块筛选和「只看改动」开关:workspaceState 读写、通知所有预览
editor/definitions.ts 查定义(vscode.executeDefinitionProvider)、规则查到的位置换成 VS Code 的、几处定义的挑选框
editor/definitionSources.ts 名字的定义去哪儿查:文档里规则标的引用问规则,其余问语言扩展
editor/definitionAction.ts 查到的定义 → 怎么跳(纯函数)
editor/occurrences.ts 查名字的各处出现(vscode.executeDocumentHighlights)
editor/occurrenceNavigation.ts 跳到下一处 / 上一处高亮:命令、上下文键
index.ts registerCodeReader(context): CodeReaderApi
vscode/webview-ui/code-reader.html + src/codeReader/ 预览界面(React,Vite 第二个入口;块右上角的图标按钮在 BlockTools.tsx,悬停提示在 src/components/ui/tooltip.tsx(和看板共用,页面根上挂一次),块的种类图标(这组图标名 → 图标组件、内置种类 → 图标名,都只在这一处)在 kindIcons.tsx,属性表在 PropertyGrid.tsx,子行排成表格还是列表在 subRows.ts,着色的词和框在 SegmentText.tsx,中文名换字在 displayNames.ts,Markdown 渲染在 markdown.ts / TextPreview.tsx,筛选栏在 FilterBar.tsx,筛选规则在 filter.ts,编辑时盖住哪些块、点「编辑」退出哪些块在 editing.ts,高亮各处出现的规则在 occurrences.ts、接到页面上在 useOccurrences.ts,问 AI 的问答区在 AskThread.tsx、状态和规则在 qa.ts、贴图拖图的规则在 qaImages.ts、收发在 useQa.ts;代码差异:页面上怎么标(块 id、删掉的块挂哪、只看改动折哪几段、各处改动、上一处 / 下一处跳到哪)在 diffView.ts,筛选栏第二行和新文件横幅在 DiffBar.tsx,卡片里的增删在 DiffText.tsx(Markdown 的增删在 markdown.ts 的 renderMarkdownDiff),总览条在 OverviewRuler.tsx,跳到一处改动在 useChangeNav.ts)
测试
- 语言插件:
envLock.ts、issueLock.ts的代码块树(种类、header 行号、层级)与原型CODE_TREE一致,只是紧贴函数的 JSDoc 算进了函数头部;Python 样例覆盖 import / def / docstring / if-elif-else / for / try-except / with / 推导式;函数的说明:JSDoc 各种挂法(export、默认导出、方法隔着装饰器、函数值的变量、嵌套函数)和不挂的情况,docstring 连同它前面的注释进头部。类:classes.ts、classes.py覆盖 export + 抽象 + 泛型 + extends / implements + 装饰器 + JSDoc、匿名默认导出、赋给变量 / 类字段的类表达式、函数里的类、类里的类、docstring、metaclass 关键字参数、下划线私有。 - 编辑范围:各样例里每块的编辑范围盖住它的子孙、兄弟块之间最多共用一行、首尾不是空行,「编辑」写回原样不变;父块的编辑框是整块代码(TSX 组件连 JSDoc、函数体、收尾括号),子块只有它自己连同子孙;展平的 else / elif / catch 各到下一块开头之前,Shell 的
fi归最后一块;函数、类说明所在的行在函数、类块的头部里。 - 折叠:默认函数、类、枚举、异常块和库规则的卡片折叠、其余展开;用户的选择盖过默认、重新加载后还在(嵌套子行的也是);各层子行的 id 互不相撞;旧版本存的状态不报错;跳到定义时默认折叠的函数、类也会展开。
- 属性表信息:
envLock.ts、sample.py、routes.py(原型里的 FastAPI 路由)加内联片段,覆盖各种导入写法、形参写法、类成员访问修饰符、解构和多变量声明、Python 下划线规则;字典 / 对象字面量的各项:Python 字符串键、名字键、嵌套字典、**展开、空字典、类属性,TypeScript 名字键、字符串键、计算属性名、简写、展开、方法和 getter(头部到主体之前)、嵌套对象、空对象,不是字面量的值没有 entries。 - 字典 / 对象的属性表:项数、键 | 值两列、嵌套一层层展开、「展开」、方法头部、空的一行;键和值里的名字、字符串带下标(嵌套的也是);每项标的定义是键,展开项不标;跳到定义落在对象属性名上只闪那一项、连外层各层一起展开,只是显示着的名字闪整块、展开到它那一层。
- 推导式:Python 语言插件读出的结构(调用唯一参数的生成器带 sharedParentheses、几个参数之一的不带,列表带条件,字典分出键和值,集合的解构变量,几节 for / if 照源码顺序,async for,套着的各自认,字面量和 for 语句不算);中文直译「推导式」一节的样例表逐字一致,框的样式、框头、关键字着色,顿号连写、条件位置、另起一句、异步遍历、集合、框里的框、多个参数旁边的生成器、跨行带注释的生成器,属性表的默认值和字典项的值里也换;框里的名字带下标且指回源码原文,高亮各处出现、跳到定义都认框里的名字(只显示在框里的推导式变量 → 框所在的块);TypeScript 不认。
- 调用与构造:语言插件读出的结构(Python 按位置 / 按名字 /
*x/**x、唯一的生成器实参、被调用的不是名字时没有 name、装饰器和父类列表不算;TypeScriptnew是新建、被调用的不含 new、...x、带标签的模板字符串和不带括号的new X不算);中文直译按「调用与构造」一节的样例:routes.py的return UserDetailResponse(…)定义是类(异常也算)时「返回 新建 UserDetailResponse · 11 个参数」、11 行表、名字带下标,没查到 / 查不到 / 是函数时「调用」、名字按函数着色;update_user的初始值「等待 调用 user_service.update_user · 10 个参数」、表是这一行的子行(列名 参数 / 值)、第一行「第 1 个」按说明词着色;一行写完的get_user(...)、file.read()照旧一行、不问定义;「展开」「展开关键字」、「第 N 个」只数按位置传的;fixtures/calls.ts.txt(本仓库的代码)new 是新建、不带 new 是调用、都不问定义;链式调用跨了几行、回调主体是子块的调用不排。嵌套:paymentConsoleRoutes.py的 15 行表逐字一致,credentials_present那一格套 4 行表、CredentialPresence同样问定义、按类型着色、带下标,bool(…)\n and Path(…).is_file()照旧一行;自造的三层(Outer → Middle → Leaf、带 await 的 fetch)一层层套、每层都问定义;chainedCalls.py的(调用 base64.b64encode · 1 个参数).decode()、被调用的表达式里的调用照常一行(圆括号贴着内容),[0]、or后面跟着时括起来、前面有东西或后面是逗号时不括,实参值里的X(\n…\n).model_dump()也括起来、表套在格子里;nestedCalls.ts.txt三个commands.registerCommand(…)各套一张表,new Server(new Options(…))套 new。calleeKinds.test.ts:按表达式原文记、同一版本不再查、被调用的不是名字不问、查了没有不算变、没答的隔一会儿只再查没答的。视图数据带table、readDefinitionKind认类 / 函数 / 异常 / 变量、renderedNames算上表里(连套着的表)的名字、documentMessage把 calleeKind 交给翻译插件;webview 跳到定义、键盘跳到高亮的各处出现都认一行译文下面那张表和值那一格里套着的表(套在变量初始值那一行下面的展开那一行)。 - 只经契约:只照 contract 写的假语言插件(
iteration把一个普通调用认成遍历)配中文模板,那个调用换成遍历框;同一个翻译插件配上不给iteration的语言插件照常翻。测试文件不 import 任何语言插件模块。 - 输入参数表格:函数的「输入参数」行带列名 名称 / 类型 / 默认值(TS、Python 一样),默认值格子只有译文、没有就是空格子,别的行没有列名;webview 按有没有列名选表格还是列表,表格的格子按列数取,列宽模板(
tableTemplate)只看列数。 - 翻译:上面两张样例表逐字一致;属性名不被翻;每个被翻的词单独成段;节点模板嵌套时正确、槽位里照样翻、没命中模板的节点照抄;属性表里的值和一行译文用同一套表和模板。
blockView:codeTokens拼起来等于code;去缩进正确;函数、类、导入、常量、变量块只带属性表,其余块只带译文。- 注释的 Markdown:注释块、函数和类「说明」的视图数据是
{ markdown },原文照抄(reST 角色、双反引号、JSDoc 标签、HTML 都不转换),行注释连成的块留得住缩进四格的代码块;渲染时 HTML 当文字、带协议的网址和邮箱成链接、不带协议的文件名不成链接,javascript:/command:/ 相对路径的链接和图片不渲染;跳到定义不认 Markdown 里的名字。 - 行替换:多行块替换后缩进正确。
- 跳到定义:TS / Python 译文和属性表里的名字、点分模块名每一节、模块字符串带下标且指回源码原文,关键字、运算符、说明词不带(前面有中文注释也对得上);查到的定义 → 不动 / 原地 / 预览 / 普通编辑器 / 挑选框;webview 里按下标找目标块、要展开的祖先块和属性行,滚动位置。闪哪一处(TS、Python 源码走完整的语言插件 + 翻译插件 + 视图数据):参数 → 它那个子行,导入项 → 它那一项,多变量声明里的一个 → 它的名称行,类的泛型参数 → 「类型参数」那一行,函数名 / 类名 / 单个变量名(含解构模式)→ 整块,循环变量 → 所在的块,翻译插件没填
defines→ 整块(照样展开属性行);翻译插件给参数、导入项、声明的变量、泛型参数标的范围指回源码原文;块自己的名字只有函数、类和单变量声明有。 - 高亮各处出现:段的源码范围和出现重叠才亮、边界相接不算、没下标的不亮,讲别处名字的段(
refers)按那个名字算、自己照抄的位置不算(occurrences.test.ts的sourceOf),点分模块名每一节各按自己的范围认(真实的 Python 译文和属性表:定义处按写入、用到处按读取);写入 → 强高亮色,读取、按文字 → 普通高亮色,同时碰上按写入;单击名字问、点空白处清、点按钮 / 链接 / 筛选栏不动、名字在按钮里照样问,拖选、双击第二下、带修饰键的点击不动,⌘ / Ctrl + 点击照旧发跳到定义、不问出现处;回答连同名字显示、空回答不亮、换名字时回答到前旧的留着、迟到的回答丢掉、答过「没有」再点照样再问、清掉后才到的回答丢掉、换文档版本或翻译清掉而同版本同翻译重发不清、没有文档时不问。键盘跳(occurrences.test.ts、reveal.test.ts、editor/occurrenceNavigation.test.ts):回答照源码先后排、点中的那处是当前那一处、「2/5」和「N 处」;往后 / 往前、到头接着跳,找不到的跳过、总数照旧、一处都找不到不动,点中的不在回答里从点中的地方数起;只有当前那一处描框;能不能跳(高亮、焦点、输入框)第一次必报、之后变了才报;页面上找第一处显示着它的地方、要展开的块 / 属性行 / 子行、跳过筛掉 / 编辑 / 盖住的块、Markdown 不算;package.json的两条命令、默认键、when、命令面板,扩展和页面的代码里不写键、不拦键,上下文键有一个预览能跳就真、变了才设,命令发给能跳的预览、没有就发给活动的;按键那一刻的位置跳转时回一次、隔太久 / 点过鼠标 / 没按过键不回,跳完守住跳到的位置、过了KEY_SCROLL_MS或又按键、点鼠标不守。 - 中文名:
- 语言插件认写法(
languages/pythonNames.test.ts、languages/typescriptNames.test.ts,真实解析):Python 注解Annotated(typing.Annotated、t.Comment,f-string、别的调用不算)、装饰器(函数、类)、行尾记号(多行参数表、*args、self.x、变量、字段、枚举成员,记号前面有别的字,记号的范围)、单独成行 / 那一行没有定义的不算、函数那一行给参数、docstring 的谷歌写法和 Sphinx 写法(带类型、星号、续行、别的段落不算)、第一小句和 12 个字的限制、明写的记号盖过说明文字、没取的记号照样去掉;TypeScript 装饰器(类、@d export class、字段、构造函数和方法的参数、方法隔着别的装饰器、名字最后一节)、行尾//记号(参数、变量、枚举成员、类字段、接口属性,块注释不算)、JSDoc@param(类型、短横、可选、属性不算、export 函数、方法、箭头函数变量、类字段)、JavaScript 默认值参数;预筛和各方言的文件。 - 记号不是说明(
nameResolution.test.ts,走readBlockViews):Python、TypeScript 枚举成员的行尾记号去掉、前面的字照留、只有记号的没有说明列。 - 落到哪些名字上(假的语言扩展):画出来的名字按下标认、本文件的定义处标 definition、长度对不上 / 没画出来的不换;别的文件的只问原名在索引里的、定义处要对上、本文件的索引项不算;语言扩展没答上算没查完;先交本文件的再交全部。工作区中文名索引(内存里的工作区):按原名查出位置和中文名、没写中文名的文件照常读但不认、
lookup照旧只给位置、一种 languageId 一份。 - 跟着版本走:每个版本查一次、手上有挪过来的就不先交一半、查到一半文档又改了丢掉、跟着改动挪位置(前面的挪、碰到的丢、后面的不动)、没答上照间隔再查到次数为止。
- 页面(
webview-ui/.../displayNames.test.tsx,渲染成 HTML):只收这个版本的、下标和长度都对上才换、用到的地方写中文名和「原名 x」悬停提示、下标照旧、定义处后面淡淡的原名、框里也换、单击时筛选栏写中文名且当前那一处按下标认。 - 真 VS Code(Pylance):routes.py 的条件读成「如果 当前用户.role 不等于 UserRole.管理员」(管理员来自 models.py),参数表「当前用户 current_user」,docstring 兜底的「要删的那一项的编号」,单击高亮、⌘↓ 跳、⌘ 点击「管理员」跳进 models.py 的预览(「管理员 ADMIN」,没有说明列),改文档时中文名一帧都没少。
- 语言插件认写法(
- Shell:
- 语言插件(
languages/bash.test.ts,真实解析):fixtures/fetchRuntime.sh的代码块树(种类、行号、层级,同原型SHELL_BLOCKS,原型略去的行也在);elif / else 展平、while / for 的主体是子块、条件串和命令串;解释器(env、写死的路径)、选项(连写拆开、-o、+、set --不是)、变量(local 一句几个、没值的、readonly 和顶层全大写写死的是常量)、函数(按位置的参数和名字、打印的几句、被$(…)拿去用、写了的 return);命令的词(写死的文字、有展开的没有)和重定向;同一文件里的名字(函数、赋值、local、for、read、展开,第几个参数不算);行尾# @Comment。 - 中文直译(
translators/bashZh.test.ts,走readBlockViews):fetchRuntime.sh整份逐行同FETCH_READING(即上面「Shell 脚本」的样例);展开(长度、去头去尾、截取、没设时、替换、特殊变量)、条件、重定向、命令规则(不认识的选项照抄、没有规则的照抄)、&&/||、遍历、$(dirname "$0")、常量;字符串色 / 变量色 / 常量色;规则出错照抄命令并报告。 - 宿主:命令规则按语言、注册顺序给(
registry.test.ts),shellscript 的 document 消息带命令规则读的译文、shell 的种类名、同一文件里的名字(editor/documentMessage.test.ts),跳到之前最近的定义、定义处算写入(localSymbols.test.ts)。
- 语言插件(
- Dockerfile:
- 语言插件(
languages/dockerfile.test.ts,真实解析):两份 luffy 的 Dockerfile 的代码块树(同原型,后端的三行注释是阶段说明);并块(紧挨着才并,空行、注释、别的指令隔开,COPY 要从同一个地方,ADD、heredoc 的 COPY 不并,第一个 FROM 之前的是顶层);ONBUILD 和各种设置;facts(解析指令、阶段的名称 / 序号 / 说明 / 标签 / 摘要 / 平台 / 接着前面的阶段、构建参数和环境变量并块、健康检查两种写法和 NONE、启动);嵌着的 shell 的几段(续行去掉、heredoc 正文);数组写法的命令(VOLUME、SHELL 的数组不算);同一文件里的名字(阶段名在 --from、--mount 的 from、FROM 里,变量);中文名;着色。 - 中文直译(
translators/dockerfileZh.test.ts,走readBlockViews,RUN 交给 bash 的插件):两份 luffy 的 Dockerfile 整份逐行同NEXT_READING、BACKEND_READING;阶段的各种写法、变量的展开和注解、RUN 的选项 / 数组写法 / heredoc / 一条循环 / 带重定向的一串、没有 shell 插件时照抄、复制和 ADD、设置、健康检查、启动;阶段名和表里变量名的源码下标、RUN 的源码按 shell 着色。 - shell 的分步(
languages/bash.test.ts):只用 && 连着的拆开、整串带着的重定向算在最后一条上,条件串、有 || 的、分几句写的不拆;a && echo x > f在 shell 脚本里也把重定向写在 echo 上(translators/bashZh.test.ts)。 - 命令规则(
commands/buildCommands.test.ts):装软件包、装依赖、跑程序、建用户、密钥、装文件、改属主各条的读法;选项到 4 个、写了几遍时排成表;'*'的规则只认--version;命令名 → 规则先注册的算、没有的交给'*'、出错记下来当没读。 - 宿主(
editor/documentMessage.test.ts):注册了 shell 和 Dockerfile 时 RUN 的各步是子块、按 shell 的种类,筛选栏有 Dockerfile 的种类名,名字连同 shell 里的一起交出。
- 语言插件(
- 注册表:注册 / 注销后
languageIds与可选翻译正确;同一语言多个翻译时按globalState选中;库规则按语言过滤、保持注册顺序,注销后不再给。 - 库规则(宿主):导入了库(
from sqlalchemy.orm import …、import sqlalchemy as sa、import os, sqlalchemy、函数里的导入)才启用,sqlalchemy_utils、相对导入.sqlalchemy不算;先注册的规则优先;规则抛错、替身不合约定时记下来、接着问下一个、都没认照常显示,document 消息不带 error;编辑范围是整个类、没并进来的子块(夹在中间的注释、两个字段中间的方法)照源码顺序留下,卡片「编辑」写回整个类,能编辑的范围只在块和它的子孙之间重叠;筛选的种类计数、名字取规则的标签;规则自己报的筛选属性名字取规则的attributeNames(翻译插件给了的以它为准,attributes.test.ts的合并、documentMessage.test.ts的「请求方法」),规则起名时抛错记下来、照写 id;rules/下的非测试文件只 import contract。 - 同一行不在两处能改(webview
editing.test.ts):块编辑时它的子孙都不画(卡片留下的方法连同主体、类的方法),跳到被盖住的块改闪它;子块编辑时只盖住自己的子孙;点「编辑」时重叠的块(祖先、子孙、} else {共用一行的兄弟块)退出编辑;任意点法下正在编辑、画出来的块两两不重叠。 - SQLAlchemy 规则:luffy
referral/models.py的节选(fixtures/sqlalchemyModels.py)里ReferralRelation的属性表逐字同上面的样例,其余模型覆盖Mapped[bool]、int | None可空、default=None、server_default、别名的PgUUID、_enum辅助函数、关系、唯一约束、删除时置空 / 级联删除;枚举类、函数照常显示;卡片的编辑范围是整个类、同一行只在块和它的子孙之间共用;两个字段中间写着的方法照常是子块、卡片「编辑」写回整个类、字段前面的注释并进卡片、讲方法的注释留下;类名、字段名的定义范围和带下标的词都指回源码原文;webview 里跳到类名闪整块、跳到字段名只闪字段表里那一行并先展开「字段」那一行,卡片默认折叠;import sqlalchemy as sa的老写法Column、类型全名、只写注解的字段、不认得的类型照写法、docstring 当说明、方法留作子块。 - 库规则的引用:引用标的不是自己、规则没给
resolve、段没带下标时当替身不合约定;查引用交给标它的规则,规则不在、没给resolve、出错时记下来、当作没有定义;视图数据里的引用(下标 → 引用)只从规则认领的块的属性表里取,查定义时是引用的问规则、其余问语言扩展,只对记下它的那个文档版本有效(editor/definitionSources.test.ts)。 - 工作区索引(内存里的工作区,
MemoryIndexHost):第一次查才建、同一个 id 只建一份;查到的是名字的行列(列按 UTF-16)、照文件和行排好;预筛不过的不解析;改了只重看那一份、新建加进来、删目录连里面的去掉;新建 / 删掉问过在不在的文件(__init__.py)时问过它的文件重看;建索引期间的改动建好后补上;读不了、规则出错的记下来、别的照常;dispose 后不再盯着。还要跳过的文件(workspaceScope.test.ts,内存里的文件夹):子目录里.git是文件的 git worktree、.git是目录的子仓库整个跳过,工作区自己的.git不算;.gitignore忽略的目录跳过,每层规则相对那一层、取反照 gitignore 写法,.ignore也读;search.useIgnoreFiles关着时不读忽略文件、嵌套的 checkout 照样跳过;同一个目录只查一次。 - Celery 规则:
fixtures/celeryApp.py的应用卡片和定时任务卡片逐字同上面的样例(列名、「12 条,另有展开 1 处」、行尾注释、条目里单独成行的注释、跨行的10⏎ * 60、每 get_settings().… 秒、展开一行);任务名是引用、下标指回源码原文;应用卡片的位置参数按参数顺序认、别的参数各一行、展开、属性 docstring、别名;没导入 celery、不是Celery(...)的不认;频率换算(秒数算式、timedelta、crontab各种写法、schedule、换算不了的「每 X 秒」和照写法);说明取哪些注释;条目名和任务名不同、没写任务名、任务名不是字符串、值不是字典、选项一列有才有;conf.update(beat_schedule=…)、不带beat_schedule的conf.update和autodiscover_tasks照常。任务名索引:显式name=、默认「模块路径.函数名」(包、src下的命名空间包、没有包)、@shared_task/@shared_task()/@app.task不带括号 / 老写法@task、async def、函数里的只认显式名字、name=不是字符串的不认、同名几处都给、文件改了和新建__init__.py后跟着变、没有任务装饰器的文件不解析。 - 枚举(
rules/enum.test.ts、languages/typescript.test.ts):fixtures/enumModels.py的CommissionStatus逐字同上面的样例,SecretScope的 docstring 当说明、没有说明列;种类 enum、叫「枚举」、访问级别同原来的类、没有标签(badge),编辑范围是整个类;Python 成员规则(dunder、sunder、私有名、_ignore_、lambda、property、nonmember、只写注解的不是成员,auto()写「自动」,member(x)取 x,说明取上面的注释块、行尾注释、属性 docstring,装饰器);方法、嵌套类、不是成员的属性照源码顺序是子块,成员前面的注释并进来、最后面的留下,「编辑」写回整个类;类名、成员名的定义范围和带下标的词指回源码原文;没导入 enum、SQLAlchemy 的Enum、不是枚举类型的父类不认,import enum as e的e.IntEnum认。TypeScript:enums.ts里 export / const / declare / 函数里的 enum 都是枚举块、JSDoc 进头部,写的值、自增算出的值(接不上数字就没有)、说明(上面的注释和 JSDoc、行尾注释,同一行的 JSDoc 讲下一个),访问级别;自增的值和「自动」按 hint。TypeScript 和 Python 同样的成员得到一样的种类、属性表行、列名和格子着色(除了 Python 多的「继承」)。宿主:规则认成现成属性表块时照语言插件报的这种块显示、并进来的不是子块时照常显示;webview 枚举块默认折叠。 - 数据类(
rules/dataclass.test.ts):fixtures/dataclassModels.py的SandboxAccess逐字同上面的样例,AdminUserRow、RedirectPayResult(field(init=False, default=…))、SmsSent(default_factory=dict)的行;种类dataclasses.dataclass、叫「数据类」、颜色、访问级别同原来的类;编辑范围是整个类、写回原样,__post_init__是子块;类名、字段名的定义范围和带下标的词指回源码原文;装饰器的写法(别名、dataclasses.dataclass、dc.dataclass)认,pydantic 的、other.dataclass不认,没导入 dataclasses 不认;每个选项的译法、默认值不讲、别的照写、**x、别的装饰器一行;字段规则(ClassVar、typing.ClassVar留作子块,InitVar的类型和「仅初始化时使用」,KW_ONLY记号并进来、后面的字段「只能按名字传参」、单独写kw_only=False的「可按位置传参」),field(...)的默认值、default_factory(名字、lambda)、各参数、metadata照写、doc=,说明取上面的注释块、行尾注释、属性 docstring;没注解的类属性、字段中间的方法照源码顺序是子块,「编辑」写回整个类。 - FastAPI 接口(
rules/fastapi.test.ts):fixtures/routes.py的update_user逐字同上面的样例(查到Forbidden是 403 时四条消息写在一行,没查到时不列),名字、颜色、图标、列名、筛选属性、定义范围、编辑范围是整个函数、函数体是子块;着色(URL 的{参数}/ 路径,花括号里的参数名refers到函数的参数,方法的 Swagger 颜色、成功的状态码绿色、自动带的 422 淡色,带下标的词指回源码原文);fixtures/skillHubRoutes.py的list_skills(Annotated 里的 Query、_的依赖)、get_skill(HTTPException 的 status_code / detail)、install(Request 不算、-> None没有内容);fixtures/consoleRoutes.py的create_user(status_code=201、Pydantic 模型、调别的函数间接抛的不算)、list_users(查到的类型别名FilterQuery是依赖、Literal 是简单类型);参数放在哪儿的各条(路径里的名字含{x:path}、各种标记两种写法、上传文件、枚举和类型别名查到的、Pydantic 模型、Security、Depends());标签、依赖、responses=叠上路由对象的,status.HTTP_…常量、response_model=None、response_description、raise … from、只写raise、嵌套函数里的;FastAPI()上的接口、别的装饰器一行;不认的(没导入、路由对象在别的文件、不是路由对象、websocket、不带括号)。跨文件(工作区索引 + 假的语言扩展):update_user的Forbidden按定义处查到core/exceptions/schemas.py的 403,同名的别处的类不算;索引认的类上的status_code(带注解、常量)、枚举类、Annotated类型别名,普通类不认。路由器卡片:fixtures/routes.py的router逐字同上面的样例,名字、颜色、图标、列名、编辑范围、定义范围;接口表的函数名带下标、路径里的{user_id}各refers到各自函数的参数、方法的颜色;接口卡片的「路由器」行排在说明后面、名字带下标指回路由器;写了依赖、默认响应才有这两行,FastAPI()没有前缀,属性 docstring 是说明,没挂接口时「0 个」、不画表格。 - 规则查别的文件里的定义(
ruleLookups.test.ts):第一遍当没查到、记下问了什么(同一索引同名的只记一处);查到后叫重排,交出定义处正是的那一项;同一版本不再查;索引里没有同名的不问语言扩展,定义处不是索引里的项算没有,没变不叫重排;新版本先用上一版的、再查、变了才叫重排;语言扩展没答时已知的不动、隔一会儿再查。 - 带自己颜色的词(webview
SegmentText.test.tsx):cr-colored,暗色、亮色写在 style 上;讲别处名字的段data-offset记那个名字的下标。 - 异常(
languages/exceptions.test.ts,真实解析 + 中文直译 + 视图数据):fixtures/appExceptions.py的AppException、NotFound逐字同「异常」的样例(列名随有没有类型、说明变),GatewayTimeout的多段说明,方法是子块;fixtures/commerceExceptions.py16 个类都是异常、只有说明的四行、写了__init__的构造参数;认哪些类(自带的异常类、Warning、不合后缀的自带类StopIteration/KeyboardInterrupt、pkg.errors.AppException、同一文件里写在后面的父类一层层往下传、泛型和别的父类混写,关键字参数、Mapping[...]、*bases不算,函数里的类也认);类属性(带不带注解、只写注解、说明取上面的注释块 / 行尾注释 / 属性 docstring,连着赋值、解构赋值、方法、讲方法的注释、最后面的注释留作子块),构造参数去掉 self、*args开头的不去;@dataclass的异常类照样是异常卡片;编辑范围是整个类、改卡片写回整个类,类名、属性名、参数名的定义范围和带下标的词指回源码原文;TypeScriptfixtures/exceptions.ts.txt(extends Error、类字段含 static、参数属性、JSDoc、写在后面的父类、类表达式、ns.X<T>,父类名不合习惯的照常是类)、JavaScript 的类字段;TypeScript 和 Python 同样的异常得到一样的行和列名;筛选的种类叫「异常」、没有 badge;属性的中文名和去掉行尾记号。webview:异常块默认折叠(viewState.test.ts),跳到类名闪整块、属性名 / 构造参数名只闪表里那一行(reveal.test.ts)。 - 筛选:块的属性(TS、TSX 有抽象、仅类型,JavaScript / JSX 没有这两个属性、属性表也没有这两行,Python 有访问级别、异步);语言插件自己报的属性叠上去、盖过推出来的,kind 不变;文件里出现过的属性按契约顺序排、语言的排后面,值按第一次出现排、带所有层的块数,名字取翻译插件的、没有的显示 id,中文直译的名字和属性表一致;下拉框只给两种以上值的属性(或一种值正被去掉的),按钮上的字;全选 / 取消全选只动这份文件里的值、别的文件的去掉状态和别的属性不动、取消全选藏掉有这个属性的块(整棵子树)、再全选全回来;同一属性内「或」、不同属性「且」、没有这个属性不受影响、藏整棵子树、「已隐藏」只数直接筛掉的;筛选记在
workspaceState、新实例读得回来、所有订阅的预览收到、形状不对的当没有;跳到被筛掉的块临时露出路径、目标的子块照常筛、文档刷新只留正在编辑的。 - 文件注释:键相对工作区文件夹、
/分隔,根和外面的路径没有键,手写的./、结尾/规范掉;保存去空白、清空即删;文件改名、目录挪位置里面各层一起走、名字只是前缀相同的不动、跨工作区文件夹先取出再放进去;删目录连带里面的;读写:文件缺失 / 解析失败 / 不是对象返回空表、跳过不合法条目、建.spx、键排序、删光留{};有注释 / 没注释的行的描述、悬停提示(Markdown 转义)、contextValue,和package.json里菜单的 when 子句对得上。 - 侧栏行上的菜单(
explorer/rowMenus.test.ts,读package.json):行尾按钮先「复制」子菜单再注释,有没有注释换「添加 / 编辑注释」,工作区根都没有;「复制」子菜单带$(copy),注释图标$(note)/$(edit);子菜单和右键菜单里文件四项、目录三项(有没有注释一样)、菜单上的字;复制命令都登记了、和代码里的命令表一致、命令面板里藏起来。 - 审查工单:
- 改动的文件树(
review/changedFiles.test.ts):原型那 6 个文件加一个改名的,目录层级照留、每层只有通往改过的文件的目录,文件带改动和相对仓库根的路径,目录没有改动;排法同普通的树;文件换成同名目录两项都留着;没有文件时根下面是空的。状态added→ A、deleted→ D、别的都是 M,三种颜色是 git 装饰色、删掉的提示「PR 里删掉了」。 - 选工单弹框(
review/pickList.test.ts):一项的字(有 / 没有工作副本、正在读、读不到、没有 PR);一打开就摆出各项、有 PR 的在读、在转圈,读到一个只换那一项(别的项是同一个对象、次序不变),都读完(没读到也算)停止转圈,没 PR 的、没有工单的一开始就不转;按列分组:先「审查」再「进行中」、每组前一行列名、没有工单的列不出标题、换掉一项后分组不变;工单从哪来:看板这次加载过用它、没加载过用存下的快照(JSON 里字符串的 PR 号认得,只留「审查」列和有 PR 的「进行中」列的 Gitea 工单)、都没有或快照坏了是 undefined。 - 规则和字(
review/reviewRules.test.ts):标题栏的 description / message;弹框列哪些(「审查」列都列、「进行中」列只列有 PR 的、别的列不列、只要 Gitea 的)和次序(先「审查」组再「进行中」组,组里工单号大的在前),列名,PR 号只认正整数;「审查」「进行中」列有 PR 的能审查,工单没了、两列都离开了、没有 PR(两列都一样),PR 没了、合并了、关了的原因。 - 菜单(
explorer/rowMenus.test.ts):标题栏先「选工单」、审查时才有「显示全部」、再「刷新」,字和图标;审查时注释按钮不出、复制照旧;命令面板里「显示全部」只在审查时有。 - 工作副本(
test/git/reviewWorktree.test.ts,真的 git:bare 的 origin 上推refs/pull/7/head):拉 PR 的提交建不挂分支的 worktree(父目录不在也建、本机不多出分支),目录手删后同一路径能再建;快进:PR 往前走了且干净 → 快进,已经最新 / 本地在它之上 → 不动,只有未跟踪的文件照样快进,有没提交的改动、本地另有提交 → 不动且改动留着,目录不是工作副本的根 → 报错、外面的仓库不动。
- 改动的文件树(
- 代码差异:
- 块对齐和整棵树(
diff/blockDiff.test.ts,真实的 Python 解析):原型的mutex.py(改之前是编的)比出「新增 2 · 修改 3 · 删除 1」,新增块的子孙跟着算新增、只有最外面挂小标,try_acquire自己改了、带属性表的标记,子块token改了、ok和返回没变,release只有子块改了(不算自己改了),删掉的_is_owner排在新增的renew后面、只带自己的内容、没有源码下标;没变的文件没有改动;子块里删掉的排在最前面时没有 after。对齐:有名字的按种类 + 名字配(挪了位置也配、同名照先后、种类不同不配、改名一删一增),没名字的按最长公共子序列和相似度配,种类不同不按相似度配。 - 卡片里的标记(
diff/viewDiff.test.ts):只是源码下标不同不算改;大部分没变时逐截标、删掉的去掉下标,改得多整段旧值 → 新值;框按样式、框头、框里的词比;Markdown 按词、中文按字;一种文字换成另一种按字比;子行按第一列配(加行、删行放回原来的位置、改了的只标那一格、新加的行下面各层也是新加的);属性行按属性名配、子行改了不另标值;一行译文、摊开实参的表、说明各比各的;属性表和一行译文没法比。 - 什么时候比(
diff/previewDiff.test.ts):比完发出去(统计、删掉的块带种类名、一共几行);基准里没有这个文件是新文件;没有基准不发、发过后基准没了发一次 null;旧内容按 key 记着、key 变了重取;同一份文档同一个基准不再比;取不到旧内容记一笔、清掉、下次重取;慢的旧结果不盖掉新的。 - 取旧内容(
git/fileAt.test.ts,真的 git):取的是提交里的、不是工作区改过的;提交里没有这个文件是 null;提交不存在抛错。对比基准的字(uncommittedFiles.test.ts、reviewRules.test.ts、changedFiles.test.ts):未跟踪的按状态认(两种git.untrackedChanges设置都认)、上一次提交里没有时的三种说明;PR 目标分支的文案;改名的取改名前的路径、PR 里新加的目标分支上没有。「只看改动」开关(editor/blockFilter.test.ts):记进workspaceState、关了不记、改了发给所有预览带着眼下的块筛选。 - 页面(
diffView.test.ts、markdown.test.ts、viewState.test.ts):块按记号认成页面上的 id、找不到的不标、不是这一版不画卡片里的标记,删掉的块按层挂、找不到挂处的和被筛掉的不画,新文件不逐块标,标了改动的默认展开;统计的字;删掉的块收起时的名字;只看改动折哪几段(改过的块下面按层折、删掉的块打断、被筛掉的不算、点开过的和要露出来的不折);各处改动的先后和行、被筛掉的不算;上一处 / 下一处接着走、按视口顶上的行找、到头绕回;带增删的 Markdown 照常排版、跨行逐行包、新加的列表项留住记号。
- 块对齐和整棵树(
- 复制出什么(
explorer/copyText.test.ts):文件、目录的名字 / 相对路径 / 绝对路径;不带后缀只去最后一个扩展名(a.test.ts、archive.tar.gz),.env原样、.env.local→.env、没有扩展名的原样;相对路径总用/,绝对路径照本机写法(Windows 反斜杠)。 - 块右上角的图标按钮(webview
BlockTools.test.tsx,渲染成 HTML):一组一个按钮、没有字、悬停提示(data-tooltip,没有原生title)和aria-label是「编辑」/「预览」、有图标、编辑时带按下标记;给了「问 AI」时它排在前面、只有图标、问答区展开时带按下标记和aria-expanded。 - 块的种类图标(webview
kindIcons.test.tsx,渲染成 HTML):内置种类的图标和原型一一对应;这组里每个名字画的就是 lucide 里同名的图标;webview 配了图标的名字和契约的KIND_ICON_NAMES一样、没有多的(kindIconNames.test.ts);只有图标、没有字,role="img",悬停提示(data-tooltip,没有原生title) 和aria-label是种类名,20px 的框、16px 的图形;库规则的卡片用规则给的图标。各库规则的测试核对badge的名字、颜色、图标。 - 悬停提示(webview
components/ui/tooltip.test.ts):停 500ms 才弹、在同一个元素里移动不重新计时、没等到就移开不弹、弹着时移到另一个上立刻换、移到没提示的地方就收、按下鼠标 / 按键 / 滚动就收且鼠标不离开不再弹、等着弹时按下这次不弹、键盘聚焦立刻弹失焦收、提示是空的不弹;位置在上方、上方放不下翻到下方、左右钳在视口内。Markdown 链接的标题写成data-tooltip(markdown.test.ts);网页 TSX 里的原生title由 lint 规则拦(button、span上报错,iframe、组件属性不报)。页面上(无头 Chrome,真的移鼠标、按 Tab):种类图标中心点命中的是图标里的线条、没有盖在上面的东西;停 250ms 还没弹、停满 500ms 在图标上方弹「函数」「接口」,颜色是悬停框的那组变量;Tab 到「编辑」立刻弹、Esc 收;页面上没有原生title;看板上「待接管」、禁用的 .env 锁按钮(禁用的按钮照样弹)、刷新按钮、摘修复弹框里「迁移」的三行文件名(一行一个)都弹,Tab 到按钮立刻弹、Esc 收。亮、暗两套颜色都看过。 - 问 AI:
- 位置记号(
qa/anchor.test.ts,真实的 Python 解析):有名字 / 没名字的块的记号、同一层重复的编号、两个函数里一样的条件互不影响;上面插几行、改空白记号不变;改别的函数的名字只动它和它里面的块;没名字的块自己的源码改了记号变、有名字的块改签名不变。 - 存法(
qa/qaMap.test.ts):记一次、删一次、删光不挂 / 不记;文件缺失、解析失败、不是对象当没有;认不出的块和问答跳过、没会话 id 的照认;再问时接着这块最近那条的会话(删了最近的接着剩下里最近的,没问过、没记会话 id 的重新开);用哪个 profile 答的跟着这条存、读得回来,没有的、空的、不是字符串的当没有;手改的顺序、重复的块读进来整理好;写出来排好序、两格缩进、读得回来、删光留{};改名、挪目录、删除跟着走。图片:文件名跟着这条存、读得回来,跑到别处的路径、不是字符串的名字跳过;删一次问答它的图片跟着删、别的留着;源文件删了图片跟着删,改名、挪目录图片不动。 - 图片文件(
qa/qaImages.test.ts):认的四种格式;写进.spx/code-qa/的名字和内容;没带图片不建目录;只认这边起的名字。条目带着的文件(pathMap.test.ts):改表后没有哪条再带着的才删、另一条还带着的留着;删文件时已经没了的不算错、跑到工作区文件夹外面的不碰。 - 问的话(
qa/prompt.test.ts):完整问题逐字(要求、文件、外层块的开头、带行号的代码、问题),顶层块没有外层、函数连主体一起贴,很长的块只贴开头、外层块每层最多几行,找不到这块没有,追问的话。 - 命令行(
qa/claudeAsk.test.ts,假的 claude):只读的参数、没有跳过权限、问题从 stdin 按 stream-json 进、追问带--resume;stdin 的那条消息是文字加按先后的图片,没贴图只有文字、问题不上命令行,追问和会话找不到重问都带图;问一次的过程和结果、在工作区文件夹里跑、带超时和叫停、带选的 profile 的--settings(「默认」不带);追问接着会话(换了 profile 也带--resume和新的--settings)、会话找不到时按完整问题重问(照样带选的 profile)、别的失败不重试;失败给人看的话。 - profile 下拉(
qa/askProfiles.test.ts):profile 列表 → 选项(默认 profile 在不在里面、一个都没有只剩「默认」)、没选过选哪个、选过的不在了回到默认;选了记进globalState、新的实例读得回来、所有订阅的预览收到;选的那个的路径进命令行的--settings,「默认」不带。 - stream-json(
qa/answerStream.test.ts,照本机 claude 2.1 的输出截的):会话 id、逐字的回答、读文件、新回复换掉前面那句、没开逐字片段时一样、一条回复里两段空一行、result 没带回答时用眼下的、失败的原因、坏行和子代理的消息不算。 - 正在答的问题(
qa/asks.test.ts):进展、答完、叫停(结束进程、报 stopped、这块能再问)、出错、同一块只答一个、最多几个、全部叫停。 - 跑 claude(
cc/spawnClaude.test.ts,假的 claude 脚本):逐行交出、最后没换行的那段、参数和 cwd、给了 input 写进 stdin 再关上 / 不给时 stdin 是空的不卡住、退出码非零带原话、叫停马上结束进程、超时。 - 页面(
qa.test.ts、AskThread.test.tsx):问一次、答完记下存成的那条、收起结束这一轮、重新加载接上还在答的、停止和失败、不认识的消息不理;问的时间、回答的前两行;问答区没问过不画、没展开只露「问过 N 次」、展开的历史 / 当前这一轮 / 输入框、出错和停止;profile 下拉在输入框和发送按钮中间、写选中的名字、是能用键盘打开的弹出列表按钮、没收到选项时写「默认」,扩展端发来的下拉问答刷新不丢;历史每条的灰字写「时间 · profile 名」,没有 profile 的只写时间;带图的历史和当前这一轮在问题下面、回答前面一排缩略图(能点开看大图、不能去掉),没带图的不画;这一问带的图片答完、失败、停止都还在,重新加载接上的没有。贴图拖图的规则(qaImages.test.ts):剪贴板里只取文件、文字不算;四种格式、5 MB(正好 5 MB 的算)、连同已有最多 6 张,没加上的按原因合成一句提示;发出去的 base64 去掉data:前缀。(webview 的单元测试认@/,靠vscode/vitest.config.mts里同 Vite 的别名。) - 页面上贴图(无头 Chrome,模拟粘贴、拖放、按键):粘贴图片加缩略图且不拦文字、只粘文字不加图;太大、不是图片、超过张数不加并提示;✕ 去掉后光标回到输入框;点缩略图出大图、焦点在「关闭」、Esc 只关大图不收起问答区、焦点回到缩略图,点一下也关;拖进来的图加上;Shift+Enter 不发送、Enter 发送带上图片、发送后缩略图清空、这一问下面显示它带的图;输入框里 Esc 照旧收起。亮、暗两套颜色都看过。
- 位置记号(
收尾
按仓库约定:package.json 版本 patch +1 → pnpm ext:package → 版本号改动一并 commit → 本机 code --install-extension <vsix> --profile augment。