1536 lines
243 KiB
Markdown
1536 lines
243 KiB
Markdown
# 代码阅读(Code Reader)设计
|
||
|
||
## 目标
|
||
|
||
代码几乎全由 agent 写,逐行读太慢。给插件加一个独立的「代码阅读」入口:打开一个源码文件时,可以切到「预览」,把代码拆成一棵代码块树,每块用模板直译成中文,扫一眼就知道这个文件在干什么;要看细节时单块切回源码,并且可以直接改。
|
||
|
||
它和现有的 Superpowers 看板(活动栏小火箭)是两套东西,只是放在同一个插件(`vscode/`)里。
|
||
|
||
视觉和交互以 Figma 原型为准:`docs/prototype/code-reader/figma-plugin/`(`code.js` 里的数据和 `test.js` 里的期望值就是本文的样例)。
|
||
|
||
## 已确认的决定
|
||
|
||
| 事项 | 决定 |
|
||
| --- | --- |
|
||
| 流程 | 源码 →(tree-sitter)语法树 → 通用代码块树 → 译文 |
|
||
| 第一版翻译 | 只做中文模板直译;AI 解读以后作为另一个翻译插件加 |
|
||
| 第一版语言 | TypeScript / TSX(含 JavaScript / JSX)、Python |
|
||
| 扩展方式 | 写代码注册:语言插件、翻译插件都实现约定接口,通过本插件 `activate()` 导出的 API 注册;内置实现也走同一个 API |
|
||
| 多种翻译怎么选 | 预览顶部筛选栏最右边的下拉框,整个文件一起换,按语言记住选择;只有一种时不显示 |
|
||
| 块筛选 | 预览顶部钉住一条筛选栏,按块的属性(种类、访问级别、异步……)筛;属性由语言插件按和语言无关的 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 改过的文件(目录层级照留、全部展开,按 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` 写「展开关键字」);一行写完的调用照旧一行译文。新建还是调用:TypeScript 看有没有 `new`;Python 写法一样,查被调用的名字的定义,是类就是「新建」,是函数、查不到都是「调用」,先按调用画、查到了再刷新。表在变量 / 常量属性表的初始值 / 值那一行下面(能折叠),在返回、语句这些一行译文的块里排在那一行下面。语言插件报结构,翻译插件定用词,webview 只画表。见「调用与构造」 |
|
||
|
||
## 用户可见行为
|
||
|
||
### 活动栏与侧栏
|
||
|
||
- 活动栏新增图标(书本,`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 改过的文件(原型画板「审查 · 选工单」「审查 · 改动的文件」)。范围就是原型画的这些:不标差异、不开 diff、没有「改动」筛选。
|
||
|
||
- 选工单:视图标题栏第一个按钮(`view/title` 的 `navigation@1`,`$(git-pull-request)`,命令 `superpowers.codeReader.pickReviewIssue`)。点开 VS Code 顶部的快速选择(`window.createQuickPick`):标题 `选工单 · 看板「审查」列`,占位 `输入工单号或标题筛选`(按第一行筛)。工单马上列出来,各 PR 改了几个文件随后一项项补上,都补完才停止转圈(`busy`):
|
||
- 工单:看板上现在摆着的,不再加载一遍(整个看板重新加载要翻每张工单的评论,几百张工单时很慢,哪个请求卡住就一直转)。面板这次会话里加载过就用它最近一次加载的(`KanbanWebviewPanel.issuesOnBoard` → `snapshotIssues()`),否则用上次存下的看板快照(`workspaceState` 的 `superpowers.kanbanSnapshot`,重开窗口时看板先画的也是它);二者都是看板加载时算好的(state JSON 定列、看板的范围、叠好本机记录)。只留「审查」列的 Gitea 工单(PR 在 Gitea 上),次序同看板列(工单号大的在前)。快照里 PR 号是字符串(`"454"`),和 `Issue.pr` 的类型一致,按正整数认。
|
||
- 看板还没加载过(两样都没有):占位换成 `看板还没加载过,先打开一次看板`,不转圈。「审查」列没有工单:占位换成 `看板「审查」列里没有工单`。
|
||
- 快照可能落后于看板上后来的挪动:刚挪进「审查」列、看板还没重新加载过的工单要等看板刷新后才列出来;选中时会重读这张工单(见下),已经挪走的不会进审查。
|
||
- 每项第一行 `#<工单号> <标题>`,后面淡色 `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 分支,审查建的这个不该走那条路。
|
||
- 审查时的树:根只有一个,是工作副本,名字照写工作区文件夹的;下面只有 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`)。
|
||
|
||
### 预览(代码块树)
|
||
|
||
- 每一句是一块:导入、函数、类、枚举、异常、类型、字段、常量、变量、条件、否则、循环、尝试、捕获、最终、返回、语句、注释。
|
||
- 嵌套的块放在父块下面、往右缩进;每一层左边一条竖线(颜色 `--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 导入 | `import` | field 字段 | `tag` | try 尝试 | `shield` |
|
||
| function 函数 | `square-function` | const 常量 | `lock` | catch 捕获 | `shield-alert` |
|
||
| class 类 | `box` | variable 变量 | `variable` | finally 最终 | `shield-check` |
|
||
| type 类型 | `braces` | if 条件 | `split` | return 返回 | `corner-down-left` |
|
||
| enum 枚举 | `list-ordered` | else 否则 | `corner-down-right` | statement 语句 | `code` |
|
||
| exception 异常 | `triangle-alert` | loop 循环 | `repeat` | comment 注释 | `message-square-text` |
|
||
|
||
库规则的种类用规则给的图标(`KindBadge.icon`):SQLAlchemy 数据表 `database`、数据类 `package`、Celery 应用 `server-cog`、定时任务 `calendar-clock`、FastAPI 接口 `globe`、FastAPI 路由器 `network`。
|
||
|
||
这组图标由插件自己定义,是一个封闭的集合:契约里的 `KIND_ICON_NAMES`(上表 18 个加库规则用的 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 算在它那一块里(中间隔着的空行也是)。
|
||
- 库规则认领的块(数据表卡片等,见「库规则」):种类的图标、颜色和名字由规则给,内容是规则给的属性表;它的编辑范围是原块整条语句(类就是整个类),「编辑」时整段一起改。规则没并进卡片的子块(方法、别的成员)不管写在类主体哪儿(两个字段中间也算),都照源码顺序是卡片的子块,各自的「编辑」照旧只改自己那几行。
|
||
- 同一行不会同时在两处能改(能同时编辑的几段互不重叠):一块在编辑时,编辑范围和它重叠的子孙块不画(卡片编辑时它下面的子块全收起来,一行写完的 `if x: return 1` 编辑时同一行的子块也不画),回到预览就照原样出来;点一块的「编辑」时,编辑范围和它重叠的别的块退出编辑。跳到定义的目标块正被盖住时闪盖住它的那块。
|
||
- 折叠:函数、类、枚举、异常块和库规则的卡片打开时默认收起子块(属性表照常显示),其余块默认展开;用户点过折叠箭头的块按他点的,记在视图状态里(见「预览数据与消息」)。
|
||
- 顶部筛选栏:见「筛选」;翻译下拉框(多于一种翻译时才显示)也在栏里。
|
||
- 解析失败:显示错误原因,并提示用「编辑」查看源码。
|
||
|
||
### 筛选
|
||
|
||
预览顶部钉住一条筛选栏,按块的属性藏掉一部分块(原型画板「筛选 · 默认 / 按访问级别 / 按种类」)。
|
||
|
||
- 三层分工,webview 不认识任何语言:
|
||
1. 语言插件给每块报和语言无关的属性「属性 = 值」,都是稳定的英文 id,不放显示文字。通用词表由契约定(`COMMON_ATTRIBUTES`):`kind`(块的种类)、`access`(`public` / `private` / `protected`,语言可以加自己的值)、`async`、`abstract`、`typeOnly`(`'true'` / `'false'`)。通用属性由宿主从块的 kind 和 facts 推出来(`FunctionFacts.access` / `async`、`ClassFacts.access` / `abstract`、`EnumFacts.access`、`ExceptionFacts.access`、`ImportFacts.typeOnly`、`VariableFacts.access`),只有这一条路,语言插件不用另报;facts 里没填的(语言没有这个概念,例如 JavaScript 没有抽象类、仅类型导入)块就没有这个属性,不是填「否」。语言自己的属性(Rust 的 `unsafe = 'true'`)、facts 表达不了的通用属性值(Kotlin 的 `access = 'internal'`)填在块的 `attributes` 里,和推出来的合在一起,同一属性以它为准;`kind` 总是块的种类。
|
||
2. 翻译插件给属性和值起名(`Translator.attributeNames`:`access` → 访问级别、`public` → 公共、`kind` 的 `function` → 函数、布尔值 → 是 / 否),和属性表里的属性名、值是同一套叫法(中文直译里属性表的「访问级别」「异步」「抽象」「仅类型」行和筛选共用一份表);种类图标的悬停提示也取它。库规则的卡片自己报的属性(`RuleCard.attributes`,FastAPI 接口的 `httpMethod`)由规则起名(`LibraryRule.attributeNames`,用词归规则、按翻译插件的显示语言挑):宿主先取翻译插件给的名字,没给的再取规则给的(`attributes.ts` 的 `mergeAttributeNames`)。没起名的显示 id。
|
||
3. webview 只按属性 id / 值 id 筛,显示翻译插件给的名字。
|
||
- 下拉框:这份文件里有两种以上值的属性各一个(只有一种值的不给——筛它等于按种类筛);只有一种值、但这个值正被去掉的也给,不然藏起来的块在栏上找不到原因。排列:通用属性按契约的顺序(种类、访问级别、异步、抽象、仅类型),语言自己的属性按在文件里第一次出现的先后排在后面。下拉菜单顶上一行「全选 · 取消全选」,分隔线下面是勾选列表,列出这份文件里出现的值(按第一次出现的先后)和各有几块(所有层的块都算,例如「私有 3 / 公共 3」)。
|
||
- 全选 / 取消全选:把这份文件里这个属性出现的值一起勾上 / 去掉,和逐个勾一样记下来;别的文件才有的值记着的去掉状态不动(筛选整个工作区共用)。取消全选后有这个属性的块全藏起来,没有这个属性的照常显示;菜单不关,可以接着勾。
|
||
- 规则:同一属性里勾着的值满足一个就显示,不同属性要同时满足;块没有某个属性就不受那个属性的筛选影响(条件、返回块没有访问级别,按访问级别筛时照常显示);块被筛掉,连同它的子块一起藏,不留空位,层级竖线跟着变短;子块都被筛掉的块不给折叠箭头。
|
||
- 汇总:什么都没筛掉时右边写「共 N 块」(N 是整份文件所有层的块数);筛掉了写「已隐藏 N 块 · 清除」,N 只数直接被筛掉的块,跟着藏起来的子块不算;点「清除」整份筛选清空。页面上一块都不剩时写「所有块都被筛选隐藏了。」
|
||
- 下拉框上的字:没去掉值时是属性名;去掉了(边框换 `--vscode-focusBorder`)勾着一种写「访问级别:公共」,几种写「种类:已选 11 种」,一种都不勾写「种类:未选」。
|
||
- 记住:记的是每个属性**去掉的值**(`{ 属性 id: 值 id[] }`),所以文件里第一次出现的值默认显示。整个工作区共用一份(不按文件),存在扩展端的 `workspaceState`(`superpowers.codeReader.filter`),重新加载窗口后还在;一个预览改了,所有开着的预览跟着变。某个属性在这份文件里没出现就没有它的下拉框,记着的去掉的值照样对它出现的文件起作用。
|
||
- 跳到定义到被筛掉的块:临时显示目标和通向它的路径(被筛掉的祖先块下面只露出这条路径,目标自己的子块照常按筛选显示,函数照样能展开看主体),本来被筛掉的块右边带小提示「已被筛选隐藏,临时显示」,照常展开、滚过去、闪。筛选一变,或文档刷新(正在编辑的块和它的祖先除外,不能打着字就消失)就回到筛选状态。
|
||
- 折叠、跳到定义、属性行 id、滚动位置恢复照旧;被筛掉的块不在页面上,按行滚动(从文本编辑器切过来)落到它前面最近的一块。视口顶部按筛选栏下沿算:记位置、恢复、滚过去都让开筛选栏。
|
||
- 样子:筛选栏钉在预览顶部(`position: sticky`,块从它下面滚过去),横贯编辑区、下边一条 `--vscode-panel-border`、底色 `--vscode-editor-background`,里面的内容和代码块同宽对齐:「≡ 筛选」(宽 52px)、每个属性一个下拉框(宽 150px、高 24px,`--vscode-dropdown-background` / `--vscode-dropdown-border` / `--vscode-dropdown-foreground`)、右边汇总(`descriptionForeground`,「清除」`--vscode-textLink-foreground`)、再右边翻译下拉框;放不下就换行。下拉菜单盖在代码块上面:宽 200px,`--vscode-editorWidget-background`、`--vscode-editorWidget-border`、`--vscode-widget-shadow` 阴影;顶上「全选」「取消全选」两个链接(`--vscode-textLink-foreground`,间距 12px),下面 1px 分隔线(`--vscode-menu-separatorBackground`),列表太长时只滚列表、这一行不动;每项勾选框(勾着是 `--vscode-focusBorder` 底)、值、右边块数(`--vscode-editorLineNumber-foreground`);悬停、键盘高亮 `--vscode-list-hoverBackground`。
|
||
- 键盘和读屏:下拉框按钮 `aria-haspopup="listbox"`、`aria-expanded`;菜单 `role="listbox"`、`aria-multiselectable`,每项 `role="option"`、`aria-selected` 表示勾着。按钮上 ↓ / 回车 / 空格打开(↑ 打开并停在最后一项),焦点落在列表上;列表里 ↑ ↓ Home End 移动,空格 / 回车勾选;Shift+Tab 回到「取消全选」「全选」(普通按钮,回车 / 空格触发),Tab 再回到列表;Esc 关掉并回到按钮;焦点离开下拉框(Tab 出去、点外面)就关。鼠标点选项、全选 / 取消全选时焦点留在列表上,方向键接着能用。
|
||
|
||
### 属性表(函数、类、枚举、异常、导入、常量、变量块)
|
||
|
||
卡片外观(折叠箭头、种类图标、底色、悬停高亮、「编辑 / 预览」切换)和其他块一样,只是内容换成一张属性表:左列属性名(界面字体、`descriptionForeground` 色,宽 76px),右列是值(等宽字体,按词性着色,长了换行),两列间距 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)` 三个都是)。
|
||
- 按命名习惯认的部分和它的限制:父类从别的文件导入时语言插件看不到那个文件,只能按名字认(PEP 8 要求异常类名以 Error 结尾,`AppException` 这种以 Exception 结尾的也常见)。所以父类名不合习惯(从别处导入的 `NotFound`、`Forbidden` 当父类)的认不出来,照常是类块;名字合习惯却不是异常的父类(普通类起名 `RenderError`)会被当成异常。同一文件里按名字认,不看作用域(同名的两个类当成一个)。
|
||
- 类属性(进「属性」表):Python 是类主体里给一个名字赋值的语句(`x = 1`、`x: int = 1`、只写注解的 `x: int`),连着赋值(`a = b = 1`)、解构赋值不算;TypeScript / JavaScript 是类字段(`public_field_definition` / `field_definition`,含 static;值是带语句块主体的函数、类表达式的照常是方法、类块)。说明同枚举成员:紧挨在上面的注释块、这一行行尾的注释、Python 紧跟在后面的属性 docstring,原文用空格连起来。类 docstring 里谷歌写法的 `Attributes:` 一节照原文留在「说明」里,不拆到各属性的说明格子。
|
||
- 构造参数:Python 是 `__init__` 的参数去掉第一个(self;第一个是 `*args` 这类时不去),TypeScript / JavaScript 是 `constructor(...)` 的参数(参数属性 `public code?: string` 也是一个参数);写了几个取最后一个(重载的实现写在最后)。`*`、`/` 分隔符同函数的输入参数,不算参数。类里没写构造函数(继承父类的)时没有这一行。
|
||
- 并进属性表的子块:各个类属性连同紧挨在它前面的注释块;构造函数、方法、别的语句、讲方法的注释、最后面的注释照常是子块(不管写在哪),默认收起。异常块的头部(编辑范围)是整个类:从说明、装饰器开始,到类连同各子块的最后一行,「编辑」时整段一起改,盖住留下的子块(同库规则的卡片)。
|
||
- 语言插件报的已经是异常块,库规则只认类块,所以异常类不交给库规则:`@dataclass` 装饰的异常类照样是异常卡片,`@dataclass` 写在「装饰器」一行,注解的字段在「属性」表里。
|
||
- 跳到定义:类名是块自己的名字(闪整块),属性名只闪「属性」表里那一行,构造函数的参数只闪「构造参数」表里那一行(构造函数自己的「输入参数」也标了同一个参数,页面上先找到的是卡片里这一行);中文名(`Annotated[…, Comment("…")]`、行尾 `# @Comment …`)照常换属性格子里的名字,行尾记号不进说明。
|
||
|
||
样例(`fixtures/appExceptions.py` 是 luffy `core/exceptions/schemas.py` 的快照,测试里逐字核对;`str | None` 译成「字符串 | 空」):
|
||
|
||
```
|
||
名称: AppException
|
||
说明: 应用异常基类. …(docstring 原文,含 Attributes: 一节)
|
||
访问级别: 公共
|
||
继承: Exception
|
||
属性: 3 个
|
||
status_code | 整数 | 500
|
||
error_code | 字符串 | "INTERNAL_ERROR"
|
||
message | 字符串 | "服务器内部错误"
|
||
构造参数: 3 个
|
||
message | 字符串 | 空 | 空
|
||
detail | Any | 空
|
||
error_code | 字符串 | 空 | 空
|
||
|
||
名称: NotFound
|
||
说明: 资源不存在 (404).
|
||
访问级别: 公共
|
||
继承: AppException
|
||
属性: 3 个
|
||
status_code | 404
|
||
error_code | "NOT_FOUND"
|
||
message | "资源不存在"
|
||
```
|
||
|
||
`AppException` 的 `__init__`、`to_dict` 是卡片的子块。`fixtures/commerceExceptions.py`(luffy `commerce/exceptions.py` 的快照)里 16 个类都是异常,只有说明的是名称 / 说明 / 访问级别 / 继承四行,写了 `__init__` 的多一行构造参数。
|
||
|
||
### 推导式(写在表达式里的遍历)
|
||
|
||
原型画板「遍历 · 推导式」。Python 的列表 / 集合 / 字典推导式、生成器表达式(含 `async for`、几节 `for`、`if`、套着的推导式)不管出现在哪(一行译文、属性表的值 / 初始值 / 默认值、字典项的值),都原地换成一个小框:
|
||
|
||
- 框:嵌在前后文字里(`display: inline-block`),框头是「循环」块的种类图标(`KIND_ICONS.loop`,lucide `repeat`,12px)和结果类型,后面是重排后的译文。边框、底色、框头字的颜色都取「循环」块的种类色(`--cr-kind-loop`:边框 45%、底色 14%,圆角 4px,左右留 5px);框头界面字体 11px、500 字重。上下边框算在 20px 行高里,框和前后文字基线对齐、不把一行撑高;框里套的框行高再减 2px。整个框放不下时挪到下一行,比一行还长时占满一行、在框里换行。
|
||
- 结果类型:生成器 / 列表 / 集合 / 字典(`[…]` 列表、`{…}` 集合、`{k: v …}` 字典、`(…)` 生成器)。
|
||
- 框里按读的顺序重排:每节 `for` 写「遍历 <来源> 的每个 <变量>」,紧挨着的几节 `for` 用顿号接着写(「遍历 xss 的每个 xs、xs 的每个 x」),`async for` 写「异步遍历」(同步、异步换了就另起一句「,异步遍历 …」),`if` 在它所在的位置写「,仅当 <条件>」(后面再有 `for` 就另起一句「,遍历 …」),最后「,取 <值>」,字典是「<键>: <值>」。来源、变量、条件、值照常直译,里面还有推导式就是框里的框。「遍历」「的每个」「仅当」「取」「异步遍历」按关键字着色,逗号、顿号、空格按普通文字。
|
||
- 括号:框代替了推导式自己的方括号 / 花括号 / 圆括号,不再写出来;Python 调用唯一的参数是生成器时只写一对括号(`f(x for x in y)`),这对括号属于调用,照常写在框外,紧贴着框:`"".join(` 框 `)`。生成器是几个参数之一时(`f(a, (x for x in y))`)括号是它自己的,不写。
|
||
- 框里的名字照样带源码下标:⌘ / Ctrl + 点击跳到定义、单击高亮各处出现都照旧;跳到定义的目标只显示在框里(推导式的变量)时,目标是框所在的块。
|
||
- 样例(必须逐字一致;框写成 `⟦类型|框里的文字⟧`):
|
||
|
||
| 源码 | 译文 |
|
||
| --- | --- |
|
||
| `return "".join(secrets.choice(ALPHABET) for _ in range(length))` | `返回 "".join(⟦生成器|遍历 range(length) 的每个 _,取 secrets.choice(ALPHABET)⟧)` |
|
||
| `return [u.id for u in users if u.is_active]` | `返回 ⟦列表|遍历 users 的每个 u,仅当 u.is_active,取 u.id⟧` |
|
||
| `return {u.id: u for u in users}` | `返回 ⟦字典|遍历 users 的每个 u,取 u.id: u⟧` |
|
||
|
||
- TypeScript 没有这种写法,不涉及。几种读不成遍历的写法(语法树认、Python 3 不认的 `for x in a, b`,写到一半缺了变量 / 来源 / 条件 / 值的)照常逐词直译。
|
||
|
||
### 调用与构造
|
||
|
||
原型画板「调用 · 构造」(`code.js` 的「调用与构造」一节)。实参多的调用一般跨几行写,挤成一行译文读不下去;把实参摊成一张表。
|
||
|
||
- 哪些调用:实参列表(括号连同里面,`CallFacts.argumentList`)跨了几行的调用,而且整个落在要翻的这一段里(块头部;TypeScript `describe('x', () => { … })` 这种主体是子块的调用不算)。一行写完的调用照旧一行译文,不问定义。链式调用跨了几行、每段的括号都写在一行里的(`items\n .filter(…)\n .map(…)`)也不算。一段里只排遇到的第一个(先外后里),实参里的调用不再排成表,照常一行译文。
|
||
- 一行:`[等待 ]新建 / 调用 被调用的名字 · N 个参数`。「等待」就是 `await` 照常的译法(在调用前面),「新建」「调用」按声明关键字着色;被调用的名字照常翻、带源码下标(⌘ / Ctrl + 点击、单击高亮、中文名都照旧),前面几节(对象、模块)按变量着色,最后那个名字新建时按类型着色、调用时按函数着色;「· 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`:表头、圆角外框、格子之间分隔线,第一列按内容定宽)。
|
||
- 放在哪:
|
||
- 变量、常量块:初始值 / 值那一行写上面那一行字,表是这一行展开的子行(`PropertyRow.columns = ['参数', '值']`),带折叠箭头、默认展开。值是字典 / 对象字面量时照旧按「字典 N 项」展开,不看里面的调用。
|
||
- 一行译文的块(返回、语句、字段……):译文里调用的位置换成那一行字(「返回 新建 UserDetailResponse · 11 个参数」),表排在译文下面(`TextTranslation.table` → 视图数据的 `table`),总是展开;块带说明时说明排在表下面。
|
||
- 分工:语言插件报结构(`LanguageAdapter.call`:被调用的表达式、最后那个名字、写法是不是新建、实参列表、各个实参),翻译插件定用词、挑哪个调用、怎么排;webview 只画表(`PreviewTable`),不认语言;跳到定义、高亮各处出现、中文名在表里照旧(`renderedNames`、`reveal.ts` 都算上这张表)。
|
||
- 样例(`fixtures/routes.py`,luffy 的用户路由):
|
||
|
||
| 源码 | 预览 |
|
||
| --- | --- |
|
||
| `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` |
|
||
|
||
### 注释的 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` 为假(默认)时先把当前预览标签固定住,免得被目标替掉。
|
||
- 闪哪一处:页面上**就是这处定义**的最具体的那一处,只按源码位置判断(定义提供者给的定义处下标,对上块和属性行带的源码范围),不看属性名文字:
|
||
1. 定义是块自己的名字(函数名、类名;只声明一个变量时的变量名,解构模式整个算)→ 整块。块的这段范围由扩展端从 facts 取(`BlockView.defines`),和翻译插件无关。
|
||
2. 定义是属性表里某一行 / 子行讲的东西(参数、导入项、一条语句声明几个变量时的某一个、类的泛型参数、对象字面量一项的键——TypeScript 对象的属性名就是定义,嵌套几层都算)→ 只闪这一行 / 子行。范围是翻译插件从 facts 取的节点(`PropertyRow.defines`、`PropertyItem.defines`;字典 / 对象的项标的是键,展开项不标),定义处落在范围里就算(`*args` 的定义在 `args` 上、解构参数里的名字都对得上)。
|
||
3. 其余(定义在一行译文里,例如循环变量;翻译插件没填 `defines`)→ 显示着定义处那个名字的块,名字在属性行的子行里时展开那一行(在嵌套的子行里时连它上面各层一起展开);页面上找不到这个名字就取包含定义那一行的块。
|
||
- 同一文件原地跳、跳进另一个文件的预览,规则一样。
|
||
- 滚过去:目标所在块的祖先块折叠着就展开,目标在折叠的属性行子行里(参数、导入项、字典 / 对象的项)就展开那一行,在嵌套的子行里就连它上面折叠着的各层子行一起展开。目标已在视口里不滚,否则滚到视口正中(比视口高就顶部对齐)。然后目标的边框和底色往聚焦色(`--vscode-focusBorder`)闪三下,共约 1.5 秒后复原;同一处再跳一次从头闪;系统开了「减少动态效果」时改成静止高亮 1.5 秒。目标是属性行 / 子行但块正在编辑(页面上没有属性表)时闪整块。
|
||
- 不按修饰键单击名字是高亮它的各处出现(见下一节);⌘ / 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 毫秒来回跳的画面;滚到原地浏览器当没滚、不打断已排上的动画,所以回位时先挪一个像素再回来(同一段脚本里连着两下,中间不画),动画在开始前就被打断,录屏里一帧都没有被滚走。
|
||
- 和编辑器一样每回点都现问,回答不留到下次用(语言扩展刚启动时答的「没有」或按文字认的结果不会一直留着);只认最近一次询问的回答,迟到的丢掉。点另一个名字:回答到之前旧高亮留着,到了就换成新的;回答是空的(没装语言扩展、没答)就什么都不亮,不按文字自己找。
|
||
- 清掉(连同筛选栏上那句):点空白处或不是名字的字、按 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 个字 | 参数(函数声明、方法、值是函数的变量和类字段) |
|
||
|
||
- 行尾记号给那一行第一个能带行尾记号的定义(上表第三行、第六行那些;函数、类用装饰器),单独成行的注释、那一行没有这种定义的不算。
|
||
- 同一个定义几处都写了:写在代码里的记号(注解、装饰器)优先,其次行尾记号,最后才是说明文字。
|
||
- 行尾记号不是说明文字:语言插件把记号的源码范围也交出来(`DisplayNameFacts.markers`,不管这个定义最后取没取它),宿主放进 `SourceDocument.markers`,翻译插件翻注释、说明时把它去掉(`# @Comment 管理员` 这样只有记号的注释就没有说明,枚举成员表里没有这一列)。docstring、JSDoc 照原样显示。
|
||
|
||
落到哪些名字上(用到它的各处由语言扩展按语义认,同名的别的东西不算;没有语言扩展时一个都不换,和高亮、跳到定义一样只走这一条路):
|
||
|
||
- 本文件定义的:每个起了中文名的定义在定义处问一次 `vscode.executeDocumentHighlights`,回答里落在视图数据里带下标的名字(段的下标、长度都对得上)上的都换成中文名。
|
||
- 别的文件定义的(`UserRole.ADMIN`,ADMIN 在 models.py):宿主给每种语言建一份工作区中文名索引(同库规则的 `WorkspaceIndex`,找文件、排除、嵌套 checkout、忽略文件、盯改动、第一次查才建都是同一套;文件是语言插件给的 `DisplayNameReader.files`,预筛是 `mentions`),键是原名;这份文档里原名在索引里(别的文件)有的名字才问 `vscode.executeDefinitionProvider`,定义处正好是索引里那个定义(同一文件、同一行列)才换。
|
||
- 按文档版本查一次,不挡第一次画:文档先发,查到本文件的先发一次,别的文件的查完再发(`displayNames` 消息);页面重新加载(标签页切回来)时随文档带上已经查到的。语言扩展刚启动时答不上来(定义处自己都查不到出现,或要问的定义一个都没答),隔 1、2、4、8、16 秒再查,还答不上就算了。
|
||
- 改文档时不闪:已经查到的中文名跟着改动挪位置(改动在前面的往后挪,碰到的丢掉),随新版本的文档先带过去;新版本查完一起换(手上有挪过来的就不先发只有本文件的那一半)。查到一半文档又改了,查到的丢掉。
|
||
|
||
怎么显示(webview 不认识任何语言,只按下标和长度换字):
|
||
|
||
- 预览里所有照抄源码名字的地方(一行译文、属性表、表格、枚举和库规则的卡片、推导式的框):名字写成中文名,悬停提示「原名 current_user」;下标不变,⌘ / Ctrl + 点击跳到定义、单击高亮各处出现、⌘↓ 跳都照旧,筛选栏上的名字写中文名。
|
||
- 定义处本身(输入参数表格的名称格子、名称行、枚举成员格子这些):中文名后面淡淡写原名(`descriptionForeground`、字小一号,`.cr-original`),例如「当前用户 current_user」。
|
||
- 只换名字那一段:`UserRole.ADMIN` 读成「UserRole.管理员」,前面的类名照写。
|
||
- 「编辑」状态的源码、Markdown 说明里的名字不换。
|
||
|
||
### 问 AI
|
||
|
||
原型画板「问 AI」、组件 `Reader / AskPanel`(`State=history | asking`)。读代码时对一块有疑问,直接在块下面问 Claude Code,答完存起来,下次打开还在,理解了就删。
|
||
|
||
- 入口:块右上角那排图标按钮里的「问 AI」(每种块都有:普通块、属性表块、库规则的卡片)。点一下在块下面展开问答区,再点(或在输入框里按 Esc)收起。文件不在任何工作区文件夹里时问答没地方存,不给这个按钮。
|
||
- 问答区紧跟在块下面、子块前面,和块同宽,左边一条 2px 的链接色竖线(`--vscode-textLink-foreground` 50%)。从上到下:
|
||
- 灰字的历史:这块以前问过的,每条一行问题(`descriptionForeground`,一行放不下截断)、右边时间(「刚刚」「5 分钟前」「3 小时前」「昨天」「3 天前」,一个月以前写日期,悬停看完整时间)跟着用哪个 profile 答的(「3 天前 · sonnet」;选「默认」答的、这功能以前存的只写时间)和垃圾桶「已理解,删除」;问的时候贴了图的,问题下面一排缩略图(见「贴图」,灰字历史里淡一点、悬停变实);下面是回答的前两行(去掉 Markdown 记号的纯文字,更淡)。点问题或这两行展开整段回答,照样灰字,按 Markdown 排(同注释的 Markdown 渲染),再点收起。
|
||
- 当前这一轮(展开以来问的,和历史之间一条细分隔线):问题照常颜色、加粗,贴了图的问题下面一排缩略图,回答照常颜色、按 Markdown 排,逐字长出来,末尾一个闪的光标;下面一行灰字写正在做的事(「读取 exceptions.py」「搜索 get_user」,还没开口时「正在想…」)。出错时这一问下面一个红框写原因(找不到 claude、退出码非零带它的原话、没登录时提示在终端里运行 claude 登录、超时);叫停的写「已停止,这次没存。」
|
||
- 输入框:没问过时提示「问这块代码… Enter 发送,Esc 收起」,问过了「继续问… Enter 发送,Esc 收起」;Enter 发送,Shift+Enter 换行,输入法选字时的 Enter 不算;Esc 收起(不清掉名字高亮);展开时光标就在输入框里。右边先是 profile 下拉,再是发送按钮(没字时不能点),正在答时换成「停止」。
|
||
- 贴图:在输入框里粘贴(⌘V / Ctrl+V)剪贴板里的图片(截图、复制的图片),图片加到输入框上面一排缩略图里,随下一个问题一起发;剪贴板里同时有的文字照常粘进输入框。图片文件拖到输入框上也一样(VS Code 里要按住 Shift 拖,不然它自己接走打开文件)。认 PNG、JPEG、GIF、WebP,每张最多 5 MB,一个问题最多 6 张;不认的、太大的、多出来的不加,缩略图下面一行提示原因(「只能贴 PNG、JPEG、GIF、WebP 图片;每张图片不能超过 5 MB;一个问题最多带 6 张图片,有 N 个没加上。」,警告色 11.5px),下次贴图或发送时清掉。问题还是要有字才能发(只贴图不写字发不出去);发出去后缩略图清空。
|
||
- 缩略图:40px 见方、圆角 4px、1px 边框,图按比例裁满;历史和当前这一轮的和回答一样往右缩到问题下面,贴了还没发的和输入框左边对齐、每张右上角一个小圆「✕」(「去掉这张图片」,去掉后光标回到输入框)。点缩略图看大图:盖住整个预览的深色底,图按比例放到放得下为止,右上角「关闭」;打开时焦点在「关闭」上,点哪儿或按 Esc 关上(只关大图,不收起问答区、不清名字高亮),焦点回到点开它的缩略图。缩略图用 `data:` 地址(还没发的、当前这一轮的)或 `webview.asWebviewUri` 给的地址(存下的),页面安全策略本来就允许这两种图片,不用改。
|
||
- profile 下拉(同筛选栏的下拉 `SelectMenu`,`ghost` 样子):只写选中的 profile 名和一个小箭头,灰字 12px、高 20px,最宽 140px,长名字截断,输入框还是主角;悬停 `--vscode-toolbar-hoverBackground`,打开时 `--vscode-toolbar-activeBackground`,键盘聚焦描 1px `--vscode-focusBorder`。点开(或聚焦后按 ↓ / ↑ / Enter / 空格)列出能用的 profile,↑ ↓ 挑、Enter 选、Esc 关;profile 多了弹层顶上有搜索框。悬停提示「用哪个 Claude Code 配置(profile)回答,下一个问题起用它」。
|
||
- 选项是插件的 profile 目录(同看板、会话:`cc/profiles` 的 `listClaudeProfiles`,名字是文件名去掉 `.json`)。插件的默认 profile(`getDefaultProfilePath`,`offical.json`)在里面时,没选过就选它;不在(一个 profile 都没有,或没有 `offical.json`)时最前面多一项「默认」:不带 `--settings`,claude 用 `~/.claude` 的登录,和以前一样照问,没选过就选它。
|
||
- 选了从下一个问题起用它(正在答的不受影响)。所有预览、所有块共用一份,记在扩展的 `globalState`(`superpowers.codeReader.askProfile`,记 profile 名,「默认」记 `''`)里:换个预览、重启 VS Code 都还是它;记着的那个没了(删了、目录换了)就回到默认那项。
|
||
- 没展开、但问过的块:块下面只露一行灰字「问过 N 次」,读的时候一眼知道这里问过;点开看灰字历史(能展开、能删),没有输入框。
|
||
- 收起就结束这一轮:这一轮答完的都回到灰字历史里(再问照样接着会话,见「追问」);还没答完的接着答,答完照样存。同一块同时只答一个问题(正在答时不能再发),不同块可以同时问,全插件同时最多 3 个,再多就在这一问下面说「同时最多问 3 个问题,等前面的答完再问」。
|
||
- 追问:这块存过问答,再问就接着最近一次(按问的时间)存下的那条的会话(`--resume <会话 id>`,会话 id 取 stream-json 的 init 消息,和问答一起存在 `code-qa.json` 里),只发问题和「还是第几行那块」;收起再展开、预览重新加载、重启 VS Code 都一样接得上,前面问过的照旧灰字显示。换了 profile 也照样接着那次会话问(`--resume` 和这次选的 `--settings` 一起带,本机 claude 2.1 试过:换 profile 后会话 id 不变、记得前面说过的),线程里不另外提示。那条没记会话 id、或会话找不到了(过期、被清理)就按第一次那样带上代码上下文重新开会话。删掉最近那条,就接着剩下里最近的;一块下面删光了就是没问过。
|
||
- 删除:点垃圾桶马上删,不用确认;一块下面删光了这块就不挂了,一份文件删光了文件里就没有这份文件。那条带的图片文件一起删。
|
||
- 找不到原来位置的问答:记号(见下)对不上这份文件里任何一块的问答不显示,也不动它,留在 `code-qa.json` 里;代码改回来、记号又对上了,就又挂回那块下面。
|
||
- 预览重新加载(标签页切走再切回来)后,扩展端还在答的问题接上(展开它那块、接着长),答完照样显示。同一份文件开在几处,问答、正在答的都一样。
|
||
|
||
怎么问(扩展端 `qa/`):
|
||
|
||
- 用 Claude Code 命令行:和插件别处的 `claude -p` 一样找 claude(`resolveClaudeBin`)、清环境变量(`ANTHROPIC_*`、嵌套会话标记)、带问的时候选的 profile(`--settings <它的路径>`,选「默认」不带;每次问都现读 profile 目录,见上面的 profile 下拉),在这份文件所在的工作区文件夹里跑,它能自己读项目里别的文件。
|
||
- 只读,靠三层:`--tools Read,Grep,Glob`(可用的内置工具只剩读、搜,编辑、写文件、跑命令的工具根本没有),`--allowedTools Read,Grep,Glob`(这几个不用再批),`--permission-mode dontAsk`(别的一律拒、不等人批,盖过 profile 里的 bypassPermissions),外加 `--strict-mcp-config` 且不给 `--mcp-config`(不加载任何 MCP 服务,免得 MCP 工具能改东西)。不带 `--dangerously-skip-permissions`。
|
||
- 问题从 stdin 发:`-p --input-format stream-json`,写一条用户消息(一行 JSON,同 `cc/spawnClaude.ts` 带图时的 `buildStreamJsonLine`:先是问题的文字,后面按贴的先后跟着各张图片,`{ type: 'image', source: { type: 'base64', media_type, data } }`)后关上 stdin。没贴图也走这一条路(只有文字),问题不上命令行。追问(`--resume`)和会话找不到时重新问都带上这一问的图片。本机 claude 2.1 试过:带图第一次问、接着会话再带一张图追问,都认得出图、记得上一张;约 6 MB 的 base64 图片照样能答。
|
||
- 输出 `--output-format stream-json --verbose --include-partial-messages`:init 消息给会话 id;逐字的 `text_delta` 拼成回答;工具调用换成「读取 X」「搜索 Y」;新的一条回复(读完文件后开口)一开口就换掉前面那句(「我先看看 X」);答完以 result 行的最终回答为准,`is_error` 当失败。超时 5 分钟;「停止」给进程发 SIGTERM,这次不存。
|
||
- 第一次问的话:要求用中文、大白话、简洁,只读,提到文件写相对项目根目录的路径(带行号);带上文件的相对路径和语言、外面套着的各层块的开头(每层最多 6 行,带行号,例如类和它里面的函数)、这块代码(从块的第一行到它最后一个子孙的最后一行,带行号,超过 120 行只贴开头并说剩下的在哪几行)、问题。追问只发问题和「还是 路径 第 a–b 行那块代码,代码可能刚改过,需要时重新读文件」。这些都只用视图数据的行号、记号和树形,不认识任何语言。
|
||
|
||
块的位置记号(`BlockView.anchor`,`qa/anchor.ts`):
|
||
|
||
- 扩展端生成视图数据时按源码先后逐块给:有名字的块(`defines` 的来源:函数名、类名、枚举名、单个变量名,库规则卡片取规则给的定义范围)是外层各个有名字的块加自己的「种类 名字」,例如 `class ReferralRelation > function display`;没名字的块(条件、语句、返回、注释、导入、多变量声明……)是外层最近的有名字的块加「种类 @源码哈希」,哈希是这块编辑范围的源码去掉所有空白后的 SHA-1 前 8 位,例如 `function display > if @3fa1c2d0`。同一层里记号相同的(同名的两个函数、一模一样的两行语句)按源码先后在后面补 `[1]`、`[2]`。
|
||
- 所以:上面插几行、改缩进空白、改别的函数的名字,记号都不变;有名字的块改了主体、签名还认得;没名字的块自己的源码改了,记号就变了,旧问答成了「找不到原来位置的」(不显示)。种类用视图数据的种类(库规则的卡片是规则的种类 id)。
|
||
- webview 只拿记号比相等,不拆开看。
|
||
|
||
存法:
|
||
|
||
- 每个工作区文件夹一份 `<工作区文件夹>/.spx/code-qa.json`,和 `.spx/code-notes.json` 同一套写法(`pathMap.ts`、`folderStore.ts` 两者共用):`{ "相对路径": [{ "anchor": 记号, "entries": [{ "id", "question", "answer", "createdAt", "sessionId", "profile"?, "images"? }] }] }`,回答是 Markdown 原文,`createdAt` 是 ISO 时间,`profile` 是用哪个 profile 答的(profile 名;选「默认」答的没有这一项),`images` 是问题带的图片的文件名(见下;没带图的没有这一项);文件缺失或解析失败当作没有,认不出的块、问答跳过;写时按需建 `.spx`,两格缩进、文件按路径排、块按记号排、问答按时间排、末尾换行;存第一条前不生成文件,删光后留 `{}`。
|
||
- 问题带的图片(`qa/qaImages.ts`):答成了才写,每张一个文件,放在同一个工作区文件夹的 `.spx/code-qa/` 里,名字是 `<问答 id>-<第几张>.<扩展名>`(`png` / `jpg` / `gif` / `webp`),内容是原图;这个目录第一张图存下时才建。没答成(失败、叫停)不留文件;图片写不进去时这一问算失败(「回答没存上:图片写不进 .spx/code-qa(原因)」),不存这条。`images` 只认这个样子的名字,手改成别的路径(`../x`)的跳过,删的时候也碰不到别处。
|
||
- 图片跟着条目走(`PathMapFile.attachments`,`folderStore.ts` 每次改表写好后删掉不再有条目带着的文件):删一次问答、删源文件(问答跟着删)时图片一起删;改名、挪位置按问答 id 起名,文件不用动;另一条还带着的不删;外部改了 `code-qa.json`(git pull、手改)不删文件。挪到另一个工作区文件夹时问答跟过去,图片留在原来那个文件夹、跟着删掉,过去的那条缩略图就没了。
|
||
- 预览要能读到 `.spx/code-qa/`:打开预览时把这份文件所在工作区文件夹的这个目录加进 webview 的 `localResourceRoots`,发给预览的问答里图片换成 `webview.asWebviewUri` 的地址。
|
||
- 在 VS Code 里改名、挪位置、删除文件 / 目录时问答跟着走(同文件注释);外部改了 `code-qa.json`(git pull、手改)文件监听重读,开着的预览跟着变。
|
||
|
||
## 通用契约
|
||
|
||
放在 `vscode/src/codeReader/contract.ts`,语法树类型用 `@vscode/tree-sitter-wasm` 自带的 web-tree-sitter 类型。
|
||
|
||
```ts
|
||
export type BlockKind =
|
||
| 'import' | 'function' | 'class' | 'enum' | 'exception' | 'type' | 'field' | 'const' | 'variable'
|
||
| 'if' | 'else' | 'loop' | 'try' | 'catch' | 'finally' | 'return' | 'statement' | 'comment'
|
||
|
||
/** 源码里一段的下标(源码字符串下标),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
|
||
}
|
||
|
||
/** 父类的一项: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[]
|
||
}
|
||
|
||
/**
|
||
* 筛选属性的通用词表,筛选栏按这个顺序排: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
|
||
/** 带说明(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)
|
||
}
|
||
|
||
export type CodeBlock = TextBlock | FactBlock
|
||
|
||
/** 着色用的词性,源码着色和译文着色共用。 */
|
||
export type TokenRole =
|
||
| 'keyword' // 流程关键字:import from export if else for return try catch await …
|
||
| 'storage' // 声明关键字与字面量关键字:const function async class interface new true false null …
|
||
| 'operator' // 被翻成文字的运算符:全等 并且 或者 非
|
||
| 'hint' // 属性表里的说明词:未标注 无 N 个 默认导出 …
|
||
| 'variable' | 'function' | 'type' | 'string' | 'number' | 'literal' | 'comment' | 'plain'
|
||
|
||
/**
|
||
* offset:这段照抄自源码里的名字(标识符、属性名、类型名、点分模块名的一节)或字符串时,它在源码里的起始下标,
|
||
* 预览拿它向 VS Code 查定义、跳过去;译词、提示词没有。由从语法树叶子生成这段的一方填(模板直译引擎的译文和属性表);
|
||
* 第三方翻译插件可以不填,不填就不能跳。
|
||
* 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[] }
|
||
|
||
/**
|
||
* 属性表的一行:属性名、着色的值或 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
|
||
/** 认代码里给定义起的中文名(见「中文名」);不填就没有中文名。 */
|
||
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
|
||
}
|
||
|
||
/** 筛选属性和值的显示名;没给名字的显示 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 名字);前 18 个给内置种类,后面的给库规则。规则要新图标就加进这组,没有按任意名字找图标的路。 */
|
||
export const KIND_ICON_NAMES = ['import', 'square-function', /* …18 个内置种类的… */ '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
|
||
}
|
||
```
|
||
|
||
- `activate()` 返回 `{ codeReader: CodeReaderApi }`;其他扩展声明 `extensionDependencies` 后用 `extensions.getExtension('clurdra.superpowers-vscode-clurdra')!.exports.codeReader` 注册。
|
||
- 内置的 TypeScript、Python 语言插件、两个中文直译翻译插件和 SQLAlchemy、Celery、enum、dataclasses、FastAPI 库规则在 `activate()` 里通过同一个 API 注册(规则按这个顺序),没有旁路。
|
||
- 注册表变化时更新 `superpowers.codeReader.languageIds` 上下文,并让已打开的预览刷新。
|
||
- 分工:语言插件只从语法树读出和语言无关的事实(facts,引用源码节点,不做正则匹配文字);翻译插件决定属性名、枚举词(公共 / 是 / 默认导出)和值怎么译,也给筛选属性和值起名(`attributeNames`)。筛选属性的通用部分由宿主从 facts 推出(`attributes.ts` 的 `blockAttributes`),语言插件只在 `BlockShape.attributes` 里补自己语言特有的。第三方语言插件给函数、类、枚举、异常、导入、常量、变量块填 facts 就能用上任何翻译插件的属性表;自己语言的字典 / 映射字面量填进 `DeclaratorFacts.entries` 就能展开成子行(中文模板直译按字面量的节点类型查叫法,见 `ZhTable.mapLiterals`)。写在表达式里的遍历(推导式这类)由语言插件的 `iteration` 给结构(节点 → `IterationFacts`),翻译插件排成一个 `SegmentGroup`(中文直译是样式 `iteration`、框头是结果类型的译名);webview 只认框的样式 id,不认语言。调用由语言插件的 `call` 给结构(节点 → `CallFacts`),跨几行写的由翻译插件排成一行字加一张表(`PreviewTable`,见「调用与构造」);webview 只画表。
|
||
- 语言插件的信息只经契约到翻译插件:宿主翻每一块时把 `{ source, language }`(`SourceDocument`,language 是解析这份源码的语言插件)交给 `translate` / `describe`,翻译插件要词性、遍历结构都从 `language` 取,不 import 任何语言插件的模块(lint 规则挡住 `translators/` 下的非测试文件 import `languages/`)。所以第三方翻译插件(英文、AI 翻译)拿得到和内置翻译插件一样的信息,第三方语言插件也能配上任何翻译插件。
|
||
- 第三方语言插件要给的:`id`、`languageIds`、`grammarPath`、`toBlocks`(块的种类、头部、子块;函数、类、枚举、异常、导入、常量、变量块带 facts)、`tokenRole`。可以给的:facts 里语言有才填的项(async、abstract、typeOnly、access、doc、`DeclaratorFacts.entries`)、`BlockShape.attributes`(通用词表以外的筛选属性)、`iteration`(有写在表达式里的遍历的语言)、`call`(调用的结构)、`displayNames`(这门语言里给定义起中文名的写法)。没给的就少那一样:没有 entries 的值不展开,没有 `iteration` 的遍历照源码结构逐词翻,没有 `call` 的调用不摊成表,没有 `displayNames` 就没有中文名。
|
||
- 中文名的分工:语言插件只认写法(`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 的规则,按下面的先后,第一条对上的算:
|
||
|
||
1. 写了 `Depends(...)` / `Security(...)`(默认值 `= Depends(x)`,或 `Annotated[T, Depends(x)]`):依赖,不是请求带的。
|
||
2. 名字在路径里(`{user_id}`、`{file_path:path}`,前缀和路径都算):路径参数。
|
||
3. 写了 `Path` / `Query` / `Header` / `Cookie` / `Body` / `Form` / `File`(同上两种写法):照写的,`Header` 进「请求头」、`Cookie` 进「Cookie」,`Body` / `Form` / `File` 进「请求体」。
|
||
4. 类型是别处写好的类型别名(`Int4Query = Annotated[int \| None, Query(...)]`、`FilterQuery = Annotated[FilterList, Depends(...)]`):经工作区索引查到它的标记(见下),照 1、3 放。
|
||
5. `Request`、`Response`、`WebSocket`、`HTTPConnection`、`BackgroundTasks`、`SecurityScopes`:FastAPI 自己填的,不列。
|
||
6. 上传文件(`UploadFile`、`list[UploadFile]`):请求体。
|
||
7. 简单类型:查询参数。简单类型是 int、float、str、bool、bytes、complex、UUID、datetime、date、time、timedelta、Decimal、Any、EmailStr、HttpUrl、AnyUrl、SecretStr、`None`、`Literal[...]`、枚举类(别处定义的经索引查),和只包着它们的 `X | None`、`Optional` / `Union` / `list` / `set` / `tuple` / `Sequence` 这些;没标注类型的也是。
|
||
8. 别的(Pydantic 模型、字典……):请求体。
|
||
|
||
`*args`、`**kwargs` 不算。
|
||
|
||
响应表(同一个状态码的几处写在一行里:内容「、」隔开、去重,说明「;」隔开、去重):
|
||
|
||
- 成功那一行:装饰器的 `status_code`(整数,或 `status.HTTP_201_CREATED` 这种常量),没写是 200;内容是 `response_model`,没写就用返回类型标注(`response_model=None`、`-> None` 没有内容);说明是 `response_description`,没写是「成功」(comment 色)。
|
||
- 路由对象和装饰器的 `responses={...}`:每一项一行,键是状态码(整数、`"404"`、常量;`"5XX"`、`"default"` 照写);值是字典时 `"model"` 是内容、`"description"` 是说明,值不是字典时整个是内容;`**x` 不列。
|
||
- 函数体里直接抛的异常(嵌套的函数、类、lambda 里的不算,只写 `raise` 的不算,`raise X from e` 算,调别的函数间接抛的不算)按状态码归:`HTTPException`(从 fastapi 或 starlette 导入)的状态码是 `status_code=` 或第一个位置参数,消息是 `detail=` 或第二个位置参数;别的类的状态码是类上的 `status_code = N`,经工作区索引查(下面),消息是第一个位置参数(或 `message=`、`detail=`)。内容是异常类名(type 色,带下标,⌘ + 点击到类的定义),说明是消息:字符串照写(comment 色),别的写法交给翻译插件翻。查不到状态码的异常(没查完、不是 HTTP 异常)不列。
|
||
- 有参数或请求体(路径 / 查询 / 请求头 / Cookie / 请求体里有一项)时,422 那一行加上「参数校验失败 \| FastAPI 自动返回」(hint 色,淡)。
|
||
- 状态码的颜色:小于 400 的绿色(暗 #2ea043 / 亮 #1a7f37,段的 `color`),别的 string 色;不是三位数字的照写、string 色。
|
||
|
||
别的文件里的定义(索引 `fastapi.names`,见「规则认领时查别的文件里的定义」):看工作区里所有 `**/*.py`,预筛有 `status_code`、`Annotated` 或 `Enum` 才解析;从模块顶层认出:类上写了 `status_code = N`(带不带注解、`status.HTTP_403_FORBIDDEN` 这种常量也算)的类 → 值是状态码;父类是 enum 的枚举类型(Enum、IntEnum、StrEnum、Flag、IntFlag)的类 → `Enum`;`X = Annotated[T, Query(...)]` 这种类型别名 → 标记名(`Query`、`Path`、`Depends`……)。父类继承下来的 `status_code`(`class AccountFrozen(Forbidden)` 没自己写)不认。
|
||
|
||
样例(`fixtures/routes.py` 的 `update_user`,`Forbidden` 定义在 luffy 的 `core/exceptions/schemas.py`(`fixtures/appExceptions.py`),`status_code = 403`;测试里逐字核对):
|
||
|
||
```
|
||
名称: update_user
|
||
说明: 更新用户信息。…
|
||
路由器: router
|
||
标签: 用户管理
|
||
异步: 是
|
||
依赖: get_db(db)、get_current_user(current_user)
|
||
请求: PUT /users/{user_id}
|
||
URL | /users/{user_id}
|
||
方法 | PUT
|
||
路径参数 | user_id: Int4Path
|
||
请求体 | UserUpdateRequest
|
||
响应: 200 / 403 / 422
|
||
200 | UserDetailResponse | 成功
|
||
403 | Forbidden | 无权更新此用户信息;无权修改角色;无权修改启用状态;无权修改密码
|
||
422 | 参数校验失败 | FastAPI 自动返回
|
||
```
|
||
|
||
`skill_hub/routes.py` 的 `list_skills`:`查询参数 | query: 字符串 | 空, tag: 字符串 | 空, skip: 整数, limit: 整数`,依赖 `get_db(db)、get_current_user`(参数名是 `_`);`user/console/routes.py` 的 `create_user`:成功那一行 `201 | UserDetailResponse | 成功`,请求体 `UserCreateRequest`。
|
||
|
||
配色:名称 function、标签 string、依赖名 function、「(参数名)」hint、表格第一列(项)hint、URL 同上、参数名 variable、类型按翻译插件、异常类名 type、消息和说明 comment、自动带的 422 hint。
|
||
|
||
## 解析运行时
|
||
|
||
- 依赖 `@vscode/tree-sitter-wasm`(VS Code 团队维护,包含 web-tree-sitter 运行时和 TypeScript / TSX / JavaScript / Python 语法包,版本配套)。
|
||
- 打包:`esbuild.mjs` 构建时把 `tree-sitter.wasm` 和用到的语法包复制到 `dist/tree-sitter/`(`package.json` 的 `files` 已包含 `dist/**/*`)。
|
||
- 运行时初始化一次 `Parser.init({ locateFile })` 指向 `dist/tree-sitter/tree-sitter.wasm`;语法包按路径懒加载并缓存。
|
||
- 文件扩展名到语法包:`.ts` → typescript;`.tsx` → tsx;`.js/.jsx/.mjs/.cjs` → javascript(tree-sitter-javascript 支持 JSX);`.py` → python。以 `languageId` 为准。
|
||
|
||
## 代码块树规则
|
||
|
||
### TypeScript / JavaScript
|
||
|
||
- 顶层与语句块内的每条语句一块;连续的行注释合成一块注释。
|
||
- JSDoc(以 `/**` 开头的块注释,`/**/` 不算)和下一条语句之间只有空白(空行也算,中间隔着装饰器也算)、那条语句是函数、类或枚举块时,它是这个函数、类、枚举的说明:进 `facts.doc`,头部往上延到注释开头,不单独成块。其余情况(后面不是函数、类、枚举块,中间隔着别的注释,在上一条语句的行尾)照旧是注释块。`//` 行注释和普通 `/* */` 块注释不变。
|
||
- `import_statement` → import。
|
||
- `export_statement` 包着声明 → 按声明定种类,节点取整个 `export_statement`(头部包含 `export`)。
|
||
- `function_declaration` / `generator_function_declaration` / `method_definition` → function,子块来自主体语句。
|
||
- 变量声明的唯一声明值是带语句块主体的箭头函数 / 函数表达式 → function(React 组件、回调常见写法),子块来自主体;是类表达式(`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。
|
||
- 复合语句头部到主体开始为止(包含行尾冒号,翻译时去掉)。
|
||
|
||
### 属性表信息(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.12 `type_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__`。
|
||
- Python 变量:`assignment` 的 left / type / right,说明是紧跟在后面的属性 docstring 的字符串节点(见上面代码块树规则);right 是 `dictionary` 时 entries 是它的具名子节点(注释除外):`pair` 的 key / value(value 又是 `dictionary` 时连它的 entries)、`dictionary_splat` 里的表达式。
|
||
|
||
## 中文模板直译
|
||
|
||
结构照抄源码(按节点模板重排的语法结构除外,见下文),只替换三类东西,其余(标识符、属性名、字面量、标点)原样保留并按词性着色:
|
||
|
||
1. 关键字:只认语法树里的关键字节点(匿名节点),所以属性名 `i.number`、对象键不会被误翻。
|
||
2. 内置类型:只在类型位置翻(TS 的 `predefined_type`、Python 注解里的内置类型名)。
|
||
3. 运算符:`===` 全等、`!==` 不全等、`==` 等于、`!=` 不等于、`&&` 并且、`||` 或者、`!` 非(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`)时按「调用与构造」一节排(`emitCall`、`argumentTable`),属性表别的值、库规则借的 `expression` 照常一行。
|
||
|
||
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:
|
||
|
||
```ts
|
||
interface BlockViewBase {
|
||
id: string // 在树里的位置,例如 '6.1.0'
|
||
anchor: string // 块的位置记号(见「问 AI」):问答挂在它上面,webview 只比相等
|
||
kind: string // BlockKind(库规则认成的现成块也是,如 enum),或库规则卡片的种类 id(sqlalchemy.table)
|
||
startLine: number // 编辑范围的起止行,0 起:一般是 header,库规则认领的块是原块整条语句(盖住它留下的子块)
|
||
endLine: number
|
||
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[]> }`:整个工作区共用的块筛选。webview 发 `ready` 后先于第一份文档发一次(第一份文档按筛选画),之后任何一个预览改了都发给所有开着的预览(包括改的那个)。
|
||
- `{ 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: 'ask', askId, anchor, question, images: { mediaType, base64 }[] }`:问 `anchor` 那块;`askId` 由 webview 起;`images` 是贴进输入框的图片(没贴是空的,`base64` 不带 `data:` 前缀)。扩展端按最近一次发出去的块树和源码找这块、拼问题,这块存过问答就接着最近那条的会话追问(见「问 AI」的追问),用扩展端记着的 profile;找不到(代码刚改过)、图片格式不认或问不了(这块正在答、同时答的太多)时直接回 `answerEnd` failed。
|
||
- `{ 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` | 同左 |
|
||
| 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(虚线框);库规则的种类用规则给的颜色(`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(源码着色、去缩进、调用翻译)
|
||
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 的规则放,异常类的状态码、类型别名、枚举经索引查
|
||
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/typescriptNames.ts TypeScript / JavaScript 的中文名写法(@Comment 装饰器、行尾记号、JSDoc)
|
||
translators/zhTemplate.ts 模板直译引擎(按语言的表驱动,词性、遍历结构从 SourceDocument.language 取;推导式排进框见 iterationGroup)
|
||
translators/typescriptZh.ts TS 表
|
||
translators/pythonZh.ts Python 表(含注解位置的判断)
|
||
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:选工单弹框、进出审查、刷新、按工作区记住
|
||
(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)
|
||
```
|
||
|
||
## 测试
|
||
|
||
- 语言插件:`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 关键字参数、下划线私有。
|
||
- 块头部:各样例里块的头部行互不重叠;函数、类说明所在的行在函数、类块里,「编辑」写回原样不变。
|
||
- 折叠:默认函数、类、枚举、异常块和库规则的卡片折叠、其余展开;用户的选择盖过默认、重新加载后还在(嵌套子行的也是);各层子行的 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、装饰器和父类列表不算;TypeScript `new` 是新建、被调用的不含 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 是调用、都不问定义;链式调用跨了几行、回调主体是子块的调用不排。`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」,没有说明列),改文档时中文名一帧都没少。
|
||
- 注册表:注册 / 注销后 `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`):卡片编辑时盖住它留下的子块(字段中间的方法连同主体),跳到被盖住的块改闪卡片;只编辑头部的块不盖住子块,一行写完的块盖住同一行的子块;点「编辑」时重叠的块退出编辑;任意点法下正在编辑、画出来的块两两不重叠。
|
||
- 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.py` 16 个类都是异常、只有说明的四行、写了 `__init__` 的构造参数;认哪些类(自带的异常类、`Warning`、不合后缀的自带类 `StopIteration` / `KeyboardInterrupt`、`pkg.errors.AppException`、同一文件里写在后面的父类一层层往下传、泛型和别的父类混写,关键字参数、`Mapping[...]`、`*bases` 不算,函数里的类也认);类属性(带不带注解、只写注解、说明取上面的注释块 / 行尾注释 / 属性 docstring,连着赋值、解构赋值、方法、讲方法的注释、最后面的注释留作子块),构造参数去掉 self、`*args` 开头的不去;`@dataclass` 的异常类照样是异常卡片;编辑范围是整个类、改卡片写回整个类,类名、属性名、参数名的定义范围和带下标的词指回源码原文;TypeScript `fixtures/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 号认得,只留「审查」列的 Gitea 工单)、都没有或快照坏了是 undefined。
|
||
- 规则和字(`review/reviewRules.test.ts`):标题栏的 description / message;「审查」列的筛选(只要 Gitea 的)和次序,PR 号只认正整数;工单没了、离开「审查」列、没有 PR,PR 没了、合并了、关了的原因。
|
||
- 菜单(`explorer/rowMenus.test.ts`):标题栏先「选工单」、审查时才有「显示全部」、再「刷新」,字和图标;审查时注释按钮不出、复制照旧;命令面板里「显示全部」只在审查时有。
|
||
- 工作副本(`test/git/reviewWorktree.test.ts`,真的 git:bare 的 origin 上推 `refs/pull/7/head`):拉 PR 的提交建不挂分支的 worktree(父目录不在也建、本机不多出分支),目录手删后同一路径能再建;快进:PR 往前走了且干净 → 快进,已经最新 / 本地在它之上 → 不动,只有未跟踪的文件照样快进,有没提交的改动、本地另有提交 → 不动且改动留着,目录不是工作副本的根 → 报错、外面的仓库不动。
|
||
- 复制出什么(`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`。
|