# 代码阅读(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 只认「框」不认语言 | ## 用户可见行为 ### 活动栏与侧栏 - 活动栏新增图标(书本,`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 #`(`description`);第二行(`detail`)`<文件数> · 本机有工作副本` 或 `<文件数> · 本机没有工作副本,选中后拉取 PR 分支`。文件数先写 `正在读改动文件数…`,读到换成 `改了 N 个文件`(`listPullRequestFiles`,各项一起读),没读到写 `读不到改动文件数`、原话记进日志。每补一项整组换一次 `items`,光标按工单号留在原来那项上。本机有没有工作副本按盘上现在的看。 - state JSON 里没有 PR 号的工单也列出来,第二行只写 `没有关联 PR`;选中它只提示 `#<工单号> 没有关联 PR`,什么都不变。 - 当前工作区没有 Gitea 仓库、没配 token:关掉弹框,报错。 - 日志(Superpowers 输出通道,来源 `codeReader`):列了几张、数据来自看板这次加载的还是存下的快照;哪张的文件数没读到和原话;进审查的每一步、不进的原因、出错。 - 选中后进审查:先重读这张工单(看板的单张加载 `loadSingleIssue`,弹框里的是看板上摆着的,可能旧了),确认它还在「审查」列、有 PR,PR 还开着(没了、合并了、关了都不进,提示原因),再读 PR 改过的文件、备好工作副本,都好了才换树。这期间视图上转进度(`withProgress` 的 `viewId`),标题下面那行写正在做哪一步(`正在读取 PR #447 改过的文件…`、`正在拉取 PR #447 的分支…`)。出错:报错,树不换(原来在审查别的工单就还是那张)。 - 工作副本(PR 最新的代码): - 本机有这张工单的工作副本就用它:看板记着的 worktree(`worktreePath`,盘上还在)优先,其次是之前审查它时建的。用之前先 `git fetch origin refs/pull//head`(Gitea 给每个 PR 留的引用),能不丢东西时快进到 PR 最新的提交(`headSha`):有没提交的改动(未跟踪的文件不算)、本地另有 PR 之外的提交时照原样用;拉取、快进出错也照原样用,提醒一声。目录必须是工作副本的根,否则不动(`git -C` 会找到外面的仓库上去)。 - 没有就拉 PR 的分支(同上的 `refs/pull//head`)建一个:和实施的 worktree 同一个目录模板(插件设置里的 worktree 目录,`$feature_name` 换成 `review-<工单号>`),停在 PR 最新的提交上、不挂分支(`git worktree add --detach`),不和实施用的 `feature/…` 分支抢名字。不写进工单的本机记录:看板上删 worktree 会连带删工单的 feature 分支,审查建的这个不该走那条路。 - 审查时的树:根只有一个,是工作副本,名字照写工作区文件夹的;下面只有 PR 改过的文件和通往它们的目录,目录层级照留、全部展开,每层排法同普通的树。节点照旧用 `resourceUri`(工作副本里的文件),点文件和平时一样打开(受支持的语言用预览),打开的就是 PR 最新的代码。 - 改动的颜色:`FileDecorationProvider` 挂在这些文件的 Uri 上,颜色用 VS Code 源代码管理的主题色——修改 `gitDecoration.modifiedResourceForeground` 徽标 `M`,新增 `gitDecoration.addedResourceForeground` 徽标 `A`,删除 `gitDecoration.deletedResourceForeground` 徽标 `D`;目录不上色。Gitea 的状态 `added` → A,`deleted` → D,别的(`changed`、改名的 `renamed` 在新路径上、`copied`……)→ M。编辑器标签页上打开的这些文件也带着,退出审查就清掉。 - 删掉的文件:列出来但打不开(没有命令),悬停提示 `PR 里删掉了`。原型把名字划掉,VS Code 的树行没有删除线,这是和原型的有意差异,只靠红色和 `D`。 - 标题栏:视图 `description` 写 `审查 #<工单号>`(标题后面淡色),`message` 写 `#<工单号> <标题> · PR # · 改了 N 个文件`;上下文键 `superpowers.codeReader.reviewing` 为真时出「显示全部」(`$(close)`,`navigation@2`,命令 `superpowers.codeReader.showAllFiles`,命令面板里也只在审查时有),点了回到工作区文件树。真实的视图没有新建按钮,原型里被「显示全部」换掉的那两个不用管。 - 行上的按钮:「复制」照旧(相对路径相对工作副本的根,就是仓库里的路径);注释按钮审查时不出(`when` 带 `!superpowers.codeReader.reviewing`):注释记在工作区文件夹里,工作副本不是工作区文件夹。 - 刷新:审查时重读工单和 PR 改过的文件,工作副本照上面的规则更新;工单离开「审查」列、PR 不能看了就提示原因、回到工作区文件树。 - 记住:选中的工单号存 `workspaceState`(`superpowers.codeReader.reviewIssue`),重开窗口后视图第一次露出来时接着审查(没打开侧栏不去拉);工单离开了「审查」列、没了 PR、PR 没了 / 合并了 / 关了,悄悄回到工作区文件树并忘掉;读的时候出错,报错后同样回去。 - 代码:`review/`。纯函数(改动状态、改动的文件树、能不能审查、标题的字、弹框的各项)在 `changedFiles.ts`、`reviewRules.ts`、`pickList.ts`;看板数据在 `reviewSource.ts`(看板上摆着的工单由看板给:`panel/kanbanSnapshot.ts` 的 `boardIssues`;仓库和 token 与看板「改动」tab 共用 `gitea/repoContext.ts`);工作副本在 `reviewWorktree.ts`(git 操作在 `git/worktree.ts`);着色在 `changeDecorations.ts`;弹框、进出审查、记住在 `reviewMode.ts`。文件树只多一个「换成这些文件」的入口(`showChanges`)。 ### 编辑器右上角切换 - 受支持语言的文件:普通编辑器里标题栏显示「预览」,预览里显示「编辑」。点一下在**同一个标签页位置**换成另一种:用 VS Code 内部的 `reopenActiveEditorWith`(「重新打开编辑器的方式」背后的机制)原位替换。不能用「另开再关原标签」:文件有未保存改动时关原标签会弹保存框,选「不保存」会丢改动。 - 用 `editor/title` 的 `navigation` 组、不带图标的命令,让按钮显示文字。若实测 VS Code 不渲染文字,改成带图标(`$(open-preview)` / `$(go-to-file)`),标题保持「预览」「编辑」。VS Code 标题栏做不出原型里的分段控件,这是和原型唯一的有意差异。 - 受支持语言 = 当前注册的语言插件覆盖的 `languageId`,通过 `setContext('superpowers.codeReader.languageIds', [...])` 提供给 when 子句(`resourceLangId in superpowers.codeReader.languageIds`)。 ### 预览(代码块树) - 每一句是一块:导入、函数、类、枚举、异常、类型、字段、常量、变量、条件、否则、循环、尝试、捕获、最终、返回、语句、注释。 - 嵌套的块放在父块下面、往右缩进;每一层左边一条竖线(颜色 `--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 限定为 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`,写到一半缺了变量 / 来源 / 条件 / 值的)照常逐词直译。 ### 注释的 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 不放行外部图片),`![alt](url)` 显示成 `!` 加链接。 - 样子:正文用界面字体(`--vscode-font-family`、`--vscode-font-size`)、comment 色、行高 20px,一行注释和一行译文一样高;行内代码用编辑器字体、`--vscode-textCodeBlock-background` 底;围栏代码块用编辑器字体和字号、同样的底,长行折行;段落、列表、代码块、表格之间 6px;标题只比正文稍大(h1 1.15em、h2 1.08em,其余同正文,加粗);链接 `--vscode-textLink-foreground`;引用左边 3px `--vscode-textBlockQuote-border` 竖线、`--vscode-textBlockQuote-background` 底;表格 1px 细线。 - 「编辑」状态照旧是源码,编辑写回不变;Markdown 里的名字不能点,不参与跳到定义。 ### 跳到定义 - 能点的:预览里(属性表的名称、类型、来源、导入项、默认值、值、字典 / 对象各层子行的键和值、类型参数、父类、实现的接口,以及一行译文)照抄自源码的名字——标识符、属性名、类型名、点分模块名的每一节(`luffy_agent.core.db` 三节各自能点,和 VS Code 一样)、字符串(TypeScript 的模块路径 `'./x'` 就是字符串)。翻出来的关键字、运算符、说明词不能点。 - 链接和编辑器里一样:只有按住修饰键(macOS ⌘,其余平台 Ctrl)、鼠标停在**查得到定义**的名字上时,名字加下划线、换成链接色(`--vscode-editorLink-activeForeground`)、光标变手形;松开修饰键或移开就恢复。有没有定义问扩展端(`vscode.executeDefinitionProvider`,文档 uri + 名字在源码里的位置;库规则标了引用的名字问规则,见「规则自己查定义」,webview 不分这两种);和编辑器一样,每回指到一个名字上(或指着名字按下修饰键、换了文档版本)都现问,回答不留到下次用——Pylance、tsserver 刚启动那几秒会答「没有」;只认最近一次询问的回答,迟到的丢掉。 - ⌘ / Ctrl + 点击: - 一处定义,在当前文件:原地滚过去; - 一处定义,另一个文件,语言有代码阅读:在同一个编辑器组用预览打开(`vscode.openWith`),滚过去;这个文件的预览已经在这一组开着就切过去、发给它,不重新打开; - 一处定义,语言没有代码阅读:在同一个编辑器组用普通编辑器打开,选中定义处的名字; - 几处定义:先弹挑选框(每项「相对路径:行号」,下面一行是那行源码),挑一处再按上面跳; - 没有定义:不动。 - 和普通编辑器里 ⌘ + 点击一样:目标是否按「预览标签」打开听 `workbench.editor.enablePreview`;`workbench.editor.enablePreviewFromCodeNavigation` 为假(默认)时先把当前预览标签固定住,免得被目标替掉。 - 闪哪一处:页面上**就是这处定义**的最具体的那一处,只按源码位置判断(定义提供者给的定义处下标,对上块和属性行带的源码范围),不看属性名文字: 1. 定义是块自己的名字(函数名、类名;只声明一个变量时的变量名,解构模式整个算)→ 整块。块的这段范围由扩展端从 facts 取(`BlockView.defines`),和翻译插件无关。 2. 定义是属性表里某一行 / 子行讲的东西(参数、导入项、一条语句声明几个变量时的某一个、类的泛型参数、对象字面量一项的键——TypeScript 对象的属性名就是定义,嵌套几层都算)→ 只闪这一行 / 子行。范围是翻译插件从 facts 取的节点(`PropertyRow.defines`、`PropertyItem.defines`;字典 / 对象的项标的是键,展开项不标),定义处落在范围里就算(`*args` 的定义在 `args` 上、解构参数里的名字都对得上)。 3. 其余(定义在一行译文里,例如循环变量;翻译插件没填 `defines`)→ 显示着定义处那个名字的块,名字在属性行的子行里时展开那一行(在嵌套的子行里时连它上面各层一起展开);页面上找不到这个名字就取包含定义那一行的块。 - 同一文件原地跳、跳进另一个文件的预览,规则一样。 - 滚过去:目标所在块的祖先块折叠着就展开,目标在折叠的属性行子行里(参数、导入项、字典 / 对象的项)就展开那一行,在嵌套的子行里就连它上面折叠着的各层子行一起展开。目标已在视口里不滚,否则滚到视口正中(比视口高就顶部对齐)。然后目标的边框和底色往聚焦色(`--vscode-focusBorder`)闪三下,共约 1.5 秒后复原;同一处再跳一次从头闪;系统开了「减少动态效果」时改成静止高亮 1.5 秒。目标是属性行 / 子行但块正在编辑(页面上没有属性表)时闪整块。 - 不按修饰键单击名字是高亮它的各处出现(见下一节);⌘ / 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` 的 Base(表达式)和它后面的泛型实参 ;Python 类参数列表的一项(含 metaclass=…、*bases)。 */ export interface BaseFacts { value: Node, typeArguments?: Node } export interface ClassFacts { name?: Node // 匿名类没有;赋给变量、类字段的类表达式取变量名、字段名 doc?: Node // 同 FunctionFacts.doc access?: Access // 嵌套在语句块里的类没有 abstract?: boolean // 只有 TypeScript 填(JavaScript 不填) decorators: Node[] typeParameters: Node[] // 每个泛型参数一项 bases: BaseFacts[] implements: Node[] // TypeScript 的 implements,每个接口一项 } /** 枚举的一个成员。 */ export interface EnumMemberFacts { name: Node value?: Node // 源码里写的值;没写(TS 自增)、交给库自动取(Python auto())时没有 computed?: number // 没写值、语言算得出来时的值(TS 数值枚举自增 0、1、2…) docs: Node[] // 说明节点,照源码顺序:紧挨在上面单独成行的注释 / JSDoc、这一行行尾的注释、Python 的属性 docstring } /** 枚举:TS 的 enum 由语言插件报;Python 自带 enum 库的类由库规则认成这种块(RuleFacts)。 */ export interface EnumFacts { name: Node doc?: Node // 同 ClassFacts.doc access?: Access // 同 ClassFacts.access decorators: Node[] bases: BaseFacts[] // 照写的父类(Python 的 str, enum.Enum);TS 没有 members: EnumMemberFacts[] } /** 异常类的一个类属性:Python 类主体里给一个名字赋值的语句,TS 的类字段(含 static)。 */ export interface FieldFacts { name: Node type?: Node value?: Node docs: Node[] // 同 EnumMemberFacts.docs } /** 异常类:Python、TS 都由语言插件报(见「异常」)。 */ export interface ExceptionFacts { name?: Node // 同 ClassFacts.name doc?: Node // 同 ClassFacts.doc access?: Access // 同 ClassFacts.access decorators: Node[] bases: BaseFacts[] // 同 ClassFacts.bases fields: FieldFacts[] // 类属性,照源码顺序 parameters?: ParameterFacts[] // 构造函数的参数(Python 的 __init__ 去掉 self,TS 的 constructor);没写构造函数时没有 } export type ImportItem = | { kind: 'named', name: Node, alias?: Node } // import { a as b }、from m import a as b | { kind: 'default', alias: Node } // import x from 'm' | { kind: 'namespace', alias?: Node } // import * as x、from m import * | { kind: 'module', module: Node, alias?: Node } // import a.b as c、import x = require('m') export interface ImportFacts { sources: Node[] // 来源模块的写法;Python 的 import a, b 有多个 typeOnly?: boolean // 只有 TypeScript 填(JavaScript 不填) items: ImportItem[] } /** 字典 / 对象字面量里的一项,照源码顺序;注释不算。 */ export type EntryFacts = | { kind: 'pair', key: Node, value: Node, entries?: EntryFacts[] } // 'a': 1、a: 1、[k]: v;TS 简写 { a } 键和值是同一个节点;entries 同 DeclaratorFacts.entries | { kind: 'spread', value: Node } // **other、...other:value 是展开的表达式 | { kind: 'method', name: Node, node: Node, header: SourceSpan } // 对象里的方法、getter、setter:header 同 BlockShape.header,只到主体开始之前 /** 写在表达式里的遍历(Python 推导式、生成器表达式)得到什么。 */ export type IterationResult = 'generator' | 'list' | 'set' | 'dict' /** 遍历的一节,照源码顺序:从来源里逐个取出变量(async:异步地取),或只留下满足条件的。 */ export type IterationClause = | { kind: 'for', variables: Node, source: Node, async: boolean } | { kind: 'if', condition: Node } /** 写在表达式里的遍历的结构:语言插件读出,翻译插件按自己的语序重排;各节、取的值里还能再套遍历。 */ export interface IterationFacts { result: IterationResult clauses: IterationClause[] // 至少一节,第一节是 for value: Node // 每一项取的值 key?: Node // 结果是字典时每一项的键 sharedParentheses?: { open: Node, close: Node } // 遍历的括号同时是外面调用的括号(Python f(x for x in y)):写在遍历外面 } export 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 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 : never : never export type TextKind = Exclude /** 预览成一行译文的块(注释块是 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[] } /** 语言插件:语法树 → 代码块树,外加只有它懂、翻译插件翻表达式时要用的信息(叶子的词性、写在表达式里的遍历的结构)。 */ 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 /** 认代码里给定义起的中文名(见「中文名」);不填就没有中文名。 */ displayNames?: DisplayNameReader } /** 代码里给一个定义起的中文名:name 是定义处那个名字,text 是中文名。 */ export interface DisplayName { name: SourceRange, text: string } export interface DisplayNameFacts { names: DisplayName[] // 起了中文名的定义,照源码顺序;一个定义几处都写了时语言插件定取哪个 markers: SourceRange[] // 写在注释里给代码阅读看的记号(`@Comment 当前用户`):翻注释、说明时去掉 } export interface DisplayNameReader { files: Record // languageId → 工作区里这种语言的文件(glob):宿主建中文名索引 mentions: (source: string) => boolean // 便宜的文字预筛 find: (root: Node, source: string, languageId: string) => DisplayNameFacts } /** 翻译插件翻一块时拿到的这份源码:源码字符串,解析它的语言插件,和要从注释、说明里去掉的记号。 */ export interface SourceDocument { source: string, language: LanguageAdapter, markers: SourceRange[] } /** 筛选属性和值的显示名;没给名字的显示 id。 */ export interface AttributeNames { attributes: Record // access → 访问级别 values: Record> // access → public → 公共、async → true → 是、kind → function → 函数 } /** 翻译插件:代码块 → 一行译文或属性表。 */ export interface Translator { id: string // 'typescript.zh-template' label: string // '中文直译' languageIds: string[] displayLanguage: string // 译文用的语言('zh'):库规则按它挑自己的词表 translate: (block: TextBlock, document: SourceDocument) => PreviewText // 只翻 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 // 第一次查时建,照文件、行排好 } 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 // 查自己标的引用;没给就不能标引用 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,不认语言。 - 语言插件的信息只经契约到翻译插件:宿主翻每一块时把 `{ 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`(有写在表达式里的遍历的语言)、`displayNames`(这门语言里给定义起中文名的写法)。没给的就少那一样:没有 entries 的值不展开,没有 `iteration` 的遍历照源码结构逐词翻,没有 `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` 拿到)认出来,引擎按「推导式」一节的规则排进框里;和节点模板一样,没整个落在要翻的这一段里就不重排、照常翻。 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 {`(envLock.ts 18–23 行,连同前面的 JSDoc) | 名称 findEnvFiles / 说明 `Scan the workspace folder rooted at …`(三行原文照留,按 Markdown 排成一段)/ 访问级别 公共 / 异步 是 / 输入参数 1 个:`workspaceRoot \| 字符串 \|` / 输出参数 `Promise<字符串[]>` | | `async function chmodAll(workspaceRoot: string, mode: number): Promise {` | 名称 chmodAll / 访问级别 私有 / 异步 是 / 输入参数 2 个:`workspaceRoot \| 字符串`、`mode \| 数值` / 输出参数 `Promise` | | `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 { … }, 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 extends Base implements Loader, Disposable {`(classes.ts 3–9 行,连同前面的 JSDoc 和 `@Injectable()`) | 名称 Repository / 说明 `仓库基类。…` / 访问级别 公共 / 抽象 是 / 装饰器 `@Injectable()` / 类型参数 `K, T 限定为 Entity = Entity` / 父类 `Base<字符串>` / 实现的接口 `Loader, 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 // 筛选属性(含 kind),见「筛选」 badge?: KindBadge // 库规则的卡片才有:种类的名字、颜色、图标;webview 按它画图标、上色、默认折叠 children: BlockView[] } /** * 属性表块(有 facts)和库规则认领的块只带 properties,其余块只带 translation(注释块是 `{ markdown }`);没有翻译插件时是空数组。 * 一行译文的块带说明(TextBlock.doc)时 doc 是翻译插件给的 Markdown 原文(Translator.doc),webview 排在译文下面。 */ type BlockView = BlockViewBase & ({ translation: PreviewText, 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, 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) rules/pythonTree.ts Python 库规则共用的读语法树的办法(名字最后一节、字符串内容、调用的参数、别名、注解里的 X[...]、库里的名字) rules/segments.ts 库规则共用的拼属性表文字的办法 rules/children.ts 库规则共用的挑子块的办法(成员连同紧挨在它前面的注释块、成员的说明节点) languages/shared.ts 语言插件共用的建树工具;异常类(withExceptions:按整份文件认异常类,类块换成异常块,并类属性、取构造参数) languages/typescript.ts (TYPESCRIPT_EXCEPTIONS:认异常类的父类名、类字段、constructor) languages/python.ts (iteration:推导式的结构;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 不认。 - 只经契约:只照 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`,父类名不合习惯的照常是类)、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 --profile augment`。