✨ feat(vscode): 代码阅读加异常卡片,Python、TypeScript 的异常类画成同一张表(属性、构造参数),方法照常是子块

This commit is contained in:
cruldra committed 2026-09-27 10:42:10 +08:00
1 parent d5a092dd6f
commit 0afd012c94
21 files changed
+1118 -66

No files matched your search

+88 -18
View File
@@ -20,7 +20,7 @@
| 块筛选 | 预览顶部钉住一条筛选栏,按块的属性(种类、访问级别、异步……)筛;属性由语言插件按和语言无关的 id 报(通用的由宿主从 facts 推),名字由翻译插件起,webview 不认识任何语言;筛选整个工作区共用、记住 |
| 侧栏点文件 | 支持的语言默认打开成「预览」;不支持的语言用普通编辑器打开 |
| 块内修改 | 改的就是文件本身:未保存标记、⌘S 保存、⌘Z 撤销都照常,切回「预览」按新代码重新翻译 |
| 折叠 | 有子块的块、属性表里带子行的行、带下一层的子行(字典 / 对象里嵌套的字典 / 对象)能折叠;函数块(含方法、嵌套函数)、类块、枚举块和库规则的卡片默认收起主体、属性表照常显示,其余块、属性行和子行默认展开;用户点过的按他点的记住 |
| 折叠 | 有子块的块、属性表里带子行的行、带下一层的子行(字典 / 对象里嵌套的字典 / 对象)能折叠;函数块(含方法、嵌套函数)、类块、枚举块、异常块和库规则的卡片默认收起主体、属性表照常显示,其余块、属性行和子行默认展开;用户点过的按他点的记住 |
| 函数、类的说明 | TypeScript 紧贴在函数、类前面的 JSDoc,Python 函数、类主体开头的 docstring 是它自己的说明:进属性表「说明」一行,不再单独成一块注释;它的源码行算在函数、类块的头部里,「编辑」时和签名一起改 |
| 变量、字段的说明 | Python 模块、类主体里紧跟在一条赋值后面的字符串(属性 docstring,PEP 257 / Sphinx 的写法)是这个变量的说明:常量、变量块进属性表「说明」一行(紧跟名称),字段块排在一行译文下面;不再单独成块,它的源码行算在这一块里,「编辑」时和赋值一起改 |
| 注释按 Markdown 显示 | 注释块和函数、类、变量、字段的「说明」按 Markdown 渲染,像 Kotlin 的 KDoc:去掉注释符号、引号和共同缩进后的原文原样交给 Markdown,不按语言转换(reStructuredText 的 `:func:`、JSDoc 的 `@param` 都照抄,注释以后慢慢改成 Markdown 写法);段内单个换行是软换行,空行分段 |
@@ -35,6 +35,7 @@
| 输入参数 | 排成带表头和边框的表格:名称 \| 类型 \| 默认值,表头写了「默认值」,格子里不再写「默认 」 |
| 库规则 | 第四种插件:按库认出一种写法,把那块整个换成专门的卡片(内置 SQLAlchemy 规则:模型类 → 「数据表」卡片;内置 Celery 规则:`Celery(...)` → 「Celery 应用」卡片、`beat_schedule` → 「定时任务」卡片),或把那块认成契约里现成的一种属性表块(内置 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 只认「框」不认语言 |
@@ -84,18 +85,18 @@
### 预览(代码块树)
- 每一句是一块:导入、函数、类、枚举、类型、字段、常量、变量、条件、否则、循环、尝试、捕获、最终、返回、语句、注释。
- 每一句是一块:导入、函数、类、枚举、异常、类型、字段、常量、变量、条件、否则、循环、尝试、捕获、最终、返回、语句、注释。
- 嵌套的块放在父块下面、往右缩进;每一层左边一条竖线(颜色 `--vscode-editorIndentGuide-background`)。
- 块的组成(从左到右):折叠箭头(只有有子块的块才显示,其余占位对齐)、种类标签(字取翻译插件给 `kind` 值起的名字,见「筛选」)、内容、右上角一排图标按钮。
- 右上角的图标按钮(原型组件 `Reader / BlockTools`,普通块、属性表块、库规则的卡片共用):只有图标、不带字,名字写在悬停提示(`title`)和 `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 能走到,回车 / 空格切换。
- 种类标签和底色按种类区分(颜色见下表);注释块虚线框。
- 内容两种状态,每块单独切:
- 预览:函数、类、枚举、导入、常量、变量块是属性表(见下节),注释块是 Markdown 说明(见「注释的 Markdown 排版」),其余块是一行译文;按词着色,等宽字体(中文部分用系统中文字体回退)。一行译文的块带说明(Python 字段的属性 docstring)时,说明按 Markdown 排在译文下面。
- 预览:函数、类、枚举、异常、导入、常量、变量块是属性表(见下节),注释块是 Markdown 说明(见「注释的 Markdown 排版」),其余块是一行译文;按词着色,等宽字体(中文部分用系统中文字体回退)。一行译文的块带说明(Python 字段的属性 docstring)时,说明按 Markdown 排在译文下面。
- 编辑:这一块的源码(按语法树着色),可以直接改;蓝色边框。按钮换成眼睛「预览」;Esc 也回到预览。
- 复合语句(函数、条件、循环……)块里只放它自己的头部那几行(主体交给子块);收尾括号、空行不成块。函数、类的说明(JSDoc / docstring)也算在函数、类块的头部里;Python 常量、变量、字段的属性 docstring 算在它那一块里(中间隔着的空行也是)。
- 库规则认领的块(数据表卡片等,见「库规则」):种类标签的字和颜色由规则给,内容是规则给的属性表;它的编辑范围是原块整条语句(类就是整个类),「编辑」时整段一起改。规则没并进卡片的子块(方法、别的成员)不管写在类主体哪儿(两个字段中间也算),都照源码顺序是卡片的子块,各自的「编辑」照旧只改自己那几行。
- 同一行不会同时在两处能改(能同时编辑的几段互不重叠):一块在编辑时,编辑范围和它重叠的子孙块不画(卡片编辑时它下面的子块全收起来,一行写完的 `if x: return 1` 编辑时同一行的子块也不画),回到预览就照原样出来;点一块的「编辑」时,编辑范围和它重叠的别的块退出编辑。跳到定义的目标块正被盖住时闪盖住它的那块。
- 折叠:函数、类、枚举块和库规则的卡片打开时默认收起子块(属性表照常显示),其余块默认展开;用户点过折叠箭头的块按他点的,记在视图状态里(见「预览数据与消息」)。
- 折叠:函数、类、枚举、异常块和库规则的卡片打开时默认收起子块(属性表照常显示),其余块默认展开;用户点过折叠箭头的块按他点的,记在视图状态里(见「预览数据与消息」)。
- 顶部筛选栏:见「筛选」;翻译下拉框(多于一种翻译时才显示)也在栏里。
- 解析失败:显示错误原因,并提示用「编辑」查看源码。
@@ -104,7 +105,7 @@
预览顶部钉住一条筛选栏,按块的属性藏掉一部分块(原型画板「筛选 · 默认 / 按访问级别 / 按种类」)。
- 三层分工,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`、`ImportFacts.typeOnly`、`VariableFacts.access`),只有这一条路,语言插件不用另报;facts 里没填的(语言没有这个概念,例如 JavaScript 没有抽象类、仅类型导入)块就没有这个属性,不是填「否」。语言自己的属性(Rust 的 `unsafe = 'true'`)、facts 表达不了的通用属性值(Kotlin 的 `access = 'internal'`)填在块的 `attributes` 里,和推出来的合在一起,同一属性以它为准;`kind` 总是块的种类。
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` → 函数、布尔值 → 是 / 否),和属性表里的属性名、值是同一套叫法(中文直译里属性表的「访问级别」「异步」「抽象」「仅类型」行和筛选共用一份表);种类标签上的字也取它。没起名的显示 id。
3. webview 只按属性 id / 值 id 筛,显示翻译插件给的名字。
- 下拉框:这份文件里有两种以上值的属性各一个(只有一种值的不给——筛它等于按种类筛);只有一种值、但这个值正被去掉的也给,不然藏起来的块在栏上找不到原因。排列:通用属性按契约的顺序(种类、访问级别、异步、抽象、仅类型),语言自己的属性按在文件里第一次出现的先后排在后面。下拉菜单顶上一行「全选 · 取消全选」,分隔线下面是勾选列表,列出这份文件里出现的值(按第一次出现的先后)和各有几块(所有层的块都算,例如「私有 3 / 公共 3」)。
@@ -118,7 +119,7 @@
- 样子:筛选栏钉在预览顶部(`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、左边一条竖线,竖线对着上一层的箭头,每层自己的两列对齐。同一层里没有带下一层的子行时不留箭头的位置(和导入项的子行一样)。
@@ -129,6 +130,7 @@
| 函数 | 名称(函数色;匿名函数写「匿名」)/ 说明(有说明才显示:JSDoc 去掉 `/**`、`*/` 和每行开头的 `*`,docstring 去掉引号;再去掉共同缩进和首尾空行,`@param` 这类标签行照抄;得到的原文按 Markdown 渲染,见「注释的 Markdown 排版」;里面的名字不能点)/ 访问级别 / 异步(是 / 否)/ 装饰器(有才显示,源码原样照抄、按源码着色,多个换行)/ 输入参数(`N 个` 或「无」,展开成表格,列名 名称 \| 类型 \| 默认值,每个参数一行:名称 \| 类型(没标注写「未标注」)\| 默认值(译文,没有留空))/ 输出参数(返回类型的译文,没标注写「未标注」) |
| 类 | 名称(类型色;匿名类 `export default class {}` 写「匿名」;赋给变量、类字段的类表达式写变量名、字段名)/ 说明(有说明才显示,规则同函数)/ 访问级别 / 抽象(是 / 否,只有 TypeScript / TSX 有,看 `abstract class`,TypeScript 的类都有这一行;JavaScript / JSX 没有)/ 装饰器(同函数)/ 类型参数(有才显示:TypeScript `<K, T extends X = Y>` 每个参数的译文,逗号隔开,如 `K, T 限定为 X = Y`;Python 3.12 的 `class G[T]` 同样)/ 父类(有才显示:TypeScript `extends` 后面的表达式连着泛型实参的译文,如 `Base<字符串>`;Python 类名后面括号里的参数列表照写的顺序逐项翻,关键字参数、`*bases` 也在这一行,如 `Base, metaclass=ABCMeta`;逗号隔开)/ 实现的接口(有才显示:TypeScript `implements` 后面每个类型的译文,逗号隔开) |
| 枚举 | 名称(类型色)/ 说明(有说明才显示,规则同函数)/ 访问级别(同类)/ 装饰器(有才显示,同函数)/ 继承(有才显示:Python 类名后面括号里照写的父类,如 `str, enum.Enum`;TypeScript 没有)/ 成员(`N 个` 或「无」,展开成表格,列名 成员 \| 值 \| 说明,每个成员一行:成员名(variable 色)\| 值(写了值是值的译文;没写值时是语言算出来的值,TypeScript 数值枚举自增的 0、1、2……,算不出来和 Python 的 `auto()` 写「自动」,这两种按 hint 着色)\| 说明(成员的注释、JSDoc、属性 docstring 的原文用空格连起来,comment 色;一个成员都没有说明时没有这一列);讲的定义是成员名,跳到成员名只闪那一行)。原型「枚举」一节(`ENUM_SAMPLE`、`enumCard`) |
| 异常 | 名称(类型色;匿名类写「匿名」)/ 说明(有说明才显示,规则同函数)/ 访问级别(同类)/ 装饰器(有才显示,同函数)/ 继承(有才显示:照写的父类,同类的「父类」,如 `Exception`、`errors.HttpException<字符串>`)/ 属性(有类属性才显示:`N 个`,展开成表格,列名 属性 \| 类型 \| 值 \| 说明,每个属性一行:属性名(variable 色)\| 类型(写了标注是它的译文,一个属性都没标注时没有这一列)\| 值(写了值是值的译文,没写留空)\| 说明(同枚举成员的说明,一个属性都没有说明时没有这一列);讲的定义是属性名)/ 构造参数(写了构造函数才显示:表格同函数的「输入参数」,Python 去掉 self;讲的定义是参数名)。见「异常」 |
| 导入 | 来源(TypeScript 是字符串字面量原样,如 `'node:fs'`,字符串色;Python 是点分模块名,如 `fastapi`、`.models`,类型色)/ 仅类型(是 / 否,只有 TypeScript / TSX 有,看 `import type`;JavaScript / JSX 没有)/ 导入项(`N 个` 或「无」,每项一行:`name`、`name 作为 alias`;默认导入写「默认导出 作为 x」,`* as x` 写「全部 作为 x」,Python `from m import *` 写「全部」,Python `import a.b as c` 写「整个模块 作为 c」,一条 `import a, b` 导入多个模块时写模块名) |
| 常量 / 变量 | 名称(解构声明写整个模式,如 `{ p, q }`)/ 说明(有说明才显示:Python 的属性 docstring,规则同函数;一条语句一个,排在第一个名称后面)/ 访问级别 / 类型(写了标注才有)/ 值(常量)或初始值(变量):初始化表达式的译文,没有写「无」;是字典 / 对象字面量时见下面「字典 / 对象字面量」。一条语句声明几个变量(`let a = 1, b = 2`)就重复几组 |
@@ -150,6 +152,49 @@
- 空字面量不展开:值是一行 `空字典` / `空对象`(一行译文里也这样翻,同 `空数组`)。
- `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`、套着的推导式)不管出现在哪(一行译文、属性表的值 / 初始值 / 默认值、字典项的值),都原地换成一个小框:
@@ -298,7 +343,7 @@
```ts
export type BlockKind =
| 'import' | 'function' | 'class' | 'enum' | 'type' | 'field' | 'const' | 'variable'
| 'import' | 'function' | 'class' | 'enum' | 'exception' | 'type' | 'field' | 'const' | 'variable'
| 'if' | 'else' | 'loop' | 'try' | 'catch' | 'finally' | 'return' | 'statement' | 'comment'
/** 源码里一段的下标(源码字符串下标),end 为开区间。 */
@@ -360,6 +405,25 @@ export interface EnumFacts {
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'
@@ -432,11 +496,12 @@ export interface BlockShape {
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 | ImportBlock | VariableBlock
export type FactBlock = FunctionBlock | ClassBlock | EnumBlock | ExceptionBlock | ImportBlock | VariableBlock
/** 带说明(facts.doc)的块。 */
export type DocumentedBlock = FunctionBlock | ClassBlock | EnumBlock | VariableBlock
export type DocumentedBlock = FunctionBlock | ClassBlock | EnumBlock | ExceptionBlock | VariableBlock
/** 属性表块的种类和 facts,不带块的形状:库规则把一块认成现成的一种属性表块时给它。 */
export type BlockFacts = FactBlock extends infer B ? B extends FactBlock ? Pick<B, 'kind' | 'facts'> : never : never
@@ -546,7 +611,7 @@ export interface Translator {
languageIds: string[]
displayLanguage: string // 译文用的语言('zh'):库规则按它挑自己的词表
translate: (block: TextBlock, document: SourceDocument) => PreviewText // 只翻 header 那一段;注释块给 Markdown 原文
describe: (block: FactBlock, document: SourceDocument) => PropertyRow[] // 函数、类、枚举、导入、常量、变量块 → 属性表
describe: (block: FactBlock, document: SourceDocument) => PropertyRow[] // 函数、类、枚举、异常、导入、常量、变量块 → 属性表
expression: (node: Node, document: SourceDocument) => Inline[] // 一个表达式 / 类型节点整个翻成一行译文,译法同属性表的值;库规则借它
doc: (node: Node, document: SourceDocument) => Markdown | undefined // 说明文字(注释、docstring、当说明用的字符串)→ Markdown 原文,同属性表的「说明」
attributeNames?: AttributeNames // 筛选属性和值叫什么,和属性表的叫法一致;筛选栏、种类标签用它
@@ -633,9 +698,9 @@ export interface CodeReaderApi {
- `activate()` 返回 `{ codeReader: CodeReaderApi }`;其他扩展声明 `extensionDependencies` 后用 `extensions.getExtension('clurdra.superpowers-vscode-clurdra')!.exports.codeReader` 注册。
- 内置的 TypeScript、Python 语言插件、两个中文直译翻译插件和 SQLAlchemy、Celery、enum 库规则在 `activate()` 里通过同一个 API 注册(规则按这个顺序),没有旁路。
- 注册表变化时更新 `superpowers.codeReader.languageIds` 上下文,并让已打开的预览刷新。
- 分工:语言插件只从语法树读出和语言无关的事实(facts,引用源码节点,不做正则匹配文字);翻译插件决定属性名、枚举词(公共 / 是 / 默认导出)和值怎么译,也给筛选属性和值起名(`attributeNames`)。筛选属性的通用部分由宿主从 facts 推出(`attributes.ts` 的 `blockAttributes`),语言插件只在 `BlockShape.attributes` 里补自己语言特有的。第三方语言插件给函数、类、枚举、导入、常量、变量块填 facts 就能用上任何翻译插件的属性表;自己语言的字典 / 映射字面量填进 `DeclaratorFacts.entries` 就能展开成子行(中文模板直译按字面量的节点类型查叫法,见 `ZhTable.mapLiterals`)。写在表达式里的遍历(推导式这类)由语言插件的 `iteration` 给结构(节点 → `IterationFacts`),翻译插件排成一个 `SegmentGroup`(中文直译是样式 `iteration`、框头是结果类型的译名);webview 只认框的样式 id,不认语言。
- 分工:语言插件只从语法树读出和语言无关的事实(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` 就没有中文名。
- 第三方语言插件要给的:`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`。按语法节点类型写的叫法(关键字表、按语境换的译法、节点模板、注解位置这类只关乎措辞的判断)属于翻译插件自己,写在它的表里。
@@ -914,6 +979,7 @@ export interface CodeReaderApi {
- 变量声明的唯一声明值是带语句块主体的箭头函数 / 函数表达式 → 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。
@@ -927,6 +993,7 @@ export interface CodeReaderApi {
- `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:中间可以隔空行和赋值的行尾注释,隔着别的注释、语句就不算;只挂一个,后面再来的字符串照下一条规则。
@@ -946,6 +1013,7 @@ export interface CodeReaderApi {
- 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` 里的表达式。
@@ -1016,7 +1084,7 @@ Python 内置类型(仅注解里):str 字符串、int 整数、float 浮
| `export abstract class Repository<K, T extends Entity = Entity> extends Base<string> implements Loader<T>, Disposable {`(classes.ts 3–9 行,连同前面的 JSDoc 和 `@Injectable()`) | 名称 Repository / 说明 `仓库基类。…` / 访问级别 公共 / 抽象 是 / 装饰器 `@Injectable()` / 类型参数 `K, T 限定为 Entity = Entity` / 父类 `Base<字符串>` / 实现的接口 `Loader<T>, Disposable` |
| `export default class {` | 名称 匿名 / 访问级别 公共 / 抽象 否 |
| `class Meta(Base, metaclass=ABCMeta):`(classes.py,带 `@dataclass(frozen=True)`) | 名称 Meta / 访问级别 公共 / 装饰器 `@dataclass(frozen=True)` / 父类 `Base, metaclass=ABCMeta` |
| `class NotFound(AppException):`(classes.py,连同 docstring) | 名称 NotFound / 说明 `找不到资源。` / 访问级别 公共 / 父类 `AppException` |
| `class NotFound(AppResponse):`(classes.py,连同 docstring) | 名称 NotFound / 说明 `找不到资源。` / 访问级别 公共 / 父类 `AppResponse` |
## 预览数据与消息
@@ -1069,7 +1137,7 @@ type BlockView = BlockViewBase & ({ translation: PreviewText, doc?: Markdown } |
- `{ type: 'stopAsk', askId }`:叫停,结束 claude 进程,这次不存。
- `{ type: 'deleteQa', anchor, entryId }`:删掉一次问答。
- `{ type: 'selectAskProfile', id }`:「问 AI」换 profile;扩展端记进 `globalState` 后发 `askProfiles` 给所有预览。webview 不在本地先改,只按扩展端发来的显示。
- 块的编辑状态和用户的折叠选择在 webview 里按块 id 记(`vscode.setState`):`{ editing: string[], folds: Record<id, boolean>, scroll? }`。`folds` 只记用户点过的(id → 折叠着没有),属性行和带下一层的子行也记在里面:属性行 id 是 `<块 id>/p<行序号>`(例如 `6.1.0/p3`),子行 id 是上一层(行或子行)的 id 加 `/<子行序号>`,一层层往后接(例如 `6.1.0/p3/1` 是第 3 行的第 1 个子行,`6.1.0/p3/1/0` 是它下面的第 0 个);块 id 只有数字和点,只有行那一节带 `p`、其余各节是纯数字,所以块、行、各层子行的 id 互不相撞。子行 id 也是跳到定义时在页面上找要闪的那一项用的。没记的按默认:函数、类、枚举块和库规则的卡片折叠,其余块、属性行和子行展开。眼下折叠着哪些由「默认 + folds」算出来,界面、跳到定义的展开都只看这一份。跳到定义展开祖先块、属性行时也记进 `folds`(记成展开)。读回来的状态逐项核对,旧版本存的、形状不对的项当没存过。文档刷新后保留;正在编辑且有焦点的块,刷新时不覆盖输入框内容。
- 块的编辑状态和用户的折叠选择在 webview 里按块 id 记(`vscode.setState`):`{ editing: string[], folds: Record<id, boolean>, scroll? }`。`folds` 只记用户点过的(id → 折叠着没有),属性行和带下一层的子行也记在里面:属性行 id 是 `<块 id>/p<行序号>`(例如 `6.1.0/p3`),子行 id 是上一层(行或子行)的 id 加 `/<子行序号>`,一层层往后接(例如 `6.1.0/p3/1` 是第 3 行的第 1 个子行,`6.1.0/p3/1/0` 是它下面的第 0 个);块 id 只有数字和点,只有行那一节带 `p`、其余各节是纯数字,所以块、行、各层子行的 id 互不相撞。子行 id 也是跳到定义时在页面上找要闪的那一项用的。没记的按默认:函数、类、枚举、异常块和库规则的卡片折叠,其余块、属性行和子行展开。眼下折叠着哪些由「默认 + folds」算出来,界面、跳到定义的展开都只看这一份。跳到定义展开祖先块、属性行时也记进 `folds`(记成展开)。读回来的状态逐项核对,旧版本存的、形状不对的项当没存过。文档刷新后保留;正在编辑且有焦点的块,刷新时不覆盖输入框内容。
## 颜色
@@ -1090,7 +1158,7 @@ type BlockView = BlockViewBase & ({ translation: PreviewText, doc?: Markdown } |
属性表里:说明按 comment,访问级别(公共 / 私有 / 受保护)和「作为」按 keyword,是 / 否按 storage,函数名称按 function,类名称按 type,未标注、无、N 个、字典 N 项、对象 N 项、展开、默认导出、全部、整个模块、匿名按 hint;输入参数表格的列名是界面文字(`descriptionForeground`),不是着色的词。推导式框里的「遍历」「的每个」「仅当」「取」「异步遍历」按 keyword,框头按「循环」块的种类色。父类名(TypeScript `extends` 后面、JavaScript `class_heritage` 里、Python 类名后面括号里直接写的名字)源码着色和属性表里都按 type。
种类(标签字色 + 标签底 18% + 块底 7% + 边框 35%;亮色主题同色系):import 导入 #c586c0;function 函数 #dcdcaa;class 类、type 类型 #4ec9b0;enum 枚举 #b180d7(亮色 #652d90);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。编辑状态:底色 `--vscode-editor-background`,边框 `--vscode-focusBorder`。
种类(标签字色 + 标签底 18% + 块底 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。编辑状态:底色 `--vscode-editor-background`,边框 `--vscode-focusBorder`。
## 代码位置
@@ -1113,8 +1181,9 @@ vscode/src/codeReader/
rules/pythonTree.ts Python 库规则共用的读语法树的办法(名字最后一节、字符串内容、调用的参数、别名、注解里的 X[...]、库里的名字)
rules/segments.ts 库规则共用的拼属性表文字的办法
rules/children.ts 库规则共用的挑子块的办法(成员连同紧挨在它前面的注释块、成员的说明节点)
languages/typescript.ts
languages/python.ts (iteration:推导式的结构)
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)
@@ -1157,7 +1226,7 @@ vscode/webview-ui/code-reader.html + src/codeReader/ 预览界面(React,Vit
- 语言插件:`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 互不相撞;旧版本存的状态不报错;跳到定义时默认折叠的函数、类也会展开。
- 折叠:默认函数、类、枚举、异常块和库规则的卡片折叠、其余展开;用户的选择盖过默认、重新加载后还在(嵌套子行的也是);各层子行的 id 互不相撞;旧版本存的状态不报错;跳到定义时默认折叠的函数、类也会展开。
- 属性表信息:`envLock.ts`、`sample.py`、`routes.py`(原型里的 FastAPI 路由)加内联片段,覆盖各种导入写法、形参写法、类成员访问修饰符、解构和多变量声明、Python 下划线规则;字典 / 对象字面量的各项:Python 字符串键、名字键、嵌套字典、`**` 展开、空字典、类属性,TypeScript 名字键、字符串键、计算属性名、简写、展开、方法和 getter(头部到主体之前)、嵌套对象、空对象,不是字面量的值没有 entries。
- 字典 / 对象的属性表:项数、键 | 值两列、嵌套一层层展开、「展开」、方法头部、空的一行;键和值里的名字、字符串带下标(嵌套的也是);每项标的定义是键,展开项不标;跳到定义落在对象属性名上只闪那一项、连外层各层一起展开,只是显示着的名字闪整块、展开到它那一层。
- 推导式:Python 语言插件读出的结构(调用唯一参数的生成器带 sharedParentheses、几个参数之一的不带,列表带条件,字典分出键和值,集合的解构变量,几节 for / if 照源码顺序,async for,套着的各自认,字面量和 for 语句不算);中文直译「推导式」一节的样例表逐字一致,框的样式、框头、关键字着色,顿号连写、条件位置、另起一句、异步遍历、集合、框里的框、多个参数旁边的生成器、跨行带注释的生成器,属性表的默认值和字典项的值里也换;框里的名字带下标且指回源码原文,高亮各处出现、跳到定义都认框里的名字(只显示在框里的推导式变量 → 框所在的块);TypeScript 不认。
@@ -1185,6 +1254,7 @@ vscode/webview-ui/code-reader.html + src/codeReader/ 预览界面(React,Vit
- 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;没注解的类属性、字段中间的方法照源码顺序是子块,「编辑」写回整个类。
- 异常(`languages/exceptions.test.ts`,真实解析 + 中文直译 + 视图数据):`fixtures/appExceptions.py` 的 `AppException`、`NotFound` 逐字同「异常」的样例(列名随有没有类型、说明变),`GatewayTimeout` 的多段说明,方法是子块;`fixtures/commerceExceptions.py` 16 个类都是异常、只有说明的四行、写了 `__init__` 的构造参数;认哪些类(自带的异常类、`Warning`、不合后缀的自带类 `StopIteration` / `KeyboardInterrupt`、`pkg.errors.AppException`、同一文件里写在后面的父类一层层往下传、泛型和别的父类混写,关键字参数、`Mapping[...]`、`*bases` 不算,函数里的类也认);类属性(带不带注解、只写注解、说明取上面的注释块 / 行尾注释 / 属性 docstring,连着赋值、解构赋值、方法、讲方法的注释、最后面的注释留作子块),构造参数去掉 self、`*args` 开头的不去;`@dataclass` 的异常类照样是异常卡片;编辑范围是整个类、改卡片写回整个类,类名、属性名、参数名的定义范围和带下标的词指回源码原文;TypeScript `fixtures/exceptions.ts.txt`(`extends Error`、类字段含 static、参数属性、JSDoc、写在后面的父类、类表达式、`ns.X<T>`,父类名不合习惯的照常是类)、JavaScript 的类字段;TypeScript 和 Python 同样的异常得到一样的行和列名;筛选的种类叫「异常」、没有 badge;属性的中文名和去掉行尾记号。webview:异常块默认折叠(`viewState.test.ts`),跳到类名闪整块、属性名 / 构造参数名只闪表里那一行(`reveal.test.ts`)。
- 筛选:块的属性(TS、TSX 有抽象、仅类型,JavaScript / JSX 没有这两个属性、属性表也没有这两行,Python 有访问级别、异步);语言插件自己报的属性叠上去、盖过推出来的,kind 不变;文件里出现过的属性按契约顺序排、语言的排后面,值按第一次出现排、带所有层的块数,名字取翻译插件的、没有的显示 id,中文直译的名字和属性表一致;下拉框只给两种以上值的属性(或一种值正被去掉的),按钮上的字;全选 / 取消全选只动这份文件里的值、别的文件的去掉状态和别的属性不动、取消全选藏掉有这个属性的块(整棵子树)、再全选全回来;同一属性内「或」、不同属性「且」、没有这个属性不受影响、藏整棵子树、「已隐藏」只数直接筛掉的;筛选记在 `workspaceState`、新实例读得回来、所有订阅的预览收到、形状不对的当没有;跳到被筛掉的块临时露出路径、目标的子块照常筛、文档刷新只留正在编辑的。
- 文件注释:键相对工作区文件夹、`/` 分隔,根和外面的路径没有键,手写的 `./`、结尾 `/` 规范掉;保存去空白、清空即删;文件改名、目录挪位置里面各层一起走、名字只是前缀相同的不动、跨工作区文件夹先取出再放进去;删目录连带里面的;读写:文件缺失 / 解析失败 / 不是对象返回空表、跳过不合法条目、建 `.spx`、键排序、删光留 `{}`;有注释 / 没注释的行的描述、悬停提示(Markdown 转义)、contextValue,和 `package.json` 里菜单的 when 子句对得上。
- 侧栏行上的菜单(`explorer/rowMenus.test.ts`,读 `package.json`):行尾按钮先「复制」子菜单再注释,有没有注释换「添加 / 编辑注释」,工作区根都没有;「复制」子菜单带 `$(copy)`,注释图标 `$(note)` / `$(edit)`;子菜单和右键菜单里文件四项、目录三项(有没有注释一样)、菜单上的字;复制命令都登记了、和代码里的命令表一致、命令面板里藏起来。
+1
View File
@@ -27,6 +27,7 @@ function factAttributes(block: CodeBlock): Partial<Record<CommonAttribute, strin
case 'class':
return { access: block.facts.access, abstract: flag(block.facts.abstract) }
case 'enum':
case 'exception':
case 'const':
case 'variable':
return { access: block.facts.access }
+1 -1
View File
@@ -117,7 +117,7 @@ describe('blockView', () => {
const notFound = byId(await views(py, 'python', pythonZhTranslator, readFixture('classes.py')), '3')
expect(notFound).toMatchObject({ kind: 'class', startLine: 17, endLine: 19 })
expect(notFound.code.split('\n')).toEqual([
'class NotFound(AppException): # 行尾',
'class NotFound(AppResponse): # 行尾',
' # 说明前面的注释',
' """找不到资源。"""',
])
+2 -1
View File
@@ -32,7 +32,7 @@ interface BlockViewBase {
}
/**
* 发给 webview 的一块(可序列化):函数、类、枚举、导入、常量、变量块和库规则的卡片预览时是属性表,注释块是 Markdown 说明,其余是一行译文。
* 发给 webview 的一块(可序列化):函数、类、枚举、异常、导入、常量、变量块和库规则的卡片预览时是属性表,注释块是 Markdown 说明,其余是一行译文。
* 一行译文的块带说明(TextBlock.doc)时 doc 是翻译插件给的 Markdown 原文,预览排在译文下面。
*/
export type BlockView = BlockViewBase & ({ translation: PreviewText, doc?: Markdown } | { properties: PropertyRow[] })
@@ -107,6 +107,7 @@ function ownName(block: CodeBlock): Node | undefined {
case 'function':
case 'class':
case 'enum':
case 'exception':
return block.facts.name
case 'const':
case 'variable':
+35 -4
View File
@@ -4,7 +4,7 @@ import type { Disposable } from 'vscode'
export type { Node }
export type BlockKind
= | 'import' | 'function' | 'class' | 'enum' | 'type' | 'field' | 'const' | 'variable'
= | 'import' | 'function' | 'class' | 'enum' | 'exception' | 'type' | 'field' | 'const' | 'variable'
| 'if' | 'else' | 'loop' | 'try' | 'catch' | 'finally' | 'return' | 'statement' | 'comment'
/** 源码里一段的下标:源码字符串下标(web-tree-sitter 的 startIndex / endIndex),end 为开区间。 */
@@ -101,6 +101,35 @@ export interface EnumFacts {
members: EnumMemberFacts[]
}
/** 异常类的一个类属性:Python 类主体里给一个名字赋值的语句(带不带注解、有没有值),TypeScript 的类字段(含 static);节点都指向源码。 */
export interface FieldFacts {
name: Node
type?: Node
value?: Node
/** 属性的说明文字节点,同 EnumMemberFacts.docs:紧挨在上面单独成行的注释 / JSDoc、这一行行尾的注释、Python 紧跟在后面的属性 docstring。 */
docs: Node[]
}
/**
* 异常类:父类是语言自带的异常类、同一文件里认出来的异常类,或名字按命名习惯以 Error / Exception(Python 还有 Warning)结尾。
* Python、TypeScript 都由语言插件报,同一种块、同一张属性表。块的头部是整个类(类属性在属性表里讲),留下的方法等是子块。
*/
export interface ExceptionFacts {
/** 同 ClassFacts.name。 */
name?: Node
/** 同 ClassFacts.doc。 */
doc?: Node
/** 同 ClassFacts.access。 */
access?: Access
decorators: Node[]
/** 照源码写的父类,同 ClassFacts.bases。 */
bases: BaseFacts[]
/** 类属性,照源码顺序。 */
fields: FieldFacts[]
/** 构造函数的参数(Python 的 `__init__` 去掉 self,TypeScript 的 `constructor(...)`);类里没写构造函数时没有。 */
parameters?: ParameterFacts[]
}
/** 导入的一项。 */
export type ImportItem
= | { kind: 'named', name: Node, alias?: Node } // import { a as b }、from m import a as b
@@ -176,7 +205,7 @@ export interface VariableFacts {
* - kind:块的种类(BlockKind);
* - access:访问级别,'public' | 'private' | 'protected',语言可以加自己的值(Kotlin 的 'internal',见 BlockShape.attributes);
* - async、abstract、typeOnly:异步、抽象、仅类型,值是 'true' | 'false'。
* 宿主从块的 kind 和 facts 推出它们(FunctionFacts.access / async、ClassFacts.access / abstract、EnumFacts.access、ImportFacts.typeOnly、VariableFacts.access),
* 宿主从块的 kind 和 facts 推出它们(FunctionFacts.access / async、ClassFacts.access / abstract、EnumFacts.access、ExceptionFacts.access、ImportFacts.typeOnly、VariableFacts.access),
* 语言插件不用另报;facts 里没填的(语言没有这个概念、块没有这一项),块就没有这个属性,也不受这个属性的筛选影响。
*/
export const COMMON_ATTRIBUTES = ['kind', 'access', 'async', 'abstract', 'typeOnly'] as const
@@ -207,12 +236,14 @@ export interface FunctionBlock extends BlockShape { kind: 'function', facts: Fun
export interface ClassBlock extends BlockShape { kind: 'class', facts: ClassFacts }
/** 枚举块的头部是整个枚举(成员都在属性表里讲),没有子块;库规则认出来的枚举(Python)是整个类,留下的方法等是子块。 */
export interface EnumBlock extends BlockShape { kind: 'enum', facts: EnumFacts }
/** 异常块的头部是整个类(类属性都在属性表里讲),留下的方法(含构造函数)等是子块。 */
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 | ImportBlock | VariableBlock
export type FactBlock = FunctionBlock | ClassBlock | EnumBlock | ExceptionBlock | ImportBlock | VariableBlock
/** 带说明(facts.doc)的块。 */
export type DocumentedBlock = FunctionBlock | ClassBlock | EnumBlock | VariableBlock
export type DocumentedBlock = FunctionBlock | ClassBlock | EnumBlock | ExceptionBlock | VariableBlock
/** 属性表块的种类和 facts,不带块的形状(节点、头部、子块):库规则把一块认成现成的一种属性表块时给它(RuleFacts)。 */
export type BlockFacts = FactBlock extends infer B ? B extends FactBlock ? Pick<B, 'kind' | 'facts'> : never : never
@@ -0,0 +1,145 @@
"""自定义异常体系.
所有业务异常继承自 AppException,携带 HTTP 状态码和结构化错误信息。
在 main.py 中注册全局异常处理器统一返回格式。
"""
from __future__ import annotations
from typing import Any
class AppException(Exception):
"""应用异常基类.
Attributes:
status_code: HTTP 状态码。
error_code: 业务错误码(机器可读)。
message: 错误描述(人类可读)。
detail: 额外错误详情。
"""
status_code: int = 500
error_code: str = "INTERNAL_ERROR"
message: str = "服务器内部错误"
def __init__(
self,
message: str | None = None,
*,
detail: Any = None,
error_code: str | None = None,
) -> None:
if message is not None:
self.message = message
if error_code is not None:
self.error_code = error_code
self.detail = detail
super().__init__(self.message)
def to_dict(self) -> dict[str, Any]:
"""序列化为 JSON 响应体."""
body: dict[str, Any] = {
"error_code": self.error_code,
"message": self.message,
}
if self.detail is not None:
body["detail"] = self.detail
return body
class NotFound(AppException):
"""资源不存在 (404)."""
status_code = 404
error_code = "NOT_FOUND"
message = "资源不存在"
class Unauthorized(AppException):
"""未认证 (401)."""
status_code = 401
error_code = "UNAUTHORIZED"
message = "未认证或凭据无效"
class Forbidden(AppException):
"""无权限 (403)."""
status_code = 403
error_code = "FORBIDDEN"
message = "无操作权限"
class ValidationError(AppException):
"""数据验证失败 (422)."""
status_code = 422
error_code = "VALIDATION_ERROR"
message = "数据验证失败"
class ExternalServiceError(AppException):
"""外部服务调用失败 (502)."""
status_code = 502
error_code = "EXTERNAL_SERVICE_ERROR"
message = "外部服务调用失败"
class BusinessError(AppException):
"""业务错误 (400)."""
status_code = 400
error_code = "BUSINESS_ERROR"
message = "业务处理失败"
class Conflict(AppException):
"""资源冲突 (409)."""
status_code = 409
error_code = "CONFLICT"
message = "资源已存在"
class GatewayTimeout(AppException):
"""上游服务响应超时 (504).
用于表达 "请求已发往上游但在时限内未拿到响应":区别于 502
(上游明确报错) 和 503 (上游不可达/未就绪)。
"""
status_code = 504
error_code = "UPSTREAM_TIMEOUT"
message = "上游服务响应超时"
class ServiceUnavailable(AppException):
"""服务暂不可用 (503).
用于表达 "功能依赖的配置/外部资源未就绪" 这类状态:
资源本身合法存在但当前不可达 / 未配置;区别于 404 (资源不存在)
和 502 (上游报错)。
"""
status_code = 503
error_code = "SERVICE_UNAVAILABLE"
message = "服务暂不可用"
class PayloadTooLarge(AppException):
"""请求体超限 (413)."""
status_code = 413
error_code = "PAYLOAD_TOO_LARGE"
message = "压缩包超过 20MB 上限"
class UploadReadTimeout(AppException):
"""上传收流超时 (408)."""
status_code = 408
error_code = "UPLOAD_READ_TIMEOUT"
message = "上传超时, 请检查网络后重试"
+4 -4
View File
@@ -1,10 +1,10 @@
"""领域异常。"""
"""接口响应。"""
from abc import ABCMeta
class AppException(Exception):
"""应用异常的基类。
class AppResponse(BaseModel):
"""应用响应的基类。
带上 HTTP 状态码。
"""
@@ -15,7 +15,7 @@ class AppException(Exception):
self.message = message
class NotFound(AppException): # 行尾
class NotFound(AppResponse): # 行尾
# 说明前面的注释
"""找不到资源。"""
status_code = 404
@@ -0,0 +1,75 @@
"""commerce 域异常体系."""
from __future__ import annotations
class CommerceError(Exception):
"""commerce 域基础异常."""
class ProductNotFoundError(CommerceError):
"""商品不存在或已下架."""
class OrderNotFoundError(CommerceError):
"""订单不存在."""
class InvalidOrderStateError(CommerceError):
"""订单状态非法转换."""
class PaymentVerifyError(CommerceError):
"""支付通道验签失败."""
class AmountMismatchError(CommerceError):
"""支付金额与订单金额不匹配."""
class FulfillmentError(CommerceError):
"""fulfillment 调 billing 失败."""
class RefundError(CommerceError):
"""退款失败."""
class UnsupportedPaymentMethodError(CommerceError):
"""未配置当前 payment_method 对应的 driver. 通常意味着环境变量缺失."""
def __init__(self, method: str) -> None:
super().__init__(f"unsupported payment method: {method}")
self.method = method
class OutOfStockError(CommerceError):
"""商品库存不足, 无法下单。"""
class PurchaseLimitExceededError(CommerceError):
"""超出商品 purchase_limit_per_user 限购次数。"""
class PendingOrderExistsError(CommerceError):
"""同一用户已有相同商品的待支付订单。"""
class ProductPlanMissingError(CommerceError):
"""商品未绑定任何 billing.Plan, 无法下单/履约。"""
class InstanceTypeNotFoundError(CommerceError):
"""instance_type_id 指向的档位不存在; admin 应回 400 而不是撞 FK 500。"""
def __init__(self, instance_type_id: int) -> None:
super().__init__(f"instance_type {instance_type_id} not found")
self.instance_type_id = instance_type_id
class ProductPlanMismatchError(CommerceError):
"""商品展示档位与 Billing Plan 实际发放档位不一致."""
class WechatPayUnknownError(CommerceError):
"""微信预下单结果未知: 网络中断/超时, 不能判定远端是否已受理."""
@@ -0,0 +1,30 @@
/** 调接口出错。 */
export class ApiError extends Error {
status = 500
// 机器可读的错误码
static readonly kind: string = 'API_ERROR'
constructor(message: string, public code?: string) {
super(message)
}
toJSON() {
return { status: this.status, code: this.code }
}
}
export class NotFoundError extends ApiError {
status = 404 // 找不到
}
class Slow extends Timeout {}
class Timeout extends NotFoundError {}
const Cancelled = class extends RangeError {}
export class Forbidden extends errors.HttpException<string> {}
export class Repository extends Base {
status = 1
}
@@ -0,0 +1,339 @@
import type { BlockView } from '../blockView'
import type { Inline, LanguageAdapter, PropertyRow, Segment, Translator } from '../contract'
import { describe, expect, it } from 'vitest'
import { fileAttributes } from '../attributes'
import { readBlockViews, toBlockViews } from '../blockView'
import { replaceLines } from '../editText'
import { celeryRule } from '../rules/celery'
import { dataclassRule } from '../rules/dataclass'
import { enumRule } from '../rules/enum'
import { sqlalchemyRule } from '../rules/sqlalchemy'
import { flatSegments, outline, propertyLines, readFixture, runtime, WASM_DIR, withBlocks } from '../testSupport'
import { pythonZhTranslator } from '../translators/pythonZh'
import { typescriptZhTranslator } from '../translators/typescriptZh'
import { createPythonAdapter } from './python'
import { createTypeScriptAdapter } from './typescript'
const py = createPythonAdapter(WASM_DIR)
const ts = createTypeScriptAdapter(WASM_DIR)
const APP = readFixture('appExceptions.py')
const COMMERCE = readFixture('commerceExceptions.py')
const TS = readFixture('exceptions.ts.txt')
/** 和扩展里注册的顺序一样。 */
const RULES = [sqlalchemyRule, celeryRule, enumRule, dataclassRule]
function views(adapter: LanguageAdapter, languageId: string, translator: Translator, source: string): Promise<BlockView[]> {
return withBlocks(adapter, languageId, source, blocks =>
toBlockViews(blocks, { source, language: adapter, markers: [] }, translator, { rules: RULES, report: () => {} }))
}
const pyViews = (source: string): Promise<BlockView[]> => views(py, 'python', pythonZhTranslator, source)
const tsViews = (source: string): Promise<BlockView[]> => views(ts, 'typescript', typescriptZhTranslator, source)
function flatten(list: readonly BlockView[]): BlockView[] {
return list.flatMap(view => [view, ...flatten(view.children)])
}
function properties(view: BlockView | undefined): PropertyRow[] {
if (!view || !('properties' in view))
throw new Error(`块 ${view?.id} 不是属性表`)
return view.properties
}
function named(list: readonly BlockView[], name: string): BlockView {
const found = flatten(list).find(view => 'properties' in view && propertyLines(view.properties)[0] === `名称: ${name}`)
if (!found)
throw new Error(`没有 ${name}`)
return found
}
function lines(list: readonly BlockView[], name: string): string[] {
return propertyLines(properties(named(list, name)))
}
/** 属性表里所有带下标的词(连表格里的格子)。 */
function linkedSegments(rows: PropertyRow[]): Segment[] {
const pieces: Inline[] = rows.flatMap(row => [
...('markdown' in row.value ? [] : row.value),
...(row.items ?? []).flatMap(item => item.cells.flat()),
])
return flatSegments(pieces).filter(segment => segment.offset !== undefined)
}
const PY_CASES = [
'from dataclasses import dataclass',
'import typing',
'',
'',
'class Late(Base):',
' """写在父类前面,照样认得出。"""',
'',
'',
'class Base(Exception):',
' # 错误码',
' # 机器可读',
' code = "E"',
' retry: bool = False # 能不能重试',
' detail: str',
' """额外的说明。"""',
'',
' # 讲方法的注释',
' def describe(self) -> str:',
' return self.code',
'',
' level = 1',
' a = b = 2',
' x, y = 1, 2',
'',
' def __init__(self, message: str, *, level: int = 1, **extra) -> None:',
' super().__init__(message)',
'',
' # 收尾',
'',
'',
'class Deeper(Late):',
' pass',
'',
'',
'class Stop(StopIteration): ...',
'class Warned(UserWarning): ...',
'class Remote(pkg.errors.AppException): ...',
'class Typed(typing.Generic[T], KeyboardInterrupt): ...',
'class Plain(Model, metaclass=ErrorMeta): ...',
'class Other(Mapping[str, int]): ...',
'class Starred(*bases): ...',
'',
'',
'class Loose(Exception):',
' def __init__(*args):',
' pass',
'',
'',
'@dataclass',
'class Payload(ValueError):',
' field: int = 0',
'',
'',
'def factory():',
' class Local(RuntimeError):',
' pass',
' return Local',
'',
].join('\n')
describe('异常卡片(Python)', () => {
it('luffy 的 core/exceptions:基类的类属性、构造参数,子类只改类属性;方法留作子块', async () => {
const list = await pyViews(APP)
expect(list.map(view => view.kind)).toEqual(['comment', 'import', 'import', ...Array.from({ length: 12 }, () => 'exception')])
expect(lines(list, 'AppException')).toEqual([
'名称: AppException',
'说明: 应用异常基类.\n\nAttributes:\n status_code: HTTP 状态码。\n error_code: 业务错误码(机器可读)。\n message: 错误描述(人类可读)。\n detail: 额外错误详情。',
'访问级别: 公共',
'继承: Exception',
'属性: 3 个',
' status_code | 整数 | 500',
' error_code | 字符串 | "INTERNAL_ERROR"',
' message | 字符串 | "服务器内部错误"',
'构造参数: 3 个',
' message | 字符串 | 空 | 空',
' detail | Any | 空',
' error_code | 字符串 | 空 | 空',
])
const app = properties(named(list, 'AppException'))
expect(app.map(row => row.columns)).toEqual([undefined, undefined, undefined, undefined, ['属性', '类型', '值'], ['名称', '类型', '默认值']])
expect(named(list, 'AppException').children.map(child => [child.kind, child.startLine])).toEqual([['function', 25], ['function', 39]])
expect(lines(list, 'NotFound')).toEqual([
'名称: NotFound',
'说明: 资源不存在 (404).',
'访问级别: 公共',
'继承: AppException',
'属性: 3 个',
' status_code | 404',
' error_code | "NOT_FOUND"',
' message | "资源不存在"',
])
expect(properties(named(list, 'NotFound')).at(-1)?.columns).toEqual(['属性', '值'])
expect(named(list, 'NotFound').children).toEqual([])
expect(lines(list, 'GatewayTimeout').slice(0, 2)).toEqual([
'名称: GatewayTimeout',
'说明: 上游服务响应超时 (504).\n\n用于表达 "请求已发往上游但在时限内未拿到响应":区别于 502\n(上游明确报错) 和 503 (上游不可达/未就绪)。',
])
})
it('luffy 的 commerce/exceptions:一串子类都是异常;只有说明的一张卡片四行,写了 __init__ 的多一行构造参数', async () => {
const list = await pyViews(COMMERCE)
expect(list.map(view => view.kind)).toEqual(['comment', 'import', ...Array.from({ length: 16 }, () => 'exception')])
expect(lines(list, 'CommerceError')).toEqual(['名称: CommerceError', '说明: commerce 域基础异常.', '访问级别: 公共', '继承: Exception'])
expect(lines(list, 'ProductNotFoundError')).toEqual(['名称: ProductNotFoundError', '说明: 商品不存在或已下架.', '访问级别: 公共', '继承: CommerceError'])
expect(lines(list, 'UnsupportedPaymentMethodError')).toEqual([
'名称: UnsupportedPaymentMethodError',
'说明: 未配置当前 payment_method 对应的 driver. 通常意味着环境变量缺失.',
'访问级别: 公共',
'继承: CommerceError',
'构造参数: 1 个',
' method | 字符串 | ',
])
expect(named(list, 'UnsupportedPaymentMethodError').children.map(child => child.kind)).toEqual(['function'])
})
it('认哪些类:父类是自带的异常类(含 Warning)、同一文件里的异常类(写在前面后面都行,一层层往下传)、名字以 Error / Exception / Warning 结尾;关键字参数、泛型、展开不算', async () => {
const list = await pyViews(PY_CASES)
const kinds = Object.fromEntries(flatten(list).filter(view => view.kind === 'exception' || view.kind === 'class')
.map(view => [propertyLines(properties(view))[0].replace('名称: ', ''), view.kind]))
expect(kinds).toEqual({
Late: 'exception',
Base: 'exception',
Deeper: 'exception',
Stop: 'exception',
Warned: 'exception',
Remote: 'exception',
Typed: 'exception',
Plain: 'class',
Other: 'class',
Starred: 'class',
Loose: 'exception',
Payload: 'exception',
Local: 'exception',
})
})
it('类属性:给一个名字赋值(带不带注解、有没有值),说明取上面的注释块、行尾注释、属性 docstring;连着赋值、解构赋值、方法照常是子块;构造参数去掉 self', async () => {
const list = await pyViews(PY_CASES)
expect(lines(list, 'Base')).toEqual([
'名称: Base',
'访问级别: 公共',
'继承: Exception',
'属性: 4 个',
' code | | "E" | 错误码 机器可读',
' retry | 布尔 | 假 | 能不能重试',
' detail | 字符串 | | 额外的说明。',
' level | | 1 | ',
'构造参数: 3 个',
' message | 字符串 | ',
' level | 整数 | 1',
' **extra | 未标注 | ',
])
const source = PY_CASES.split('\n')
const base = named(list, 'Base')
expect(base.children.map(child => source[child.startLine].trim())).toEqual([
'# 讲方法的注释',
'def describe(self) -> str:',
'a = b = 2',
'x, y = 1, 2',
'def __init__(self, message: str, *, level: int = 1, **extra) -> None:',
'# 收尾',
])
expect(lines(list, 'Loose').slice(-2)).toEqual(['构造参数: 1 个', ' *args | 未标注 | '])
expect(lines(list, 'Payload')).toEqual(['名称: Payload', '访问级别: 公共', '装饰器: @dataclass', '继承: ValueError', '属性: 1 个', ' field | 整数 | 0'])
expect(named(list, 'Local').attributes).toEqual({ kind: 'exception' })
})
it('编辑范围是整个类(并进来的属性、留下的方法都在里面),改卡片写回整个类;块自己是类名的定义,属性行、参数行各是它那个名字的定义', async () => {
const source = PY_CASES.split('\n')
const base = named(await pyViews(PY_CASES), 'Base')
expect([source[base.startLine], source[base.endLine]]).toEqual(['class Base(Exception):', ' # 收尾'])
expect(replaceLines(PY_CASES, base.startLine, base.endLine, base.code.replace('code = "E"', 'code = "F"')))
.toBe(PY_CASES.replace('code = "E"', 'code = "F"'))
expect(PY_CASES.slice(base.defines?.startIndex, base.defines?.endIndex)).toBe('Base')
const rows = properties(base)
const defined = (label: string): string[] => (rows.find(row => row.label === label)?.items ?? []).map(item => PY_CASES.slice(item.defines?.startIndex, item.defines?.endIndex))
expect(defined('属性')).toEqual(['code', 'retry', 'detail', 'level'])
expect(defined('构造参数')).toEqual(['message', 'level', '**extra'])
const [fieldName] = rows.find(row => row.label === '属性')?.items?.[0].cells[0] as Segment[]
expect(fieldName).toEqual({ text: 'code', role: 'variable', offset: PY_CASES.indexOf('code = "E"') })
for (const segment of linkedSegments(rows))
expect(PY_CASES.slice(segment.offset, (segment.offset ?? 0) + segment.text.length), segment.text).toBe(segment.text)
})
})
describe('异常卡片(TypeScript)', () => {
it('extends Error 的类:JSDoc 当说明,类字段(含 static)进属性表,constructor 的参数(含参数属性)进构造参数,方法留作子块', async () => {
const list = await tsViews(TS)
expect(lines(list, 'ApiError')).toEqual([
'名称: ApiError',
'说明: 调接口出错。',
'访问级别: 公共',
'继承: Error',
'属性: 2 个',
' status | | 500 | ',
' kind | 字符串 | \'API_ERROR\' | 机器可读的错误码',
'构造参数: 2 个',
' message | 字符串 | ',
' code? | 字符串 | ',
])
const api = named(list, 'ApiError')
expect(api.children.map(child => [child.kind, child.startLine])).toEqual([['function', 6], ['function', 10]])
expect([api.startLine, api.endLine]).toEqual([0, 13])
expect(lines(list, 'NotFoundError')).toEqual(['名称: NotFoundError', '访问级别: 公共', '继承: ApiError', '属性: 1 个', ' status | 404 | 找不到'])
})
it('认哪些类:同一文件里的异常类(写在后面也算)、赋给变量的类表达式、`ns.X<T>`;父类名不合习惯、不在这份文件里认出来的照常是类', async () => {
const list = await tsViews(TS)
expect(lines(list, 'Slow')).toEqual(['名称: Slow', '访问级别: 私有', '继承: Timeout'])
expect(named(list, 'Timeout').kind).toBe('exception')
expect(lines(list, 'Cancelled')).toEqual(['名称: Cancelled', '访问级别: 私有', '继承: RangeError'])
expect(lines(list, 'Forbidden')).toEqual(['名称: Forbidden', '访问级别: 公共', '继承: errors.HttpException<字符串>'])
const repository = named(list, 'Repository')
expect(repository.kind).toBe('class')
expect(repository.children.map(child => child.kind)).toEqual(['field'])
})
it('javaScript 的类字段同样认', async () => {
const source = 'class HttpError extends Error {\n status = 500\n static code\n}\n'
const list = await views(ts, 'javascript', typescriptZhTranslator, source)
expect(lines(list, 'HttpError')).toEqual(['名称: HttpError', '访问级别: 私有', '继承: Error', '属性: 2 个', ' status | 500', ' code | '])
})
it('typeScript 和 Python 同样的异常得到一样的种类、属性表行、列名', async () => {
const pyList = await pyViews('class Failed(Exception):\n status = 500 # 状态码\n\n def __init__(self, message: str) -> None:\n pass\n')
const tsList = await tsViews('export class Failed extends Error {\n status = 500 // 状态码\n\n constructor(message: string) {\n super(message)\n }\n}\n')
const shape = (list: BlockView[]): unknown => properties(named(list, 'Failed')).map(row => [row.label, row.columns, row.items?.map(item => item.cells.length)])
expect(named(pyList, 'Failed').kind).toBe('exception')
expect(named(tsList, 'Failed').kind).toBe('exception')
expect(shape(pyList)).toEqual(shape(tsList))
expect(lines(pyList, 'Failed').slice(-4)).toEqual(lines(tsList, 'Failed').slice(-4))
})
})
describe('异常卡片(宿主、筛选、中文名)', () => {
it('筛选:种类是 exception、叫「异常」,访问级别同原来的类;没有种类标签(badge),库规则不认', async () => {
const list = await pyViews(PY_CASES)
expect(named(list, 'Base').attributes).toEqual({ kind: 'exception', access: 'public' })
expect(named(list, 'Base').badge).toBeUndefined()
expect(fileAttributes(list, pythonZhTranslator.attributeNames).find(attribute => attribute.id === 'kind')?.values)
.toContainEqual({ id: 'exception', name: '异常', count: 10 })
})
it('各块的源码着色拼起来等于编辑范围的整行源码', async () => {
for (const [list, source] of [[await pyViews(APP), APP], [await pyViews(COMMERCE), COMMERCE], [await tsViews(TS), TS]] as const) {
const rows = source.split('\n')
for (const view of flatten(list)) {
expect(view.codeTokens.map(token => token.text).join(''), view.id).toBe(view.code)
const indent = Math.min(...rows.slice(view.startLine, view.endLine + 1).filter(row => row.trim()).map(row => row.length - row.trimStart().length))
expect(view.code, view.id).toBe(rows.slice(view.startLine, view.endLine + 1).map(row => row.slice(indent)).join('\n'))
}
}
})
it('代码块树:异常块的头部是整个类,留下的方法是子块', async () => {
expect(await withBlocks(py, 'python', 'class E(Exception):\n code = 1\n\n def run(self):\n pass\n', outline)).toEqual([
{ at: [1, 5], kind: 'exception', kids: [{ at: 4, kind: 'function', kids: [{ at: 5, kind: 'statement' }] }] },
])
})
it('属性的中文名:属性格子按下标换字,行尾记号不进说明', async () => {
const source = [
'from typing import Annotated',
'class Failed(Exception):',
' code: Annotated[str, Comment("错误码")] = "E"',
' status = 500 # 状态 @Comment 状态码',
].join('\n')
const { blocks, displayNames } = await readBlockViews(runtime, py, pythonZhTranslator, 'python', source)
expect(propertyLines(properties(named(blocks, 'Failed'))).slice(-3)).toEqual([
'属性: 2 个',
' code | Annotated[字符串, Comment("错误码")] | "E" | ',
' status | | 500 | 状态',
])
expect(displayNames.map(({ name, text }) => `${source.slice(name.startIndex, name.endIndex)} → ${text}`)).toEqual(['code → 错误码', 'status → 状态码'])
})
})
@@ -367,15 +367,15 @@ describe('python 类', () => {
it('classes.py:名称、说明、访问级别(同函数的下划线规则,类里的类也按它,函数里的没有)、装饰器、父类(参数列表照写的顺序,关键字参数也算)', async () => {
const found = await facts(readFixture('classes.py'))
expect(found[6]).toEqual({
name: 'AppException',
doc: '"""应用异常的基类。\n\n 带上 HTTP 状态码。\n """',
name: 'AppResponse',
doc: '"""应用响应的基类。\n\n 带上 HTTP 状态码。\n """',
access: 'public',
decorators: [],
typeParameters: [],
bases: [{ value: 'Exception' }],
bases: [{ value: 'BaseModel' }],
implements: [],
})
expect(found[18]).toMatchObject({ name: 'NotFound', doc: '"""找不到资源。"""', bases: [{ value: 'AppException' }] })
expect(found[18]).toMatchObject({ name: 'NotFound', doc: '"""找不到资源。"""', bases: [{ value: 'AppResponse' }] })
expect(found[24]).toEqual({
name: 'Meta',
access: 'public',
+119 -3
View File
@@ -1,8 +1,8 @@
import type { Access, BlockShape, ClassFacts, CodeBlock, EntryFacts, FunctionFacts, ImportFacts, ImportItem, IterationClause, IterationFacts, IterationResult, LanguageAdapter, Node, ParameterFacts, TextKind, TokenRole } from '../contract'
import type { CommentRules } from './shared'
import type { CommentRules, ExceptionRules } from './shared'
import * as path from 'node:path'
import { pythonNameReader } from './pythonNames'
import { children, compoundShape, leafBlock, leafShape, listBlocks, named, sameNode, syntaxChildren } from './shared'
import { children, compoundShape, leafBlock, leafShape, listBlocks, named, sameNode, syntaxChildren, withExceptions } from './shared'
/** 常量、类字段只认直接写在模块 / 类主体里的赋值;这两处的声明也才有访问级别。 */
type Scope = 'module' | 'class' | 'other'
@@ -398,6 +398,122 @@ function tryBlocks(node: Node): CodeBlock[] {
return blocks
}
// ─── 异常类 ───────────────────────────────────────────────────────────────────
/** Python 自带的异常类(builtins 里的,含各种 Warning)。 */
const BUILTIN_EXCEPTIONS = new Set([
'BaseException',
'BaseExceptionGroup',
'GeneratorExit',
'KeyboardInterrupt',
'SystemExit',
'Exception',
'ArithmeticError',
'FloatingPointError',
'OverflowError',
'ZeroDivisionError',
'AssertionError',
'AttributeError',
'BufferError',
'EOFError',
'ExceptionGroup',
'ImportError',
'ModuleNotFoundError',
'LookupError',
'IndexError',
'KeyError',
'MemoryError',
'NameError',
'UnboundLocalError',
'OSError',
'EnvironmentError',
'IOError',
'WindowsError',
'BlockingIOError',
'ChildProcessError',
'ConnectionError',
'BrokenPipeError',
'ConnectionAbortedError',
'ConnectionRefusedError',
'ConnectionResetError',
'FileExistsError',
'FileNotFoundError',
'InterruptedError',
'IsADirectoryError',
'NotADirectoryError',
'PermissionError',
'ProcessLookupError',
'TimeoutError',
'ReferenceError',
'RuntimeError',
'NotImplementedError',
'PythonFinalizationError',
'RecursionError',
'StopAsyncIteration',
'StopIteration',
'SyntaxError',
'IndentationError',
'TabError',
'SystemError',
'TypeError',
'ValueError',
'UnicodeError',
'UnicodeDecodeError',
'UnicodeEncodeError',
'UnicodeTranslateError',
'Warning',
'BytesWarning',
'DeprecationWarning',
'EncodingWarning',
'FutureWarning',
'ImportWarning',
'PendingDeprecationWarning',
'ResourceWarning',
'RuntimeWarning',
'SyntaxWarning',
'UnicodeWarning',
'UserWarning',
])
/** PEP 8 的命名习惯:异常类名以 Error 结尾(别的文件里的 `AppException`、`*Warning` 也常见)。 */
const EXCEPTION_NAME = /(?:Error|Exception|Warning)$/
/** 父类写的类名:`X`、`pkg.X` 取 X,泛型 `Base[T]` 取 Base 的;关键字参数、`*bases`、调用没有。 */
function className(node: Node | null): string | undefined {
switch (node?.type) {
case 'identifier':
return node.text
case 'attribute':
return node.childForFieldName('attribute')?.text
case 'subscript':
return className(node.childForFieldName('value'))
default:
return undefined
}
}
/**
* 类属性:类主体里给一个名字赋值的语句(`x = 1`、`x: int = 1`、只写注解的 `x: int`);连着赋值(`a = b = 1`)、解构赋值不算。
* 构造函数是 `__init__`,参数去掉第一个(self;`*args` 这类不是 self 的不去)。
*/
const PYTHON_EXCEPTIONS: ExceptionRules = {
baseName: base => className(base.value),
isExceptionName: name => BUILTIN_EXCEPTIONS.has(name) || EXCEPTION_NAME.test(name),
field: (block) => {
const assignment = block.kind === 'field' || block.kind === 'variable' ? assignmentIn(block.node) : undefined
const name = assignment?.childForFieldName('left')
const value = assignment?.childForFieldName('right')
if (!assignment || name?.type !== 'identifier' || value?.type === 'assignment')
return undefined
return { name, type: assignment.childForFieldName('type') ?? undefined, value: value ?? undefined }
},
constructorParameters: (block) => {
if (block.kind !== 'function' || block.facts.name?.text !== '__init__')
return undefined
const [receiver, ...rest] = block.facts.parameters
return receiver?.name.type === 'identifier' ? rest : block.facts.parameters
},
}
// ─── 源码着色 ─────────────────────────────────────────────────────────────────
const STORAGE_KEYWORDS = new Set(['def', 'class', 'async', 'lambda', 'global', 'nonlocal', 'del'])
@@ -484,7 +600,7 @@ export function createPythonAdapter(wasmDir: string): LanguageAdapter {
id: 'python',
languageIds: ['python'],
grammarPath: () => path.join(wasmDir, 'tree-sitter-python.wasm'),
toBlocks: root => statements(named(root), 'module', -1),
toBlocks: root => withExceptions(statements(named(root), 'module', -1), PYTHON_EXCEPTIONS),
tokenRole: pythonTokenRole,
iteration: pythonIteration,
displayNames: pythonNameReader,
+126 -1
View File
@@ -1,4 +1,4 @@
import type { BlockShape, CodeBlock, DocumentedBlock, Node, SourceSpan, TextKind } from '../contract'
import type { BaseFacts, BlockShape, ClassBlock, ClassFacts, CodeBlock, DocumentedBlock, ExceptionBlock, FieldFacts, Node, ParameterFacts, SourceSpan, TextKind } from '../contract'
/** 语言插件共用的建树工具:取子节点、算头部、把一串语句切成块。 */
@@ -164,3 +164,128 @@ export function listBlocks(
export function sameNode(a: Node | null | undefined, b: Node): boolean {
return !!a && a.startIndex === b.startIndex && a.endIndex === b.endIndex && a.type === b.type
}
// ─── 异常类 ───────────────────────────────────────────────────────────────────
/** 语言怎么认异常类、类主体里哪些子块是类属性、哪个是构造函数。 */
export interface ExceptionRules {
/** 父类一项写的类名(点分写法取最后一节);关键字参数、展开、调用这类不是类名的项没有。 */
baseName: (base: BaseFacts) => string | undefined
/** 光看名字就认得是异常类:语言自带的异常类,或按命名习惯以 Error 这类后缀结尾。 */
isExceptionName: (name: string) => boolean
/** 类主体里的子块是类属性时它的名字、类型、值;不是时没有。 */
field: (block: CodeBlock) => Omit<FieldFacts, 'docs'> | undefined
/** 子块是构造函数时它的参数(Python 去掉 self);不是时没有。 */
constructorParameters: (block: CodeBlock) => ParameterFacts[] | undefined
}
function classBlocks(blocks: readonly CodeBlock[]): ClassBlock[] {
return blocks.flatMap(block => [...(block.kind === 'class' ? [block] : []), ...classBlocks(block.children)])
}
/** 紧挨在 child 前面的注释块(连着几块都算,中间没隔别的子块),照源码顺序:讲的是它下面的这个 child。 */
function leadingComments(children: readonly CodeBlock[], child: CodeBlock): CodeBlock[] {
const at = children.indexOf(child)
let from = at
while (from > 0 && children[from - 1].kind === 'comment')
from--
return children.slice(from, at)
}
/** 注释块里的各条注释(几条行注释连成的一块是好几个节点);Python 当注释用的字符串是那个字符串。 */
function commentNodes(block: CodeBlock): Node[] {
if (block.node.type !== 'comment')
return syntaxChildren(block.node).slice(0, 1)
const nodes: Node[] = []
for (let node: Node | null = block.node; node?.type === 'comment' && node.endIndex <= block.header.endIndex; node = node.nextSibling)
nodes.push(node)
return nodes
}
/** 块自己的说明节点:属性表块的 facts.doc,一行译文块的 doc(Python 字段的属性 docstring)。 */
function ownDoc(block: CodeBlock): Node | undefined {
if (!('facts' in block))
return block.doc
return block.kind === 'import' ? undefined : block.facts.doc
}
/** 类属性的说明,照源码顺序:紧挨在它上面的注释块、它这一行行尾的注释、Python 紧跟在后面的属性 docstring。 */
function fieldDocs(children: readonly CodeBlock[], block: CodeBlock): Node[] {
const trailing = block.node.nextNamedSibling
const doc = ownDoc(block)
return [
...leadingComments(children, block).flatMap(commentNodes),
...(trailing?.type === 'comment' && trailing.startPosition.row === block.node.endPosition.row ? [trailing] : []),
...(doc ? [doc] : []),
]
}
/**
* 类块换成异常块:类属性连同紧挨在它前面的注释块并进属性表,构造函数(写了几个取最后一个,重载的实现写在最后)的参数进属性表,
* 留下的子块(方法、构造函数本身、别的语句)不管写在哪都照源码顺序留作子块;头部是整个类(到它连同各子块的最后一行),
* 「编辑」时整段一起改,webview 在它编辑时不画留下的子块。
*/
function exceptionBlock(block: ClassBlock, rules: ExceptionRules): ExceptionBlock {
const { children, facts } = block
const fields: FieldFacts[] = []
const members: CodeBlock[] = []
let parameters: ParameterFacts[] | undefined
for (const child of children) {
const field = rules.field(child)
if (field) {
fields.push({ ...field, docs: fieldDocs(children, child) })
members.push(child)
}
parameters = rules.constructorParameters(child) ?? parameters
}
const absorbed = new Set(members.flatMap(member => [...leadingComments(children, member), member]))
const ends = [block.header, nodeSpan(block.node), ...children.flatMap(child => [child.header, nodeSpan(child.node)])]
const last = ends.reduce((end, span) => span.endIndex > end.endIndex ? span : end)
return {
kind: 'exception',
node: block.node,
header: { ...block.header, endIndex: last.endIndex, endRow: last.endRow },
children: children.filter(child => !absorbed.has(child)),
...(block.attributes && { attributes: block.attributes }),
facts: {
name: facts.name,
doc: facts.doc,
access: facts.access,
decorators: facts.decorators,
bases: facts.bases,
fields,
...(parameters && { parameters }),
},
}
}
/**
* 认出异常类,把它们的类块换成异常块(见 exceptionBlock)。父类里有一项是异常类就是异常类:光看名字认得出(isExceptionName),
* 或是同一文件里(不论写在前面还是后面、嵌套在哪)认出来的异常类,一层层往下传。父类从别的文件导入、名字又不合命名习惯的认不出来。
*/
export function withExceptions(blocks: CodeBlock[], rules: ExceptionRules): CodeBlock[] {
const classes = classBlocks(blocks)
const local = new Set<string>()
const isException = (facts: ClassFacts): boolean => facts.bases.some((base) => {
const name = rules.baseName(base)
return name !== undefined && (rules.isExceptionName(name) || local.has(name))
})
let grew = true
while (grew) {
grew = false
for (const { facts } of classes) {
const name = facts.name?.text
if (name !== undefined && !local.has(name) && isException(facts)) {
local.add(name)
grew = true
}
}
}
const convert = (list: CodeBlock[]): CodeBlock[] => list.map((block) => {
const children = convert(block.children)
const same = children.every((child, i) => child === block.children[i])
const next: CodeBlock = same ? block : { ...block, children }
return next.kind === 'class' && isException(next.facts) ? exceptionBlock(next, rules) : next
})
return convert(blocks)
}
+37 -3
View File
@@ -1,7 +1,7 @@
import type { Access, BaseFacts, BlockShape, ClassFacts, CodeBlock, DeclaratorFacts, EntryFacts, EnumMemberFacts, FunctionFacts, ImportFacts, ImportItem, LanguageAdapter, Node, ParameterFacts, TextKind, TokenRole } from '../contract'
import type { CommentRules } from './shared'
import type { CommentRules, ExceptionRules } from './shared'
import * as path from 'node:path'
import { children, compoundShape, extendHeader, headerSpan, leafBlock, leafShape, listBlocks, named, nodeSpan, sameNode, syntaxChildren } from './shared'
import { children, compoundShape, extendHeader, headerSpan, leafBlock, leafShape, listBlocks, named, nodeSpan, sameNode, syntaxChildren, withExceptions } from './shared'
import { typescriptNameReader } from './typescriptNames'
const GRAMMARS: Record<string, string> = {
@@ -476,6 +476,40 @@ function blockReader(typed: boolean): (root: Node) => CodeBlock[] {
return root => statements(named(root), true, -1)
}
// ─── 异常类 ───────────────────────────────────────────────────────────────────
/** 自带的错误类(Error、TypeError、RangeError、AggregateError……)名字都以 Error 结尾,DOMException 以 Exception 结尾;自己写的照这个习惯起名。 */
const EXCEPTION_NAME = /(?:Error|Exception)$/
/** 类字段:TypeScript 的 public_field_definition(名字在 name),JavaScript 的 field_definition(名字在 property)。 */
const FIELDS = new Set(['public_field_definition', 'field_definition'])
/** extends 后面写的类名:`X`、`ns.X` 取 X;调用(`Mixin(Base)`)这类没有。 */
function className(node: Node): string | undefined {
switch (node.type) {
case 'identifier':
case 'type_identifier':
return node.text
case 'member_expression':
return node.childForFieldName('property')?.text
default:
return undefined
}
}
/** 类属性是类字段(含 static,值不是带语句块主体的函数、类表达式的:那些照常是方法、类块);构造函数是 `constructor`。 */
const TYPESCRIPT_EXCEPTIONS: ExceptionRules = {
baseName: base => className(base.value),
isExceptionName: name => EXCEPTION_NAME.test(name),
field: (block) => {
const member = block.node
const name = block.kind === 'field' && FIELDS.has(member.type) ? member.childForFieldName('name') ?? member.childForFieldName('property') : null
if (!name)
return undefined
return { name, type: annotatedType(member.childForFieldName('type')), value: member.childForFieldName('value') ?? undefined }
},
constructorParameters: block => block.kind === 'function' && block.facts.name?.text === 'constructor' ? block.facts.parameters : undefined,
}
// ─── 源码着色 ─────────────────────────────────────────────────────────────────
/** 流程关键字;其余字母关键字都算声明关键字(storage)。 */
@@ -584,7 +618,7 @@ export function createTypeScriptAdapter(wasmDir: string): LanguageAdapter {
throw new Error(`TypeScript 语言插件不支持 ${languageId}`)
return path.join(wasmDir, file)
},
toBlocks: (root, _source, languageId) => blockReader(TYPED_LANGUAGES.has(languageId))(root),
toBlocks: (root, _source, languageId) => withExceptions(blockReader(TYPED_LANGUAGES.has(languageId))(root), TYPESCRIPT_EXCEPTIONS),
tokenRole: typescriptTokenRole,
displayNames: typescriptNameReader,
}
@@ -725,8 +725,8 @@ describe('属性表', () => {
expect(tsRows[32]).toEqual(['名称: Plain', '访问级别: 私有', '抽象: 否', '装饰器: @sealed'])
const pyRows = rowTexts(await describeAll(py, 'python', pythonZhTranslator, readFixture('classes.py')))
expect(pyRows[6]).toEqual(['名称: AppException', '说明: 应用异常的基类。\n\n带上 HTTP 状态码。', '访问级别: 公共', '父类: Exception'])
expect(pyRows[18]).toEqual(['名称: NotFound', '说明: 找不到资源。', '访问级别: 公共', '父类: AppException'])
expect(pyRows[6]).toEqual(['名称: AppResponse', '说明: 应用响应的基类。\n\n带上 HTTP 状态码。', '访问级别: 公共', '父类: BaseModel'])
expect(pyRows[18]).toEqual(['名称: NotFound', '说明: 找不到资源。', '访问级别: 公共', '父类: AppResponse'])
expect(pyRows[24]).toEqual(['名称: Meta', '访问级别: 公共', '装饰器: @dataclass(frozen=True)', '父类: Base, metaclass=ABCMeta'])
expect(pyRows[29]).toEqual(['名称: _Hidden', '访问级别: 私有'])
expect(pyRows[38]).toEqual(['名称: Local', '说明: 局部类。'])
@@ -987,11 +987,11 @@ describe('源码下标(跳到定义用)', () => {
expect(linked(valueOf(found, '实现的接口'))).toEqual([['Loader', at('Loader')], ['K', at('K>,')], ['Disposable', at('Disposable')]])
expect(found.filter(row => row.defines).map(row => [row.label, source.slice(row.defines?.startIndex, row.defines?.endIndex)])).toEqual([['类型参数', 'K, T extends Entity']])
const pySource = 'class NotFound(errors.AppException, metaclass=ABCMeta):\n pass'
const pySource = 'class NotFound(models.AppResponse, metaclass=ABCMeta):\n pass'
const pyRows = await describeAll(py, 'python', pythonZhTranslator, pySource)
expect(linked(valueOf(pyRows.get(1), '父类'))).toEqual([
['errors', pySource.indexOf('errors')],
['AppException', pySource.indexOf('AppException')],
['models', pySource.indexOf('models')],
['AppResponse', pySource.indexOf('AppResponse')],
['metaclass', pySource.indexOf('metaclass')],
['ABCMeta', pySource.indexOf('ABCMeta')],
])
+73 -17
View File
@@ -1,4 +1,4 @@
import type { Access, AttributeNames, BaseFacts, BlockKind, ClassFacts, CommonAttribute, Defines, EntryFacts, EnumFacts, FactBlock, FunctionFacts, ImportFacts, ImportItem, Inline, IterationClause, IterationFacts, IterationResult, LanguageAdapter, Markdown, Node, PreviewText, PropertyItem, PropertyRow, Segment, SegmentGroup, SourceDocument, SourceRange, SourceSpan, TextBlock, TokenRole, Translator, VariableBlock } from '../contract'
import type { Access, AttributeNames, BaseFacts, BlockKind, ClassFacts, CommonAttribute, Defines, EntryFacts, EnumFacts, ExceptionFacts, FactBlock, FunctionFacts, ImportFacts, ImportItem, Inline, IterationClause, IterationFacts, IterationResult, LanguageAdapter, Markdown, Node, ParameterFacts, PreviewText, PropertyItem, PropertyRow, Segment, SegmentGroup, SourceDocument, SourceRange, SourceSpan, TextBlock, TokenRole, Translator, VariableBlock } from '../contract'
import { commonIndent, splitLines } from '../editText'
/** 按所在语法节点区分译法的关键字,例如 Python 的 else:if 后「否则」,for / while 后「未中断时」。 */
@@ -148,6 +148,7 @@ const KIND_ZH: Record<BlockKind, string> = {
function: '函数',
class: '类',
enum: '枚举',
exception: '异常',
type: '类型',
field: '字段',
const: '常量',
@@ -451,6 +452,8 @@ export function createZhTemplateTranslator(table: ZhTable): Translator {
return classRows(block.facts, phrases)
case 'enum':
return enumRows(block.facts, phrases)
case 'exception':
return exceptionRows(block.facts, phrases)
case 'import':
return importRows(block.facts, phrases)
default:
@@ -599,23 +602,28 @@ function functionRows(facts: FunctionFacts, say: Phrases): PropertyRow[] {
...accessRows(facts.access),
...flagRows('async', facts.async),
...decoratorRows(facts.decorators, say),
{
label: '输入参数',
value: count(facts.parameters.length),
columns: ['名称', '类型', '默认值'],
items: facts.parameters.map(param => ({
cells: [
param.optional ? [...say.translated(param.name), { text: '?', role: 'plain' }] : say.translated(param.name),
param.type ? say.translated(param.type) : [hint('未标注')],
param.default ? say.translated(param.default) : [],
],
...defining(param.name),
})),
},
parameterRow('输入参数', facts.parameters, say),
{ label: '输出参数', value: facts.returnType ? say.translated(facts.returnType) : [hint('未标注')] },
]
}
/** 参数表:列名是名称 / 类型 / 默认值,每个参数一行,讲的定义是参数名;没有默认值的格子留空。 */
function parameterRow(label: string, parameters: ParameterFacts[], say: Phrases): PropertyRow {
return {
label,
value: count(parameters.length),
columns: ['名称', '类型', '默认值'],
items: parameters.map(param => ({
cells: [
param.optional ? [...say.translated(param.name), { text: '?', role: 'plain' }] : say.translated(param.name),
param.type ? say.translated(param.type) : [hint('未标注')],
param.default ? say.translated(param.default) : [],
],
...defining(param.name),
})),
}
}
/**
* 类:名称 / 说明 / 访问级别 / 抽象(只有分这个的语言才有)/ 装饰器 / 类型参数 / 父类 / 实现的接口,
* 说明及以后各行有才显示;类型参数、父类、实现的接口各是一行,多项用逗号隔开。类型参数那一行讲的是这几个泛型参数的定义。
@@ -646,14 +654,14 @@ function baseText(base: BaseFacts, say: Phrases): Inline[] {
* 说明是成员的注释、JSDoc、属性 docstring 的原文用空格连起来,一个成员都没有说明时不给这一列。
*/
function enumRows(facts: EnumFacts, say: Phrases): PropertyRow[] {
const docs = facts.members.map(member => member.docs.map(node => say.doc(node)?.markdown).filter(Boolean).join(' '))
const docs = facts.members.map(member => docsText(member.docs, say))
const described = docs.some(Boolean)
return [
{ label: '名称', value: say.name(facts.name, 'type') },
...docRows(facts.doc, say),
...accessRows(facts.access),
...decoratorRows(facts.decorators, say),
...(facts.bases.length > 0 ? [{ label: '继承', value: joined(facts.bases.map(base => baseText(base, say)), ', ') }] : []),
...baseRows(facts.bases, say),
{
label: '成员',
value: count(facts.members.length),
@@ -662,7 +670,7 @@ function enumRows(facts: EnumFacts, say: Phrases): PropertyRow[] {
cells: [
say.name(member.name, 'variable'),
member.value ? say.translated(member.value) : [hint(member.computed === undefined ? '自动' : String(member.computed))],
...(described ? [docs[i] ? [segment(docs[i], 'comment', undefined)] : []] : []),
...(described ? [docCell(docs[i])] : []),
],
...defining(member.name),
})),
@@ -670,6 +678,54 @@ function enumRows(facts: EnumFacts, say: Phrases): PropertyRow[] {
]
}
/**
* 异常(Python、TypeScript 同一张表):名称 / 说明 / 访问级别 / 装饰器 / 继承(照写的父类),说明及以后有才显示;
* 类属性收成一张表:属性 | 类型 | 值 | 说明,一个属性都没写类型时没有「类型」列、都没有说明时没有「说明」列,没写的格子留空;
* 构造参数同函数的输入参数表。没有类属性、没写构造函数时没有这两行。
*/
function exceptionRows(facts: ExceptionFacts, say: Phrases): PropertyRow[] {
const docs = facts.fields.map(field => docsText(field.docs, say))
const described = docs.some(Boolean)
const typed = facts.fields.some(field => field.type)
const fields: PropertyRow = {
label: '属性',
value: count(facts.fields.length),
columns: ['属性', ...(typed ? ['类型'] : []), '值', ...(described ? ['说明'] : [])],
items: facts.fields.map((field, i) => ({
cells: [
say.name(field.name, 'variable'),
...(typed ? [field.type ? say.translated(field.type) : []] : []),
field.value ? say.translated(field.value) : [],
...(described ? [docCell(docs[i])] : []),
],
...defining(field.name),
})),
}
return [
{ label: '名称', value: facts.name ? say.name(facts.name, 'type') : [hint('匿名')] },
...docRows(facts.doc, say),
...accessRows(facts.access),
...decoratorRows(facts.decorators, say),
...baseRows(facts.bases, say),
...(facts.fields.length > 0 ? [fields] : []),
...(facts.parameters ? [parameterRow('构造参数', facts.parameters, say)] : []),
]
}
/** 继承:照写的父类,逗号隔开;没有父类时没有这一行。 */
function baseRows(bases: BaseFacts[], say: Phrases): PropertyRow[] {
return bases.length > 0 ? [{ label: '继承', value: joined(bases.map(base => baseText(base, say)), ', ') }] : []
}
/** 表格里一格说明:几段说明文字的原文用空格连起来。 */
function docsText(nodes: Node[], say: Phrases): string {
return nodes.map(node => say.doc(node)?.markdown).filter(Boolean).join(' ')
}
function docCell(text: string): Inline[] {
return text ? [segment(text, 'comment', undefined)] : []
}
/** 函数、类、常量、变量的说明:有文字才有这一行。 */
function docRows(doc: Node | undefined, say: Phrases): PropertyRow[] {
const markdown = doc && say.doc(doc)
@@ -15,6 +15,7 @@ body {
--cr-kind-function: #dcdcaa;
--cr-kind-type: #4ec9b0;
--cr-kind-enum: #b180d7;
--cr-kind-exception: #f14c4c;
--cr-kind-data: #9cdcfe;
--cr-kind-branch: #ce9178;
--cr-kind-loop: #8f9cff;
@@ -45,6 +46,7 @@ body.vscode-high-contrast-light {
--cr-kind-function: #795e26;
--cr-kind-type: #267f99;
--cr-kind-enum: #652d90;
--cr-kind-exception: #cd3131;
--cr-kind-data: #001080;
--cr-kind-branch: #a31515;
--cr-kind-loop: #4f56d6;
@@ -59,6 +61,7 @@ body.vscode-high-contrast-light {
[data-kind="function"] { --kind: var(--cr-kind-function); }
:is([data-kind="class"], [data-kind="type"]) { --kind: var(--cr-kind-type); }
[data-kind="enum"] { --kind: var(--cr-kind-enum); }
[data-kind="exception"] { --kind: var(--cr-kind-exception); }
:is([data-kind="field"], [data-kind="const"], [data-kind="variable"]) { --kind: var(--cr-kind-data); }
:is([data-kind="if"], [data-kind="else"]) { --kind: var(--cr-kind-branch); }
[data-kind="loop"] { --kind: var(--cr-kind-loop); }
@@ -326,12 +326,34 @@ describe('跳到定义:TS / Python 源码里闪哪一处', () => {
const views = await withBlocks(ts, 'typescript', source, blocks => toBlockViews(blocks, { source, language: ts, markers: [] }, typescriptZhTranslator))
expect([...collapsedIds(views, {})].sort()).toEqual(['0', '0.0'])
const pySource = ['class NotFound(AppException):', ' """找不到。"""', ' def run(self):', ' pass'].join('\n')
const pySource = ['class NotFound(AppResponse):', ' """找不到。"""', ' def run(self):', ' pass'].join('\n')
const atPy = (needle: string): ReturnType<typeof jump> => jump(py, 'python', pythonZhTranslator, pySource, needle)
expect(await atPy('NotFound')).toEqual({ plan: { id: '0', expand: [] }, flashed: 'class NotFound(AppException):' })
expect(await atPy('NotFound')).toEqual({ plan: { id: '0', expand: [] }, flashed: 'class NotFound(AppResponse):' })
expect(await atPy('run')).toEqual({ plan: { id: '0.0', expand: ['0'] }, flashed: 'def run(self):' })
})
it('异常块:类名闪整块,属性名只闪「属性」表里它那一行,构造函数的参数只闪「构造参数」表里它那一行(都先展开那一行);留下的构造函数照常是子块,异常块默认折叠', async () => {
const source = [
'class Failed(Exception):',
' status = 500',
'',
' def __init__(self, message: str) -> None:',
' self.message = message',
].join('\n')
const at = (needle: string): ReturnType<typeof jump> => jump(py, 'python', pythonZhTranslator, source, needle)
expect(await at('Failed')).toEqual({ plan: { id: '0', expand: [] }, flashed: 'class Failed(Exception):' })
expect(await at('status')).toEqual({ plan: { id: '0', row: '0/p3/0', expand: ['0/p3'] }, flashed: 'status | 500' })
expect(await at('message: str')).toEqual({ plan: { id: '0', row: '0/p4/0', expand: ['0/p4'] }, flashed: 'message | 字符串 | ' })
expect(await at('__init__')).toEqual({ plan: { id: '0.0', expand: ['0'] }, flashed: 'def __init__(self, message: str) -> None:' })
const tsSource = 'export class ApiError extends Error {\n status = 500\n constructor(public code: string) {\n super(code)\n }\n}'
const atTs = (needle: string): ReturnType<typeof jump> => jump(ts, 'typescript', typescriptZhTranslator, tsSource, needle)
expect(await atTs('status')).toEqual({ plan: { id: '0', row: '0/p3/0', expand: ['0/p3'] }, flashed: 'status | 500' })
expect(await atTs('code:')).toEqual({ plan: { id: '0', row: '0/p4/0', expand: ['0/p4'] }, flashed: 'code | 字符串 | ' })
const views = await withBlocks(ts, 'typescript', tsSource, blocks => toBlockViews(blocks, { source: tsSource, language: ts, markers: [] }, typescriptZhTranslator))
expect([...collapsedIds(views, {})].sort()).toEqual(['0', '0.0'])
})
it('库规则认领的块:类名闪整块,字段名只闪字段表里它那一行(先展开「字段」那一行);留下的方法照常,卡片默认折叠', async () => {
const source = [
'from sqlalchemy.orm import Mapped, mapped_column',
+1 -1
View File
@@ -3,7 +3,7 @@
*/
export type BlockKind
= | 'import' | 'function' | 'class' | 'enum' | 'type' | 'field' | 'const' | 'variable'
= | 'import' | 'function' | 'class' | 'enum' | 'exception' | 'type' | 'field' | 'const' | 'variable'
| 'if' | 'else' | 'loop' | 'try' | 'catch' | 'finally' | 'return' | 'statement' | 'comment'
/** 着色用的词性,源码着色和译文着色共用。 */
@@ -115,6 +115,10 @@ describe('眼下折叠着哪些', () => {
expect([...collapsedIds([block('0', 'enum', [block('0.0', 'function')]), block('1', 'if')], {})]).toEqual(['0', '0.0'])
})
it('没点过:异常块(下面留着构造函数、方法)和类块一样折叠', () => {
expect([...collapsedIds([block('0', 'exception', [block('0.0', 'function')]), block('1', 'if')], {})]).toEqual(['0', '0.0'])
})
it('点过的按用户的:展开的函数、类保持展开,折叠的其他块、属性行保持折叠', () => {
const folds = { '1': false, '2': false, '1.0': true, '1.0.0': false, [propertyRowId('0', 2)]: true, [propertyRowId('2.0', 1)]: false }
expect([...collapsedIds(BLOCKS, folds)].sort()).toEqual(['0/p2', '1.0', '2.0'])
@@ -54,9 +54,9 @@ function readViewState(saved: unknown): ViewState {
return isScrollAnchor(state.scroll) ? { editing, folds, scroll: state.scroll } : { editing, folds }
}
/** 块默认折叠吗:函数、类、枚举块和库规则的卡片收起子块(属性表照常显示),其余块展开。属性行和带下一层的子行默认都展开。 */
/** 块默认折叠吗:函数、类、枚举、异常块和库规则的卡片收起子块(属性表照常显示),其余块展开。属性行和带下一层的子行默认都展开。 */
export function foldedByDefault(block: Pick<BlockView, 'kind' | 'badge'>): boolean {
return block.badge !== undefined || block.kind === 'function' || block.kind === 'class' || block.kind === 'enum'
return block.badge !== undefined || ['function', 'class', 'enum', 'exception'].includes(block.kind)
}
/** 眼下折叠着的块、属性行和子行:用户点过的按他点的,没点过的按 foldedByDefault。 */