✨ feat(vscode): 代码阅读把说明里的 @param / @return / @throws 拆到参数表、输出、异常表
This commit is contained in:
1 parent
c2982537d7
commit
0958506681
43 files changed
+1300
-323
No files matched your search
@@ -240,14 +240,14 @@
|
|||||||
|
|
||||||
卡片外观(折叠箭头、种类图标、底色、悬停高亮、「编辑 / 预览」切换)和其他块一样,只是内容换成一张属性表:左列属性名(界面字体、`descriptionForeground` 色,宽 76px,这张表里有更长的属性名时整列按最长的那个放宽,值照样对齐在同一处,属性名不折行:shell 的「管道里任一步失败都算失败」),右列是值(等宽字体,按词性着色,长了换行),两列间距 12px,行高 20px。「输入参数」「导入项」和值是字典 / 对象字面量的「值」「初始值」下面展开子行,属性名前带折叠箭头(图标、大小、颜色同块的折叠箭头;点箭头或属性名切换,默认展开,折叠后值「N 个」「字典 N 项」照常显示,只收起子行);子行往右缩进 5px,左边一条 1px 竖线(`editorIndentGuide-background1`);输入参数例外,是一张表格(见下)。字典 / 对象的子行分键 / 值两列,键列宽 150px(编辑区窄时按 28% 收),值列占剩下的。子行下面还能再展开一层(嵌套的字典 / 对象):带下一层的子行第一列前面有折叠箭头(点箭头或第一列切换,默认展开),同一层里其余子行在同样的位置留空、第一列对齐;下一层同样往右缩进 5px、左边一条竖线,竖线对着上一层的箭头,每层自己的两列对齐。同一层里没有带下一层的子行时不留箭头的位置(和导入项的子行一样)。
|
卡片外观(折叠箭头、种类图标、底色、悬停高亮、「编辑 / 预览」切换)和其他块一样,只是内容换成一张属性表:左列属性名(界面字体、`descriptionForeground` 色,宽 76px,这张表里有更长的属性名时整列按最长的那个放宽,值照样对齐在同一处,属性名不折行:shell 的「管道里任一步失败都算失败」),右列是值(等宽字体,按词性着色,长了换行),两列间距 12px,行高 20px。「输入参数」「导入项」和值是字典 / 对象字面量的「值」「初始值」下面展开子行,属性名前带折叠箭头(图标、大小、颜色同块的折叠箭头;点箭头或属性名切换,默认展开,折叠后值「N 个」「字典 N 项」照常显示,只收起子行);子行往右缩进 5px,左边一条 1px 竖线(`editorIndentGuide-background1`);输入参数例外,是一张表格(见下)。字典 / 对象的子行分键 / 值两列,键列宽 150px(编辑区窄时按 28% 收),值列占剩下的。子行下面还能再展开一层(嵌套的字典 / 对象):带下一层的子行第一列前面有折叠箭头(点箭头或第一列切换,默认展开),同一层里其余子行在同样的位置留空、第一列对齐;下一层同样往右缩进 5px、左边一条竖线,竖线对着上一层的箭头,每层自己的两列对齐。同一层里没有带下一层的子行时不留箭头的位置(和导入项的子行一样)。
|
||||||
|
|
||||||
输入参数的表格(原型 `paramRows`):往右缩进 5px,第一行是表头「名称 | 类型 | 默认值」,下面每个参数一行。列宽按整列的内容定,webview 不认是哪种表格(库规则的字段表同样排,见 `tableTemplate`):整张表格一个网格、各行借用它的列,前几列宽裕时放到整列不换行,最后一列(默认值、说明)分剩下的、至少留 `min(20%, 8em)`;放不下时先让最后一列换行,编辑区再窄前几列平分剩下的宽度、在格子里换行(长名字从中间断开),不撑出卡片、不藏掉哪一列。格子左右留 8px、上下 2px,长类型、长默认值在格子里换行,不挤掉别的列。外框 1px、圆角 4px,格子之间、行之间 1px 分隔线,颜色都是主题的表格分隔色(`--vscode-tree-tableColumnsBorder`,主题没给时退到 `--vscode-editorWidget-border`);表头垫主题的表头底色(`--vscode-keybindingTable-headerBackground`,比编辑区深一档或浅一档),列名界面字体 12px、`descriptionForeground` 色。没有默认值的格子留空。折叠「输入参数」那一行时整张表收起;子行 id、跳到定义闪「它那个参数」、高亮名字的各处出现都和列表一样,闪的是表格里那一行(行边框和相邻行共用,改在行里面描一圈聚焦色)。哪些子行排成表格由翻译插件定:给了列名(`PropertyRow.columns`)的排成表格,没给的(导入项、字典 / 对象的各项)照旧是列表,webview 只看有没有列名。
|
输入参数的表格(原型 `paramRows`):往右缩进 5px,第一行是表头「名称 | 类型 | 默认值」,下面每个参数一行。列宽按整列的内容定,webview 不认是哪种表格(库规则的字段表同样排,见 `tableTemplate`):整张表格一个网格、各行借用它的列,前几列宽裕时放到整列不换行,最后一列(默认值、说明)分剩下的、至少留 `min(20%, 8em)`;放不下时先让最后一列换行,编辑区再窄前几列平分剩下的宽度、在格子里换行(长名字从中间断开),不撑出卡片、不藏掉哪一列。格子左右留 8px、上下 2px,长类型、长默认值在格子里换行,不挤掉别的列。外框 1px、圆角 4px,格子之间、行之间 1px 分隔线,颜色都是主题的表格分隔色(`--vscode-tree-tableColumnsBorder`,主题没给时退到 `--vscode-editorWidget-border`);表头垫主题的表头底色(`--vscode-keybindingTable-headerBackground`,比编辑区深一档或浅一档),列名界面字体 12px、`descriptionForeground` 色。没有默认值的格子留空。格子可以是 Markdown(参数的「说明」、「异常」表的两列、对不上的参数的说明):按「注释的 Markdown 排版」显示(界面字体、注释色),着色的词照旧排在等宽字体的段落里,差异逐词标。属性行跟着的说明(`PropertyRow.note`,输出参数的 `@return`)接在值后面,同一行放得下就并排,剩下的地方不到 12 个字宽时换到下一行。折叠「输入参数」那一行时整张表收起;子行 id、跳到定义闪「它那个参数」、高亮名字的各处出现都和列表一样,闪的是表格里那一行(行边框和相邻行共用,改在行里面描一圈聚焦色)。哪些子行排成表格由翻译插件定:给了列名(`PropertyRow.columns`)的排成表格,没给的(导入项、字典 / 对象的各项)照旧是列表,webview 只看有没有列名。
|
||||||
|
|
||||||
| 种类 | 行 |
|
| 种类 | 行 |
|
||||||
| --- | --- |
|
| --- | --- |
|
||||||
| 函数 | 名称(函数色;匿名函数写「匿名」)/ 说明(有说明才显示:JSDoc 去掉 `/**`、`*/` 和每行开头的 `*`,docstring 去掉引号;再去掉共同缩进和首尾空行,`@param` 这类标签行照抄;得到的原文按 Markdown 渲染,见「注释的 Markdown 排版」;里面的名字不能点)/ 访问级别 / 异步(是 / 否)/ 装饰器(有才显示,源码原样照抄、按源码着色,多个换行)/ 输入参数(`N 个` 或「无」,展开成表格,列名 名称 \| 类型 \| 默认值,每个参数一行:名称 \| 类型(没标注写「未标注」)\| 默认值(译文,没有留空))/ 输出参数(返回类型的译文,没标注写「未标注」) |
|
| 函数 | 名称(函数色;匿名函数写「匿名」)/ 说明(有正文才显示:JSDoc 去掉 `/**`、`*/` 和每行开头的 `*`,docstring 去掉引号;再去掉共同缩进和首尾空行,拆掉块标签(见「注释的 Markdown 排版」),剩下的正文按 Markdown 渲染)/ 对不上的参数(说明里有 `@param` 的名字在输入参数里没有时才显示:`N 个`,展开成表格「名称 \| 说明」,名称是 hint 色、悬停「没有这个参数」)/ 访问级别 / 异步(是 / 否)/ 装饰器(有才显示,源码原样照抄、按源码着色,多个换行)/ 输入参数(`N 个` 或「无」,展开成表格,列名 名称 \| 类型 \| 默认值,每个参数一行:名称 \| 类型(没标注写「未标注」)\| 默认值(译文,没有留空);任何一个参数对上了 `@param` 时多一列「说明」(Markdown,没对上的格子留空),按名字对,`*args` / `**kwargs` / `...rest` 去掉前缀对)/ 输出参数(返回类型的译文,没标注写「未标注」;写了 `@return` 时类型后面跟着这段说明,Markdown、注释色)/ 异常(写了 `@throws` 才显示:`N 个`,展开成表格「异常 \| 触发条件」,一条一行,都是 Markdown,异常名能点,见「注释的 Markdown 排版」的引用) |
|
||||||
| 类 | 名称(类型色;匿名类 `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` 后面每个类型的译文,逗号隔开) |
|
| 类 | 名称(类型色;匿名类 `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` 后面每个类型的译文,逗号隔开)/ 构造参数(说明里写了 `@param` 才显示:类卡片没有构造参数表可对,照写成「名称 \| 说明」表)/ 异常(同函数) |
|
||||||
| 枚举 | 名称(类型色)/ 说明(有说明才显示,规则同函数)/ 访问级别(同类)/ 装饰器(有才显示,同函数)/ 继承(有才显示:Python 类名后面括号里照写的父类,如 `str, enum.Enum`;TypeScript 没有)/ 成员(`N 个` 或「无」,展开成表格,列名 成员 \| 值 \| 说明,每个成员一行:成员名(variable 色)\| 值(写了值是值的译文;没写值时是语言算出来的值,TypeScript 数值枚举自增的 0、1、2……,算不出来和 Python 的 `auto()` 写「自动」,这两种按 hint 着色)\| 说明(成员的注释、JSDoc、属性 docstring 的原文用空格连起来,comment 色;一个成员都没有说明时没有这一列);讲的定义是成员名,跳到成员名只闪那一行)。原型「枚举」一节(`ENUM_SAMPLE`、`enumCard`) |
|
| 枚举 | 名称(类型色)/ 说明(有说明才显示,规则同函数)/ 访问级别(同类)/ 装饰器(有才显示,同函数)/ 继承(有才显示: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;讲的定义是参数名)。见「异常」 |
|
| 异常 | 名称(类型色;匿名类写「匿名」)/ 说明(有说明才显示,规则同函数)/ 访问级别(同类)/ 装饰器(有才显示,同函数)/ 继承(有才显示:照写的父类,同类的「父类」,如 `Exception`、`errors.HttpException<字符串>`)/ 属性(有类属性才显示:`N 个`,展开成表格,列名 属性 \| 类型 \| 值 \| 说明,每个属性一行:属性名(variable 色)\| 类型(写了标注是它的译文,一个属性都没标注时没有这一列)\| 值(写了值是值的译文,没写留空)\| 说明(同枚举成员的说明,一个属性都没有说明时没有这一列);讲的定义是属性名)/ 构造参数(写了构造函数才显示:表格同函数的「输入参数」,Python 去掉 self,类说明里的 `@param` 同函数对进「说明」列、对不上的排在说明下面;讲的定义是参数名;没写构造函数时同类,说明里的 `@param` 照写成表)/ 异常(同函数)。见「异常」 |
|
||||||
| 导入 | 来源(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` 导入多个模块时写模块名) |
|
| 导入 | 来源(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`)就重复几组 |
|
| 常量 / 变量 | 名称(解构声明写整个模式,如 `{ p, q }`)/ 说明(有说明才显示:Python 的属性 docstring,规则同函数;一条语句一个,排在第一个名称后面)/ 访问级别 / 类型(写了标注才有)/ 值(常量)或初始值(变量):初始化表达式的译文,没有写「无」;是字典 / 对象字面量时见下面「字典 / 对象字面量」。一条语句声明几个变量(`let a = 1, b = 2`)就重复几组 |
|
||||||
|
|
||||||
@@ -462,8 +462,14 @@
|
|||||||
|
|
||||||
### 注释的 Markdown 排版
|
### 注释的 Markdown 排版
|
||||||
|
|
||||||
- 用在:注释块(`#`、`//`、`/* */`、不挂在函数、类上的 JSDoc、Python 模块开头的 docstring 和别的当注释用的单独字符串)的预览,函数、类、常量、变量属性表的「说明」,字段译文下面的说明。视图数据里它们是 `{ markdown }`(见「通用契约」的 `Markdown`),webview 按数据决定怎么渲染,不猜。
|
- 用在:注释块(`#`、`//`、`/* */`、不挂在函数、类上的 JSDoc、Python 模块开头的 docstring 和别的当注释用的单独字符串)的预览,函数、类、常量、变量属性表的「说明」,参数表的「说明」列、「异常」表、输出参数后面的 `@return`,字段译文下面的说明。视图数据里它们是 `{ markdown }`(见「通用契约」的 `Markdown`),webview 按数据决定怎么渲染,不猜。
|
||||||
- 原文:翻译插件去掉注释符号(`#`、`//`、`/*` `*/` 和每行开头的 `*`)、引号和中文名记号(`SourceDocument.markers`,见「中文名」),再去掉行尾空白、首尾空行和共同缩进,保留换行,不做别的转换。一个注释节点(块注释、docstring)的第一行紧跟在注释符号 / 引号后面,不参与算共同缩进(同 PEP 257);几条行注释连成的一块每行地位相同,第一行也算,缩进四格的代码块才留得住。
|
- 原文:翻译插件去掉注释符号(`#`、`//`、`/*` `*/` 和每行开头的 `*`)、引号和中文名记号(`SourceDocument.markers`,见「中文名」),再去掉行尾空白、首尾空行和共同缩进,保留换行,不做别的转换。一个注释节点(块注释、docstring)的第一行紧跟在注释符号 / 引号后面,不参与算共同缩进(同 PEP 257);几条行注释连成的一块每行地位相同,第一行也算,缩进四格的代码块才留得住。
|
||||||
|
- 块标签:说明(`Translator.doc` 读的:函数、类这些定义的说明,字段、成员的说明,库规则取的说明)的原文按 KDoc 拆成正文和块标签(`Doc`,解析在 `docTags.ts`,各卡片共用);单独成块的注释照原文显示,不拆。
|
||||||
|
- 认 `@param`、`@return`(`@returns` 同义)、`@throws`(`@exception` 同义)。一行(去掉开头空白)以标签开头算一条;后面接着的行(不是空行、不以 `@` 开头)续在这条后面(去掉开头空白、换行留着);空行结束这条,以 `@` 开头的行也结束上一条。围栏代码块里的行不认标签。
|
||||||
|
- `@param 名字 说明`;JSDoc 的 `@param {类型} 名字 - 说明`、`@param [名字] 说明`、`@param [名字=默认值] 说明` 也认(类型、横杠、默认值去掉)。`@return 说明`、`@returns {类型} 说明` 同样去掉类型。`@throws 异常 触发条件`:异常是一个名字、`[Foo]`、`[说法](路径#Foo)` 或 JSDoc 的 `{Foo}`,只是名字的写成不带路径的引用 `[Foo]`。几条 `@return` 用空行连成一段。
|
||||||
|
- 正文是拿掉这几种标签后剩下的行(标签前后的正文都留着),去掉首尾空行,连着的几个空行并成一个;只写了标签时没有正文。别的标签(`@see`、`@since`)留在正文里。
|
||||||
|
- 放到哪见「属性表」和各库规则:`@param` 对到参数表多出的「说明」列(按名字对,`*args` / `**kwargs` / `...rest` 去掉前缀),对不上的在说明下面列成「对不上的参数」;`@return` 跟在输出参数 / 返回那一行的值后面(`PropertyRow.note`);`@throws` 是「异常」一行。卡片共用的几行在 `rules/docRows.ts`。
|
||||||
|
- 中文名的兜底(`languages/typescriptNames.ts`、`pythonNames.ts` 从 `@param` / `Args:` 取参数的中文名)是另一回事,照旧读源码。
|
||||||
- 渲染:webview 用 markdown-it(`html: false`、`linkify: true`),CommonMark 加 GFM 的表格、删除线:段落、强调、行内代码(含双反引号)、代码块(围栏、缩进四格)、列表、链接、标题、引用、表格。段内单个换行是软换行,空行分段。同一段文字只渲染一次(缓存最近 500 段)。
|
- 渲染:webview 用 markdown-it(`html: false`、`linkify: true`),CommonMark 加 GFM 的表格、删除线:段落、强调、行内代码(含双反引号)、代码块(围栏、缩进四格)、列表、链接、标题、引用、表格。段内单个换行是软换行,空行分段。同一段文字只渲染一次(缓存最近 500 段)。
|
||||||
- 安全:源码里的 HTML 当文字显示;链接认 http / https / mailto,带协议的网址和邮箱自动成链接(`sample.py` 这种不带协议的不算),别的写法(`javascript:`、`command:`、`./`、`..`、绝对路径、盘符)照原文显示。http / https / mailto 由 VS Code 的 webview 自己拦下、交给宿主在外部打开,不用另发消息。http / https 图片不渲染(webview 的 CSP 不放行外部图片),`` 显示成 `!` 加链接。仓库内图片按原图比例显示,点了打开原图,没有就写「找不到图片」和路径。
|
- 安全:源码里的 HTML 当文字显示;链接认 http / https / mailto,带协议的网址和邮箱自动成链接(`sample.py` 这种不带协议的不算),别的写法(`javascript:`、`command:`、`./`、`..`、绝对路径、盘符)照原文显示。http / https / mailto 由 VS Code 的 webview 自己拦下、交给宿主在外部打开,不用另发消息。http / https 图片不渲染(webview 的 CSP 不放行外部图片),`` 显示成 `!` 加链接。仓库内图片按原图比例显示,点了打开原图,没有就写「找不到图片」和路径。
|
||||||
- 仓库路径链接(`[名字](从仓库根起的路径)`,可带 `#类名` 或 `#类名.成员`)能点,点了打开文件并跳到符号。指到目录的在代码阅读侧栏的树上定位(展开并选中,和侧栏跟着当前文件走是同一个定位;侧栏没露着时亮出来),不切到 VS Code 资源管理器;树上没有(开着筛选时没摆它)时提示「侧栏的树上没有 …」,不退出筛选。
|
- 仓库路径链接(`[名字](从仓库根起的路径)`,可带 `#类名` 或 `#类名.成员`)能点,点了打开文件并跳到符号。指到目录的在代码阅读侧栏的树上定位(展开并选中,和侧栏跟着当前文件走是同一个定位;侧栏没露着时亮出来),不切到 VS Code 资源管理器;树上没有(开着筛选时没摆它)时提示「侧栏的树上没有 …」,不退出筛选。
|
||||||
@@ -847,8 +853,9 @@ export type TokenRole =
|
|||||||
* color:这段自己的颜色(暗色、亮色主题各一个),盖过词性的颜色:库规则按库自己的配色上色(FastAPI 的 HTTP 方法用 Swagger UI 的颜色)。
|
* color:这段自己的颜色(暗色、亮色主题各一个),盖过词性的颜色:库规则按库自己的配色上色(FastAPI 的 HTTP 方法用 Swagger UI 的颜色)。
|
||||||
* refers:这段讲的是源码里别处的一个名字(那个名字的范围),语言扩展不认得这段自己的位置:写在字符串里的名字(FastAPI 路径里的参数名)。
|
* refers:这段讲的是源码里别处的一个名字(那个名字的范围),语言扩展不认得这段自己的位置:写在字符串里的名字(FastAPI 路径里的参数名)。
|
||||||
* 预览把这段当那个名字:⌘ / Ctrl + 点击跳到它的定义,单击高亮它的各处出现,它的某处出现盖住 refers 时这段也高亮;offset 照旧是照抄的位置。
|
* 预览把这段当那个名字:⌘ / Ctrl + 点击跳到它的定义,单击高亮它的各处出现,它的某处出现盖住 refers 时这段也高亮;offset 照旧是照抄的位置。
|
||||||
|
* tooltip:悬停这段时看的字(说明里写了、参数表里没有的参数名:「没有这个参数」)。
|
||||||
*/
|
*/
|
||||||
export interface Segment { text: string, role: TokenRole, offset?: number, reference?: RuleReference, color?: ThemeColor, refers?: SourceRange }
|
export interface Segment { text: string, role: TokenRole, offset?: number, reference?: RuleReference, color?: ThemeColor, refers?: SourceRange, tooltip?: string }
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 框的样式,决定框头图标和框的颜色:iteration 写在表达式里的遍历(渲染大纲的「对 … 的每一项」也是),图标、颜色同「循环」块;
|
* 框的样式,决定框头图标和框的颜色:iteration 写在表达式里的遍历(渲染大纲的「对 … 的每一项」也是),图标、颜色同「循环」块;
|
||||||
@@ -873,6 +880,18 @@ export type Inline = Segment | SegmentGroup
|
|||||||
*/
|
*/
|
||||||
export interface Markdown { markdown: string }
|
export interface Markdown { markdown: string }
|
||||||
|
|
||||||
|
/** 说明里讲的一个参数(`@param 名字 说明`):名字照写(`*args`、`...rest` 带着前缀),说明是 Markdown。 */
|
||||||
|
export interface DocParam { name: string, text: Markdown }
|
||||||
|
|
||||||
|
/** 说明里讲的一种异常(`@throws 异常 什么时候抛`):只写了名字的(`ValueError`、JSDoc 的 `{ValueError}`)写成 `[ValueError]`,`[Foo]`、`[说法](路径#Foo)` 照写。 */
|
||||||
|
export interface DocThrows { exception: Markdown, when: Markdown }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 一段说明(Translator.doc 读出来的):原文按 KDoc 拆成正文和块标签(规则见「注释的 Markdown 排版」的块标签)。
|
||||||
|
* 只写了标签、没有正文时没有 body。
|
||||||
|
*/
|
||||||
|
export interface Doc { body?: Markdown, params: DocParam[], returns?: Markdown, throws: DocThrows[] }
|
||||||
|
|
||||||
/** 预览里显示的一段文字:着色的词(一行译文、属性值),或整段 Markdown 说明(注释块、函数和类的说明)。 */
|
/** 预览里显示的一段文字:着色的词(一行译文、属性值),或整段 Markdown 说明(注释块、函数和类的说明)。 */
|
||||||
export type PreviewText = Inline[] | Markdown
|
export type PreviewText = Inline[] | Markdown
|
||||||
|
|
||||||
@@ -883,9 +902,12 @@ export type PreviewText = Inline[] | Markdown
|
|||||||
*/
|
*/
|
||||||
export interface Defines { defines?: SourceRange }
|
export interface Defines { defines?: SourceRange }
|
||||||
|
|
||||||
/** 属性表里展开的一个子行:几列对齐的格子;items 是展开在它下面的下一层子行(嵌套的字典 / 对象),一层层可以一直往下套。 */
|
/**
|
||||||
|
* 属性表里展开的一个子行:几列对齐的格子,格子是着色的词或一段 Markdown(参数的说明、异常的触发条件);
|
||||||
|
* items 是展开在它下面的下一层子行(嵌套的字典 / 对象),一层层可以一直往下套。
|
||||||
|
*/
|
||||||
export interface PropertyItem extends Defines {
|
export interface PropertyItem extends Defines {
|
||||||
cells: Inline[][]
|
cells: PreviewText[]
|
||||||
items?: PropertyItem[]
|
items?: PropertyItem[]
|
||||||
table?: PreviewTable // 排在最后一格里、文字下面的表:实参的值里跨几行写的调用的实参表,一层层往下套,总是展开
|
table?: PreviewTable // 排在最后一格里、文字下面的表:实参的值里跨几行写的调用的实参表,一层层往下套,总是展开
|
||||||
}
|
}
|
||||||
@@ -893,8 +915,9 @@ export interface PropertyItem extends Defines {
|
|||||||
/**
|
/**
|
||||||
* 属性表的一行:属性名、着色的值或 Markdown 说明(函数、类的「说明」)、展开在下面的子行(输入参数、导入项、字典 / 对象的各项)。
|
* 属性表的一行:属性名、着色的值或 Markdown 说明(函数、类的「说明」)、展开在下面的子行(输入参数、导入项、字典 / 对象的各项)。
|
||||||
* columns 是子行各列的列名(输入参数的名称 / 类型 / 默认值):给了的子行排成带表头的表格(只有一层),没给的排成列表。
|
* columns 是子行各列的列名(输入参数的名称 / 类型 / 默认值):给了的子行排成带表头的表格(只有一层),没给的排成列表。
|
||||||
|
* note 是跟在值后面的一段说明(Markdown,注释色):输出参数那一行,返回类型后面是说明里的 `@return`。
|
||||||
*/
|
*/
|
||||||
export interface PropertyRow extends Defines { label: string, value: PreviewText, items?: PropertyItem[], columns?: string[] }
|
export interface PropertyRow extends Defines { label: string, value: PreviewText, note?: Markdown, items?: PropertyItem[], columns?: string[] }
|
||||||
|
|
||||||
/** 一张带表头和边框的表:列名,每行几列对齐的格子,同 PropertyRow 给了 columns 的子行。 */
|
/** 一张带表头和边框的表:列名,每行几列对齐的格子,同 PropertyRow 给了 columns 的子行。 */
|
||||||
export interface PreviewTable { columns: string[], items: PropertyItem[] }
|
export interface PreviewTable { columns: string[], items: PropertyItem[] }
|
||||||
@@ -981,7 +1004,7 @@ export interface Translator {
|
|||||||
translate: (block: TextBlock, document: SourceDocument) => TextTranslation // 只翻 header 那一段;注释块给 Markdown 原文;跨几行写的调用带实参表
|
translate: (block: TextBlock, document: SourceDocument) => TextTranslation // 只翻 header 那一段;注释块给 Markdown 原文;跨几行写的调用带实参表
|
||||||
describe: (block: FactBlock, document: SourceDocument) => PropertyRow[] // 函数、类、枚举、异常、导入、常量、变量块 → 属性表
|
describe: (block: FactBlock, document: SourceDocument) => PropertyRow[] // 函数、类、枚举、异常、导入、常量、变量块 → 属性表
|
||||||
expression: (node: Node, document: SourceDocument) => Inline[] // 一个表达式 / 类型节点整个翻成一行译文,译法同属性表的值;库规则借它
|
expression: (node: Node, document: SourceDocument) => Inline[] // 一个表达式 / 类型节点整个翻成一行译文,译法同属性表的值;库规则借它
|
||||||
doc: (node: Node, document: SourceDocument) => Markdown | undefined // 说明文字(注释、docstring、当说明用的字符串)→ Markdown 原文,同属性表的「说明」
|
doc: (node: Node, document: SourceDocument) => Doc | undefined // 说明文字(注释、docstring、当说明用的字符串)→ 正文和块标签(Doc);正文同属性表的「说明」;连着几行的行文档注释(Rust 的 `///`)node 是第一条,翻译插件自己往下连
|
||||||
attributeNames?: AttributeNames // 筛选属性和值叫什么,和属性表的叫法一致;筛选栏、种类图标的悬停提示用它
|
attributeNames?: AttributeNames // 筛选属性和值叫什么,和属性表的叫法一致;筛选栏、种类图标的悬停提示用它
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1039,7 +1062,7 @@ export interface RuleContext {
|
|||||||
imports: ImportFacts[] // 这份源码里各处导入,照源码顺序:认别名用
|
imports: ImportFacts[] // 这份源码里各处导入,照源码顺序:认别名用
|
||||||
displayLanguage?: string // 当前翻译插件的 displayLanguage;没有翻译插件时没有
|
displayLanguage?: string // 当前翻译插件的 displayLanguage;没有翻译插件时没有
|
||||||
translate: (node: Node) => Inline[] // 借当前翻译插件的 expression;没有翻译插件时照抄源码
|
translate: (node: Node) => Inline[] // 借当前翻译插件的 expression;没有翻译插件时照抄源码
|
||||||
doc: (node: Node) => Markdown | undefined // 借当前翻译插件的 doc
|
doc: (node: Node) => Doc | undefined // 借当前翻译插件的 doc:正文和块标签,规则按自己的卡片把标签放到对应的行
|
||||||
lookup: (index: WorkspaceIndex, name: Node) => IndexEntry | undefined // name 指的定义在 index 里认出的那一项(见「规则认领时查别的文件里的定义」);还没查到是 undefined
|
lookup: (index: WorkspaceIndex, name: Node) => IndexEntry | undefined // name 指的定义在 index 里认出的那一项(见「规则认领时查别的文件里的定义」);还没查到是 undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1143,7 +1166,7 @@ export interface CommandRule {
|
|||||||
- 规则自己的卡片(`RuleCard`),下面几条都说的是它。
|
- 规则自己的卡片(`RuleCard`),下面几条都说的是它。
|
||||||
- 卡片必须给:`kind`(写成「规则 id.名字」,宿主核对前缀;筛选记住的就是它,要稳定)、`badge`(种类的名字、暗 / 亮两种颜色和 lucide 图标名)、`rows`(用现成的 `PropertyRow`:属性名 + 着色的值或 Markdown,表格就是带 `columns` 的子行)、`absorbs`(并进来的子块)。可以给:`defines`(块自己的名字,跳到定义是它时闪整块)、`attributes`(自己的筛选属性)、`concepts`(额外认出的概念,排在种类图标后面)、`slots`(挂在卡片某一行的子行上、点子行才展开的子块,见下);表格子行填 `PropertyItem.defines`(字段名),跳到这个定义时只闪那一行。
|
- 卡片必须给:`kind`(写成「规则 id.名字」,宿主核对前缀;筛选记住的就是它,要稳定)、`badge`(种类的名字、暗 / 亮两种颜色和 lucide 图标名)、`rows`(用现成的 `PropertyRow`:属性名 + 着色的值或 Markdown,表格就是带 `columns` 的子行)、`absorbs`(并进来的子块)。可以给:`defines`(块自己的名字,跳到定义是它时闪整块)、`attributes`(自己的筛选属性)、`concepts`(额外认出的概念,排在种类图标后面)、`slots`(挂在卡片某一行的子行上、点子行才展开的子块,见下);表格子行填 `PropertyItem.defines`(字段名),跳到这个定义时只闪那一行。
|
||||||
- 挂在卡片子行上的子块(`RuleSlot`):留下的子块(没并进来的)可以不排在卡片后面,挂在卡片第 `row` 行的第 `item` 个子行上(讲它的那一个子行);它默认收着,点那个子行的第一格才在这一行下面展开当子卡片、同一行里别的收起(Rust 实现块的方法表点方法名)。它照常转成块,照样问各规则,编辑、问 AI、差异、筛选都照常。一块只能放一处,不能同时并进来;`row` 要有这一行、`item` 要有这个子行。只有这一种放法:子块要么挂在讲它的子行上,要么照常排在卡片后面。
|
- 挂在卡片子行上的子块(`RuleSlot`):留下的子块(没并进来的)可以不排在卡片后面,挂在卡片第 `row` 行的第 `item` 个子行上(讲它的那一个子行);它默认收着,点那个子行的第一格才在这一行下面展开当子卡片、同一行里别的收起(Rust 实现块的方法表点方法名)。它照常转成块,照样问各规则,编辑、问 AI、差异、筛选都照常。一块只能放一处,不能同时并进来;`row` 要有这一行、`item` 要有这个子行。只有这一种放法:子块要么挂在讲它的子行上,要么照常排在卡片后面。
|
||||||
- 只经契约:规则拿到的是块(kind、node、header、children、facts)和 `RuleContext`;源码里的名字照抄时自己带下标(`Segment.offset`),表达式、类型交给 `context.translate`(当前翻译插件的 `expression`,名字带下标,⌘ + 点击、高亮各处出现照常),说明文字交给 `context.doc`(当前翻译插件的 `doc`,和别的「说明」一样按 Markdown 显示),别的文件里定义的名字交给 `context.lookup`(见「规则认领时查别的文件里的定义」);库自己的配色(HTTP 方法)写在段的 `color` 上。规则可以读自己那门语言的语法树(规则声明了 `languageIds`),但不 import 语言插件、翻译插件和宿主模块(`rules/contractOnly.test.ts` 检查 `rules/` 下非测试文件只 import `../contract` 和同目录的文件)。
|
- 只经契约:规则拿到的是块(kind、node、header、children、facts)和 `RuleContext`;源码里的名字照抄时自己带下标(`Segment.offset`),表达式、类型交给 `context.translate`(当前翻译插件的 `expression`,名字带下标,⌘ + 点击、高亮各处出现照常),说明文字交给 `context.doc`(当前翻译插件的 `doc`,给正文和块标签:正文和别的「说明」一样按 Markdown 显示,标签放到规则自己卡片对应的行,共用的几行在同目录的 `docRows.ts`),别的文件里定义的名字交给 `context.lookup`(见「规则认领时查别的文件里的定义」);库自己的配色(HTTP 方法)写在段的 `color` 上。规则可以读自己那门语言的语法树(规则声明了 `languageIds`),但不 import 语言插件、翻译插件和宿主模块(`rules/contractOnly.test.ts` 检查 `rules/` 下非测试文件只 import `../contract` 和同目录的文件)。
|
||||||
- 用词归规则:属性名、表头、种类名、类型译名都在规则自己的词表里,按 `context.displayLanguage`(翻译插件的 `displayLanguage`)挑,没有这种语言的词表就用规则默认的那份;加一种显示语言就是给规则加一份词表。webview 只画结构,不认识「数据表」这些字。
|
- 用词归规则:属性名、表头、种类名、类型译名都在规则自己的词表里,按 `context.displayLanguage`(翻译插件的 `displayLanguage`)挑,没有这种语言的词表就用规则默认的那份;加一种显示语言就是给规则加一份词表。webview 只画结构,不认识「数据表」这些字。
|
||||||
|
|
||||||
### 启用、认领、优先
|
### 启用、认领、优先
|
||||||
@@ -1376,7 +1399,7 @@ export interface CommandRule {
|
|||||||
|
|
||||||
- 字段:字段名(variable 色,带下标)。类型:注解照写法翻(翻译插件翻,`str` → 字符串),`InitVar[X]` 写 X。
|
- 字段:字段名(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` 带来的「只能按名字传参」。各项用 ` · ` 隔开。
|
- 默认值:`= 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 色。
|
- 说明:`field(doc="…")` 的字符串;没写时同枚举成员的说明取法(紧挨在上面的注释块、这一行行尾的注释、紧跟在后面的属性 docstring,原文用空格连起来),comment 色;都没有时是类说明里讲它的 `@param`(字段就是构造参数,Markdown)。类说明里 `@param` 的名字不是哪个字段时,在卡片的「说明」下面列成「对不上的参数」;`@throws` 是卡片最后的「异常」表。
|
||||||
|
|
||||||
样例(`fixtures/dataclassModels.py` 的 `SandboxAccess`,测试里逐字核对;说明的 Markdown 从略):
|
样例(`fixtures/dataclassModels.py` 的 `SandboxAccess`,测试里逐字核对;说明的 Markdown 从略):
|
||||||
|
|
||||||
@@ -1435,7 +1458,7 @@ export interface CommandRule {
|
|||||||
|
|
||||||
- 字段:字段名(variable 色,带下标)。类型:注解照写法翻(翻译插件翻,`str` → 字符串,`str \| None` → 字符串 \| 空);类型里的名字带源码下标,能点。
|
- 字段:字段名(variable 色,带下标)。类型:注解照写法翻(翻译插件翻,`str` → 字符串,`str \| None` → 字符串 \| 空);类型里的名字带源码下标,能点。
|
||||||
- 默认值:`= x` 写 x(`None` → 空);`Field(default=x)` 或第一个位置参数写 x(`Field(...)` 的 `...` 是必填,不算默认值);`Field(default_factory=f)` 写「每次新建 f()」,和数据类同一个说法。后面接 `Field(...)` 的约束,照写的顺序用 ` · ` 隔开:`min_length=1` 至少 1 个字符、`max_length=255` 最长 255、`ge=0` ≥ 0、`le` ≤、`gt` >、`lt` <、`multiple_of=3` 3 的倍数、`pattern` 匹配、`min_items` / `max_items` 至少 / 最多 N 项、`max_digits` 最多 N 位数字、`decimal_places` 小数 N 位。`default`、`default_factory`、`description` 不在这里重复。不认得的照写 `名=值`,不丢掉。
|
- 默认值:`= x` 写 x(`None` → 空);`Field(default=x)` 或第一个位置参数写 x(`Field(...)` 的 `...` 是必填,不算默认值);`Field(default_factory=f)` 写「每次新建 f()」,和数据类同一个说法。后面接 `Field(...)` 的约束,照写的顺序用 ` · ` 隔开:`min_length=1` 至少 1 个字符、`max_length=255` 最长 255、`ge=0` ≥ 0、`le` ≤、`gt` >、`lt` <、`multiple_of=3` 3 的倍数、`pattern` 匹配、`min_items` / `max_items` 至少 / 最多 N 项、`max_digits` 最多 N 位数字、`decimal_places` 小数 N 位。`default`、`default_factory`、`description` 不在这里重复。不认得的照写 `名=值`,不丢掉。
|
||||||
- 说明:`Field(description="…")` 的字符串优先(不是字符串就照写法翻);没有就用字段上方的注释、这一行行尾的注释、紧跟在后面的属性 docstring,原文用空格连起来,comment 色。
|
- 说明:`Field(description="…")` 的字符串优先(不是字符串就照写法翻);没有就用字段上方的注释、这一行行尾的注释、紧跟在后面的属性 docstring,原文用空格连起来,comment 色;都没有时是类说明里讲它的 `@param`(同数据类)。对不上的 `@param`、`@throws` 同数据类。
|
||||||
|
|
||||||
样例(`fixtures/aigcMetadata.py` 的 `AIGCMetadata`,从 luffy `model/aigc.py` 摘的,测试里逐字核对):
|
样例(`fixtures/aigcMetadata.py` 的 `AIGCMetadata`,从 luffy `model/aigc.py` 摘的,测试里逐字核对):
|
||||||
|
|
||||||
@@ -1504,14 +1527,16 @@ export interface CommandRule {
|
|||||||
| 行 | 内容 |
|
| 行 | 内容 |
|
||||||
| --- | --- |
|
| --- | --- |
|
||||||
| 名称 | 函数名(function 色,带下标) |
|
| 名称 | 函数名(function 色,带下标) |
|
||||||
| 说明 | 函数的 docstring,同函数块;Markdown |
|
| 说明 | 函数的 docstring 的正文,同函数块;Markdown |
|
||||||
|
| 对不上的参数 | 说明里 `@param` 的名字不是这个函数的参数时才有,同函数块(依赖、FastAPI 自己填的参数是函数的参数,不算对不上) |
|
||||||
| 路由器 | 挂在哪个路由对象上:它的名字(variable 色,带下标,⌘ + 点击跳到路由器卡片) |
|
| 路由器 | 挂在哪个路由对象上:它的名字(variable 色,带下标,⌘ + 点击跳到路由器卡片) |
|
||||||
| 标签 | 路由对象的 `tags=` 加装饰器的 `tags=`,字符串不带引号(string 色,带下标),「、」隔开 |
|
| 标签 | 路由对象的 `tags=` 加装饰器的 `tags=`,字符串不带引号(string 色,带下标),「、」隔开 |
|
||||||
| 异步 | 是 / 否(storage 色) |
|
| 异步 | 是 / 否(storage 色) |
|
||||||
| 装饰器 | 路由装饰器以外的装饰器(`@cache(60)`),有才显示 |
|
| 装饰器 | 路由装饰器以外的装饰器(`@cache(60)`),有才显示 |
|
||||||
| 依赖 | 路由对象、装饰器的 `dependencies=[Depends(x)]` 写 `x`,参数上的 `Depends(x)` / `Security(x)` 写 `x(参数名)`,参数名是 `_` 的只写 `x`;`Depends()` 不带参数时 x 是参数的类型;照这个顺序,「、」隔开。x 是一个名字时按 function 色、带下标,别的写法交给翻译插件翻 |
|
| 依赖 | 路由对象、装饰器的 `dependencies=[Depends(x)]` 写 `x`,参数上的 `Depends(x)` / `Security(x)` 写 `x(参数名)`,参数名是 `_` 的只写 `x`;`Depends()` 不带参数时 x 是参数的类型;照这个顺序,「、」隔开。x 是一个名字时按 function 色、带下标,别的写法交给翻译插件翻 |
|
||||||
| 请求 | 值是 `方法 URL`(hint),下面一张表格「项 \| 值」,一项一行,没有的项不列:URL、方法、路径参数、查询参数、请求头、Cookie、请求体 |
|
| 请求 | 值是 `方法 URL`(hint),下面一张表格「项 \| 值」,一项一行,没有的项不列:URL、方法、路径参数、查询参数、请求头、Cookie、请求体。说明里有请求参数对上了 `@param` 时多一列「说明」(Markdown),这几种参数改成一个参数一行、各带它的说明,URL、方法和没写说明的格子留空 |
|
||||||
| 响应 | 值是各状态码用 ` / ` 连起来(hint),下面一张表格「状态码 \| 内容 \| 说明」,一个状态码一行,按状态码从小到大,不是三位数字的(`"5XX"`)排在最后 |
|
| 响应 | 值是各状态码用 ` / ` 连起来(hint),下面一张表格「状态码 \| 内容 \| 说明」,一个状态码一行,按状态码从小到大,不是三位数字的(`"5XX"`)排在最后;说明里写了 `@return` 时跟在值后面 |
|
||||||
|
| 异常 | 说明里写了 `@throws` 才有,同函数块的「异常」表 |
|
||||||
|
|
||||||
请求表:
|
请求表:
|
||||||
|
|
||||||
@@ -1583,7 +1608,7 @@ TSX 和 Python 一样,从上往下一块语法一张卡片,嵌套的块排
|
|||||||
| `react.source` | 数据源 | `download` | #9cdcfe / #001080 | 别的钩子赋给变量(`useQuery`、`usePathname`、`useRouter`、自定义 `useXxx`) | 变量 / 钩子 / 说明 |
|
| `react.source` | 数据源 | `download` | #9cdcfe / #001080 | 别的钩子赋给变量(`useQuery`、`usePathname`、`useRouter`、自定义 `useXxx`) | 变量 / 钩子 / 说明 |
|
||||||
| `react.derived` | 派生值 | `sigma` | #9cdcfe / #001080 | `useMemo`、`useCallback` | 名称 / 计算表达式 / 依赖项 |
|
| `react.derived` | 派生值 | `sigma` | #9cdcfe / #001080 | `useMemo`、`useCallback` | 名称 / 计算表达式 / 依赖项 |
|
||||||
| `react.effect` | 副作用 | `zap` | #e5c07b / #9a6a00 | `useEffect`、`useLayoutEffect`、`useInsertionEffect` | 触发时机 / 清理函数 |
|
| `react.effect` | 副作用 | `zap` | #e5c07b / #9a6a00 | `useEffect`、`useLayoutEffect`、`useInsertionEffect` | 触发时机 / 清理函数 |
|
||||||
| `react.handler` | 事件处理函数 | `mouse-pointer-click` | #dcdcaa / #795e26 | 组件体里定义的函数(函数声明、值是函数的常量) | 名称 / 参数 / 绑定事件 |
|
| `react.handler` | 事件处理函数 | `mouse-pointer-click` | #dcdcaa / #795e26 | 组件体里定义的函数(函数声明、值是函数的常量) | 名称 / 说明 / 参数 / 绑定事件(说明里的块标签另有几行,见下) |
|
||||||
| `react.render` | 渲染输出 | `layout-template` | #89d185 / #388a34 | 组件函数自己返回 JSX 的 return | JSX 结构大纲 |
|
| `react.render` | 渲染输出 | `layout-template` | #89d185 / #388a34 | 组件函数自己返回 JSX 的 return | JSX 结构大纲 |
|
||||||
|
|
||||||
组件色同时用在渲染大纲里的组件、「客户端组件」标签上;状态、引用、数据源、派生值都是数据,共用变量块的浅蓝,靠图标区分;副作用同「选项」块的黄,事件处理函数同「函数」块,渲染输出同「返回」块的绿。筛选栏的「种类」按这几个 id 筛,叫上表的名字。
|
组件色同时用在渲染大纲里的组件、「客户端组件」标签上;状态、引用、数据源、派生值都是数据,共用变量块的浅蓝,靠图标区分;副作用同「选项」块的黄,事件处理函数同「函数」块,渲染输出同「返回」块的绿。筛选栏的「种类」按这几个 id 筛,叫上表的名字。
|
||||||
@@ -1603,8 +1628,14 @@ TSX 和 Python 一样,从上往下一块语法一张卡片,嵌套的块排
|
|||||||
| 行 | 内容 |
|
| 行 | 内容 |
|
||||||
| --- | --- |
|
| --- | --- |
|
||||||
| 名称 | 组件名(组件色,带下标);后面跟小标签(`tag` 框):「具名导出」「默认导出」(同 import / export 关键字的颜色;`export default X`、`export { X }` 写在文件别处也算)、「客户端组件」(组件色)、「记忆化」「转发 ref」(同 const 关键字的颜色,memo / forwardRef 包着) |
|
| 名称 | 组件名(组件色,带下标);后面跟小标签(`tag` 框):「具名导出」「默认导出」(同 import / export 关键字的颜色;`export default X`、`export { X }` 写在文件别处也算)、「客户端组件」(组件色)、「记忆化」「转发 ref」(同 const 关键字的颜色,memo / forwardRef 包着) |
|
||||||
| 说明 | 组件上面的 JSDoc,Markdown |
|
| 说明 | 组件上面的 JSDoc 的正文,Markdown(块标签拆开放,见下) |
|
||||||
| 构造参数 | `N 个`(hint);属性类型写的是一个名字(接口、类型别名、导入的类型)时后面写「 · 类型 XxxProps」,名字带下标(⌘ + 点击跳到类型)。下面一张表格「名称 \| 类型 \| 默认值 \| 必填」,有属性带说明时多一列「说明」。没写参数时写「无」 |
|
| 对不上的参数 | 说明里 `@param` 的名字不是哪个属性时才有,同函数块 |
|
||||||
|
| 构造参数 | `N 个`(hint);属性类型写的是一个名字(接口、类型别名、导入的类型)时后面写「 · 类型 XxxProps」,名字带下标(⌘ + 点击跳到类型)。下面一张表格「名称 \| 类型 \| 默认值 \| 必填」,有属性带说明时多一列「说明」。没写参数时写「无」。说明里讲整个属性参数的 `@param props` 跟在这一行的值后面 |
|
||||||
|
| 异常 | 说明里写了 `@throws` 才有,同函数块 |
|
||||||
|
|
||||||
|
组件说明里的 `@param`:属性名对上的进「说明」一格(字段自己有注释的照旧用注释,没注释时才用它,Markdown);写成 `props.title`(属性参数名.属性名)的也算,属性参数是解构写的时点号前面写的那个名字当它的名字。
|
||||||
|
|
||||||
|
事件处理函数卡片:名称 / 说明(函数上面 JSDoc 的正文)/ 对不上的参数 / 参数(逗号隔开写在一行;说明里有参数对上了 `@param` 时排成「名称 \| 类型 \| 默认值 \| 说明」表)/ 返回(写了 `@return` 才有,值空着、后面跟说明)/ 异常 / 绑定事件。
|
||||||
|
|
||||||
直接返回 JSX 的箭头函数组件(`const Tag = ({ text }) => <span>{text}</span>`)没有子块可放,它渲染的大纲作为第四行「JSX 结构大纲」写在组件卡片里。
|
直接返回 JSX 的箭头函数组件(`const Tag = ({ text }) => <span>{text}</span>`)没有子块可放,它渲染的大纲作为第四行「JSX 结构大纲」写在组件卡片里。
|
||||||
|
|
||||||
@@ -1683,11 +1714,11 @@ react.component 23
|
|||||||
|
|
||||||
文件导入了对应 crate 才启用(来源等于 crate 名,或以 `crate::` 开头)。tokio 不单独做卡片,异步已经写在函数上。样例 `fixtures/rustLibs.rs`。
|
文件导入了对应 crate 才启用(来源等于 crate 名,或以 `crate::` 开头)。tokio 不单独做卡片,异步已经写在函数上。样例 `fixtures/rustLibs.rs`。
|
||||||
|
|
||||||
- serde(`rules/serde.ts`,`libraries: ['serde']`):`derive` 了 `Serialize` 或 `Deserialize` 的结构体。种类 `serde.struct`,标签「可序列化」,图标 `braces`,颜色同结构体(暗 #4ec9b0 / 亮 #267f99)。名称后面再标一个「可序列化」。`rename_all = "camelCase"` 收成选项「字段名改驼峰」。字段表多一列「序列化」:`rename` 写「改名 xxx」,`skip` / `skip_serializing` 写「不输出」,没标是「—」。
|
- serde(`rules/serde.ts`,`libraries: ['serde']`):`derive` 了 `Serialize` 或 `Deserialize` 的结构体。种类 `serde.struct`,标签「可序列化」,图标 `braces`,颜色同结构体(暗 #4ec9b0 / 亮 #267f99)。名称后面再标一个「可序列化」。`rename_all = "camelCase"` 收成选项「字段名改驼峰」。字段表多一列「序列化」:`rename` 写「改名 xxx」,`skip` / `skip_serializing` 写「不输出」,没标是「—」。说明里的 `@param` 同结构体对到字段表的「说明」列。
|
||||||
- thiserror(`rules/thiserror.ts`,`libraries: ['thiserror']`):`derive` 了 `Error` 的枚举。种类 `thiserror.error`,标签「错误」,图标 `triangle-alert`,颜色同异常(暗 #f14c4c / 亮 #cd3131)。变体表「变体 | 何时 | 展示」:何时是变体上面的文档注释,展示是 `#[error("…")]` 的文案。变体上的 `#[from]` 或 `#[source]` 在展示后面标「透传来源」。
|
- thiserror(`rules/thiserror.ts`,`libraries: ['thiserror']`):`derive` 了 `Error` 的枚举。种类 `thiserror.error`,标签「错误」,图标 `triangle-alert`,颜色同异常(暗 #f14c4c / 亮 #cd3131)。变体表「变体 | 何时 | 展示」:何时是变体上面的文档注释,展示是 `#[error("…")]` 的文案。变体上的 `#[from]` 或 `#[source]` 在展示后面标「透传来源」。
|
||||||
- axum(`rules/axum.ts`,`libraries: ['axum']`):处理函数上的 `#[get("/path")]`、`#[axum::post(…)]` 等(get / post / put / delete / patch / head / options / trace),或顶层语句里 `Router` 的 `.route(路径, get(函数))`。函数已经标了路由属性时,这句 `route` 不再单独成卡。种类 `axum.route`,标签「接口」,图标 `globe`,颜色同 FastAPI 接口(暗 #61afef / 亮 #1a6fb5)。属性表:名称、说明、异步(是)、请求(一项「方法」、一项「路径」、路径里的 `{名字}` 对上参数写成「路径参数」,`Path(id): Path<Uuid>` 写 `id: Uuid`)、返回。
|
- axum(`rules/axum.ts`,`libraries: ['axum']`):处理函数上的 `#[get("/path")]`、`#[axum::post(…)]` 等(get / post / put / delete / patch / head / options / trace),或顶层语句里 `Router` 的 `.route(路径, get(函数))`。函数已经标了路由属性时,这句 `route` 不再单独成卡。种类 `axum.route`,标签「接口」,图标 `globe`,颜色同 FastAPI 接口(暗 #61afef / 亮 #1a6fb5)。属性表:名称、说明、异步(是)、请求(一项「方法」、一项「路径」、路径里的 `{名字}` 对上参数写成「路径参数」,`Path(id): Path<Uuid>` 写 `id: Uuid`)、参数(说明里写了 `@param` 才有:参数是 `Path(id)` 这类模式,不对名字,照写成「名称 \| 说明」表)、返回(`@return` 跟在后面)、异常(`@throws`)。
|
||||||
- sqlx(`rules/sqlx.ts`,`libraries: ['sqlx']`):`query!` / `query_as!` / `query_scalar!`。种类 `sqlx.query`,标签「查询」,图标 `database`,颜色暗 #ce9178 / 亮 #a31515。紧挨在前面的注释收成「说明」,不再单独成块。SQL 原文等宽,参数 `$1` 对上传入的表达式,`query_as!` / `query_scalar!` 的第一个实参是返回类型,`query!` 的返回类型是「—」。
|
- sqlx(`rules/sqlx.ts`,`libraries: ['sqlx']`):`query!` / `query_as!` / `query_scalar!`。种类 `sqlx.query`,标签「查询」,图标 `database`,颜色暗 #ce9178 / 亮 #a31515。紧挨在前面的注释收成「说明」,不再单独成块。SQL 原文等宽,参数 `$1` 对上传入的表达式,`query_as!` / `query_scalar!` 的第一个实参是返回类型,`query!` 的返回类型是「—」。
|
||||||
- tauri(`rules/tauri.ts`,`libraries: ['tauri']`):`#[tauri::command]` 的函数。种类 `tauri.command`,标签「桌面命令」,图标 `square-function`,颜色暗 #e5c07b / 亮 #9a6a00。名称后面再标「桌面命令」。参数表「名称 | 类型 | 来源」:`State<…>` 是「窗口状态」,其余是「调用方传入」。
|
- tauri(`rules/tauri.ts`,`libraries: ['tauri']`):`#[tauri::command]` 的函数。种类 `tauri.command`,标签「桌面命令」,图标 `square-function`,颜色暗 #e5c07b / 亮 #9a6a00。名称后面再标「桌面命令」。参数表「名称 | 类型 | 来源」:`State<…>` 是「窗口状态」,其余是「调用方传入」;说明里的 `@param` 对上时多一列「说明」,`@return` 跟在「返回」后面,`@throws` 成「异常」表。
|
||||||
|
|
||||||
## 命令规则
|
## 命令规则
|
||||||
|
|
||||||
@@ -1806,7 +1837,7 @@ shell 脚本大半是一条条命令,一条命令读成什么取决于命令
|
|||||||
|
|
||||||
语法包是 `@vscode/tree-sitter-wasm` 里的 `tree-sitter-rust.wasm`(和 python 的 wasm 同一层:包入口就在 `wasm/`)。`languageIds: ['rust']`。样例 `fixtures/rustLang.rs`、`fixtures/rustLibs.rs`、`fixtures/rustComments.rs`。原型是 `docs/prototype/code-reader/openpencil/exports/` 的 `rust-lang.png`、`rust-lib.png`、`rust-notes.png`。中文直译 `translators/rustZh.ts`:结构体不叫「类」,可见性不叫「访问级别」,`pub` 是「公开」。
|
语法包是 `@vscode/tree-sitter-wasm` 里的 `tree-sitter-rust.wasm`(和 python 的 wasm 同一层:包入口就在 `wasm/`)。`languageIds: ['rust']`。样例 `fixtures/rustLang.rs`、`fixtures/rustLibs.rs`、`fixtures/rustComments.rs`。原型是 `docs/prototype/code-reader/openpencil/exports/` 的 `rust-lang.png`、`rust-lib.png`、`rust-notes.png`。中文直译 `translators/rustZh.ts`:结构体不叫「类」,可见性不叫「访问级别」,`pub` 是「公开」。
|
||||||
|
|
||||||
可见性:`pub` 公开,没写是本模块,`pub(super)` 父模块,`pub(crate)` 和 `pub(in …)` 本 crate。公开写在 `facts.access`,其余写在块的 `access` 属性上(筛选值 `module` / `super` / `crate`)。说明是紧挨在前面的文档注释(`///`、`/**`)的第一句,句号留着;连着几行的 `///` 算一段,中间可以隔着 `#[…]`。
|
可见性:`pub` 公开,没写是本模块,`pub(super)` 父模块,`pub(crate)` 和 `pub(in …)` 本 crate。公开写在 `facts.access`,其余写在块的 `access` 属性上(筛选值 `module` / `super` / `crate`)。说明是紧挨在前面的文档注释(`///`、`/**`)正文的第一句,句号留着;连着几行的 `///` 算一段(`facts.doc` 是第一条,中文直译的 `docNodes` 往下连),中间可以隔着 `#[…]`。卡片(含 Rust 库规则的)都经翻译插件的 `doc`(规则经 `context.doc`)读说明,块标签拆开放:函数的 `@param` 进参数表的「说明」列、`@return` 跟在「返回」后面、`@throws` 成「异常」表,结构体的 `@param` 对到字段表的「说明」列,对不上的列成「对不上的参数」。
|
||||||
|
|
||||||
注释和别的语言走同一条路(`listBlocks`,Rust 的 `CommentRules` 在 `languages/rust.ts`),文件顶层、`mod` 身体、`impl` 身体都一样:
|
注释和别的语言走同一条路(`listBlocks`,Rust 的 `CommentRules` 在 `languages/rust.ts`),文件顶层、`mod` 身体、`impl` 身体都一样:
|
||||||
|
|
||||||
@@ -1942,7 +1973,7 @@ interface BlockViewBase {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 属性表块(有 facts)和库规则认领的块只带 properties,其余块只带 translation(注释块是 `{ markdown }`);没有翻译插件时是空数组。
|
* 属性表块(有 facts)和库规则认领的块只带 properties,其余块只带 translation(注释块是 `{ markdown }`);没有翻译插件时是空数组。
|
||||||
* 一行译文里有跨几行写的调用时 table 是摊开实参的表(TextTranslation.table),块带说明(TextBlock.doc)时 doc 是翻译插件给的 Markdown 原文(Translator.doc),
|
* 一行译文里有跨几行写的调用时 table 是摊开实参的表(TextTranslation.table),块带说明(TextBlock.doc)时 doc 是翻译插件读出的说明正文(Translator.doc 的 body),
|
||||||
* webview 依次排在译文下面。
|
* webview 依次排在译文下面。
|
||||||
*/
|
*/
|
||||||
type BlockView = BlockViewBase & ({ translation: PreviewText, table?: PreviewTable, doc?: Markdown } | { properties: PropertyRow[] })
|
type BlockView = BlockViewBase & ({ translation: PreviewText, table?: PreviewTable, doc?: Markdown } | { properties: PropertyRow[] })
|
||||||
@@ -2035,6 +2066,8 @@ vscode/src/codeReader/
|
|||||||
rules/pythonTree.ts Python 库规则共用的读语法树的办法(名字最后一节、字符串内容、调用的参数、别名、注解里的 X[...]、库里的名字)
|
rules/pythonTree.ts Python 库规则共用的读语法树的办法(名字最后一节、字符串内容、调用的参数、别名、注解里的 X[...]、库里的名字)
|
||||||
rules/segments.ts 库规则共用的拼属性表文字的办法
|
rules/segments.ts 库规则共用的拼属性表文字的办法
|
||||||
rules/children.ts 库规则共用的挑子块的办法(成员连同紧挨在它前面的注释块、成员的说明节点)
|
rules/children.ts 库规则共用的挑子块的办法(成员连同紧挨在它前面的注释块、成员的说明节点)
|
||||||
|
docTags.ts 说明的原文按 KDoc 拆成正文和块标签(Doc:@param / @return / @throws),翻译插件的 doc 用它(纯函数)
|
||||||
|
rules/docRows.ts 讲说明的几行,中文直译和库规则共用:正文、对不上的参数、参数按名字对说明(paramDocs)、输出后面的 @return、异常表
|
||||||
languages/shared.ts 语言插件共用的建树工具;异常类(withExceptions:按整份文件认异常类,类块换成异常块,并类属性、取构造参数)
|
languages/shared.ts 语言插件共用的建树工具;异常类(withExceptions:按整份文件认异常类,类块换成异常块,并类属性、取构造参数)
|
||||||
languages/typescript.ts (call:调用的结构,new 是新建;TYPESCRIPT_EXCEPTIONS:认异常类的父类名、类字段、constructor)
|
languages/typescript.ts (call:调用的结构,new 是新建;TYPESCRIPT_EXCEPTIONS:认异常类的父类名、类字段、constructor)
|
||||||
languages/python.ts (iteration:推导式的结构;call:调用的结构;PYTHON_EXCEPTIONS:自带的异常类、类属性、__init__)
|
languages/python.ts (iteration:推导式的结构;call:调用的结构;PYTHON_EXCEPTIONS:自带的异常类、类属性、__init__)
|
||||||
@@ -2125,7 +2158,8 @@ vscode/webview-ui/code-reader.html + src/codeReader/ 预览界面(React,Vit
|
|||||||
- 输入参数表格:函数的「输入参数」行带列名 名称 / 类型 / 默认值(TS、Python 一样),默认值格子只有译文、没有就是空格子,别的行没有列名;webview 按有没有列名选表格还是列表,表格的格子按列数取,列宽模板(`tableTemplate`)只看列数。
|
- 输入参数表格:函数的「输入参数」行带列名 名称 / 类型 / 默认值(TS、Python 一样),默认值格子只有译文、没有就是空格子,别的行没有列名;webview 按有没有列名选表格还是列表,表格的格子按列数取,列宽模板(`tableTemplate`)只看列数。
|
||||||
- 翻译:上面两张样例表逐字一致;属性名不被翻;每个被翻的词单独成段;节点模板嵌套时正确、槽位里照样翻、没命中模板的节点照抄;属性表里的值和一行译文用同一套表和模板。
|
- 翻译:上面两张样例表逐字一致;属性名不被翻;每个被翻的词单独成段;节点模板嵌套时正确、槽位里照样翻、没命中模板的节点照抄;属性表里的值和一行译文用同一套表和模板。
|
||||||
- `blockView`:`codeTokens` 拼起来等于 `code`;去缩进正确;函数、类、导入、常量、变量块只带属性表,其余块只带译文。
|
- `blockView`:`codeTokens` 拼起来等于 `code`;去缩进正确;函数、类、导入、常量、变量块只带属性表,其余块只带译文。
|
||||||
- 注释的 Markdown:注释块、函数和类「说明」的视图数据是 `{ markdown }`,原文照抄(reST 角色、双反引号、JSDoc 标签、HTML 都不转换),行注释连成的块留得住缩进四格的代码块;渲染时 HTML 当文字、带协议的网址和邮箱成链接、不带协议的文件名不成链接,`javascript:` / `command:` / `./` / `..` / 绝对路径的链接和 http 图片不渲染,仓库内图片按原图比例显示,点了打开原图,没有就写「找不到图片」,仓库路径链接能点、点了打开文件并跳到符号;跳到定义不认 Markdown 里的名字。
|
- 块标签:拆法(`docTags.test.ts`:多行续写、空行结束、`@returns` / `@exception` 同义、JSDoc 的 `{类型}`、`[名字=默认值]`、横杠、标签前后的正文、没有标签时原样、别的标签和围栏代码块里的行留在正文、异常名写成引用);卡片(`translators/zhTemplate.test.ts` 的「说明里的块标签」):`fixtures/conversationBrief.py`(luffy 的 `render_conversation_brief`)说明里没有 `@param` / `@return`,输入参数表「说明」列四个参数的原文,输出参数后面跟 `@return`;`fixtures/localRuntimeGate.py`(luffy 的 `gate.py`)「异常」表两行,第一行是完整链接、第二行是不带路径的引用;对不上的 `@param` 排在说明下面、提示色、悬停「没有这个参数」;类的 `@param` 照写成表、异常类的对到构造参数(`*args` 去掉星号对)。React 组件(字段没注释时用 `@param`、`props.x`、讲整个参数的跟在构造参数后面、对不上的、异常表)和事件处理函数(`rules/react.test.ts`)、FastAPI 请求表的说明列和响应后面的 `@return`(`rules/fastapi.test.ts`)、Rust 函数和结构体(`languages/rust.test.ts`)、数据类字段(`rules/dataclass.test.ts`)。
|
||||||
|
- 注释的 Markdown:注释块、函数和类「说明」的视图数据是 `{ markdown }`,原文照抄(reST 角色、双反引号、`{@link}`、HTML 都不转换,块标签拆出去),行注释连成的块留得住缩进四格的代码块;渲染时 HTML 当文字、带协议的网址和邮箱成链接、不带协议的文件名不成链接,`javascript:` / `command:` / `./` / `..` / 绝对路径的链接和 http 图片不渲染,仓库内图片按原图比例显示,点了打开原图,没有就写「找不到图片」,仓库路径链接能点、点了打开文件并跳到符号;跳到定义不认 Markdown 里的名字。
|
||||||
- 行替换:多行块替换后缩进正确。
|
- 行替换:多行块替换后缩进正确。
|
||||||
- 跳到定义:TS / Python 译文和属性表里的名字、点分模块名每一节、模块字符串带下标且指回源码原文,关键字、运算符、说明词不带(前面有中文注释也对得上);查到的定义 → 不动 / 原地 / 预览 / 普通编辑器 / 挑选框;webview 里按下标找目标块、要展开的祖先块和属性行,滚动位置。闪哪一处(TS、Python 源码走完整的语言插件 + 翻译插件 + 视图数据):参数 → 它那个子行,导入项 → 它那一项,多变量声明里的一个 → 它的名称行,类的泛型参数 → 「类型参数」那一行,函数名 / 类名 / 单个变量名(含解构模式)→ 整块,循环变量 → 所在的块,翻译插件没填 `defines` → 整块(照样展开属性行);翻译插件给参数、导入项、声明的变量、泛型参数标的范围指回源码原文;块自己的名字只有函数、类和单变量声明有。
|
- 跳到定义: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` 或又按键、点鼠标不守。
|
- 高亮各处出现:段的源码范围和出现重叠才亮、边界相接不算、没下标的不亮,讲别处名字的段(`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` 或又按键、点鼠标不守。
|
||||||
|
|||||||
@@ -296,7 +296,7 @@ export function toBlockViews(
|
|||||||
if (!layerTranslator)
|
if (!layerTranslator)
|
||||||
return { translation: [] }
|
return { translation: [] }
|
||||||
const { text, table } = layerTranslator.translate(block, layerDocument)
|
const { text, table } = layerTranslator.translate(block, layerDocument)
|
||||||
const doc = block.doc && layerTranslator.doc(block.doc, layerDocument)
|
const doc = block.doc && layerTranslator.doc(block.doc, layerDocument)?.body
|
||||||
return { translation: text, ...(table && { table }), ...(doc && { doc }) }
|
return { translation: text, ...(table && { table }), ...(doc && { doc }) }
|
||||||
}
|
}
|
||||||
/** 块执行的嵌着的代码分成的几步:只有一步时是它的子块,几步时是各步,交给那种语言转(见 TextBlock.embedded)。 */
|
/** 块执行的嵌着的代码分成的几步:只有一步时是它的子块,几步时是各步,交给那种语言转(见 TextBlock.embedded)。 */
|
||||||
|
|||||||
@@ -579,8 +579,9 @@ export type TokenRole
|
|||||||
* refers:这段讲的是源码里别处的一个名字(那个名字的范围),语言扩展不认得这段自己的位置:写在字符串里的名字
|
* refers:这段讲的是源码里别处的一个名字(那个名字的范围),语言扩展不认得这段自己的位置:写在字符串里的名字
|
||||||
* (FastAPI 路径 `"/{user_id}"` 里的 user_id 讲的是函数的参数 user_id)。预览把这段当那个名字:⌘ / Ctrl + 点击跳到它的定义,
|
* (FastAPI 路径 `"/{user_id}"` 里的 user_id 讲的是函数的参数 user_id)。预览把这段当那个名字:⌘ / Ctrl + 点击跳到它的定义,
|
||||||
* 单击高亮它的各处出现,那个名字的某处出现盖住 refers 时这段也跟着高亮。offset 照旧是这段照抄的位置。
|
* 单击高亮它的各处出现,那个名字的某处出现盖住 refers 时这段也跟着高亮。offset 照旧是这段照抄的位置。
|
||||||
|
* tooltip:悬停这段时看的字(说明里写了、参数表里没有的参数名:「没有这个参数」)。
|
||||||
*/
|
*/
|
||||||
export interface Segment { text: string, role: TokenRole, offset?: number, reference?: RuleReference, color?: ThemeColor, refers?: SourceRange }
|
export interface Segment { text: string, role: TokenRole, offset?: number, reference?: RuleReference, color?: ThemeColor, refers?: SourceRange, tooltip?: string }
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 框的样式,决定框头的图标和框的颜色:
|
* 框的样式,决定框头的图标和框的颜色:
|
||||||
@@ -612,10 +613,37 @@ export type Inline = Segment | SegmentGroup
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 一段 Markdown 说明文字:注释、JSDoc、docstring 去掉注释符号、引号和共同缩进后的原文,不做别的转换
|
* 一段 Markdown 说明文字:注释、JSDoc、docstring 去掉注释符号、引号和共同缩进后的原文,不做别的转换
|
||||||
* (reStructuredText 的 `:func:`、JSDoc 的 `@param` 都照抄)。预览按 Markdown 渲染成注释色的段落、列表、代码块,源码里的 HTML 当文字显示。
|
* (reStructuredText 的 `:func:` 照抄)。预览按 Markdown 渲染成注释色的段落、列表、代码块,源码里的 HTML 当文字显示;
|
||||||
|
* KDoc 写法的不带路径的引用(`[名字]`、`[类.成员]`)由预览向宿主问它指的定义,问到了能点。
|
||||||
*/
|
*/
|
||||||
export interface Markdown { markdown: string }
|
export interface Markdown { markdown: string }
|
||||||
|
|
||||||
|
/** 说明里讲的一个参数(`@param 名字 说明`):名字照写(`*args`、`...rest` 带着前缀),说明是 Markdown。 */
|
||||||
|
export interface DocParam { name: string, text: Markdown }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 说明里讲的一种异常(`@throws 异常 什么时候抛`)。异常写成 Markdown:只写了名字的(`ValueError`、JSDoc 的 `{ValueError}`)
|
||||||
|
* 写成不带路径的引用 `[ValueError]`,`[Foo]`、`[说法](路径#Foo)` 照写;触发条件是 Markdown。
|
||||||
|
*/
|
||||||
|
export interface DocThrows { exception: Markdown, when: Markdown }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 一段说明(函数、类这些定义的说明,注释,当说明用的字符串):原文(同 Markdown)按 KDoc 拆成正文和块标签。
|
||||||
|
* 一行以 `@param`、`@return`(`@returns` 同义)、`@throws`(`@exception` 同义)开头算一条标签,后面接着的行(不是空行、不以 `@` 开头)
|
||||||
|
* 续在这条后面,空行结束这条;JSDoc 的 `@param {类型} 名字 - 说明`、`@param [名字] 说明` 也认(类型、横杠去掉)。
|
||||||
|
* 别的标签(`@see`、`@since`)、围栏代码块里的行留在正文里。
|
||||||
|
*/
|
||||||
|
export interface Doc {
|
||||||
|
/** 摘要和正文:拿掉这几种标签以后的原文;只写了标签时没有。 */
|
||||||
|
body?: Markdown
|
||||||
|
/** 照写的顺序。 */
|
||||||
|
params: DocParam[]
|
||||||
|
/** `@return` 的说明;写了几条时连成一段。 */
|
||||||
|
returns?: Markdown
|
||||||
|
/** 照写的顺序。 */
|
||||||
|
throws: DocThrows[]
|
||||||
|
}
|
||||||
|
|
||||||
/** 预览里显示的一段文字:着色的词(一行译文、属性值),或整段 Markdown 说明(注释块、函数和类的说明)。 */
|
/** 预览里显示的一段文字:着色的词(一行译文、属性值),或整段 Markdown 说明(注释块、函数和类的说明)。 */
|
||||||
export type PreviewText = Inline[] | Markdown
|
export type PreviewText = Inline[] | Markdown
|
||||||
|
|
||||||
@@ -625,9 +653,12 @@ export type PreviewText = Inline[] | Markdown
|
|||||||
*/
|
*/
|
||||||
export interface Defines { defines?: SourceRange }
|
export interface Defines { defines?: SourceRange }
|
||||||
|
|
||||||
/** 属性表里展开的一个子行:几列对齐的格子,例如参数的名称 / 类型 / 默认值、字典一项的键 / 值。 */
|
/**
|
||||||
|
* 属性表里展开的一个子行:几列对齐的格子,例如参数的名称 / 类型 / 默认值、字典一项的键 / 值。
|
||||||
|
* 格子是着色的词,或一段 Markdown(参数的说明、异常的触发条件)。
|
||||||
|
*/
|
||||||
export interface PropertyItem extends Defines {
|
export interface PropertyItem extends Defines {
|
||||||
cells: Inline[][]
|
cells: PreviewText[]
|
||||||
/** 展开在这个子行下面的下一层子行(字典 / 对象的值又是字典 / 对象时,它的各项);一层层可以一直往下套。 */
|
/** 展开在这个子行下面的下一层子行(字典 / 对象的值又是字典 / 对象时,它的各项);一层层可以一直往下套。 */
|
||||||
items?: PropertyItem[]
|
items?: PropertyItem[]
|
||||||
/**
|
/**
|
||||||
@@ -644,6 +675,8 @@ export interface PropertyItem extends Defines {
|
|||||||
export interface PropertyRow extends Defines {
|
export interface PropertyRow extends Defines {
|
||||||
label: string
|
label: string
|
||||||
value: PreviewText
|
value: PreviewText
|
||||||
|
/** 跟在值后面的一段说明(Markdown,注释色):函数输出参数那一行,返回类型后面是说明里的 `@return`。 */
|
||||||
|
note?: Markdown
|
||||||
items?: PropertyItem[]
|
items?: PropertyItem[]
|
||||||
columns?: string[]
|
columns?: string[]
|
||||||
}
|
}
|
||||||
@@ -765,8 +798,11 @@ export interface Translator {
|
|||||||
describe: (block: FactBlock, document: SourceDocument) => PropertyRow[]
|
describe: (block: FactBlock, document: SourceDocument) => PropertyRow[]
|
||||||
/** 一个表达式或类型节点整个翻成一行译文,译法同属性表里的值:库规则翻默认值、类型时借它。 */
|
/** 一个表达式或类型节点整个翻成一行译文,译法同属性表里的值:库规则翻默认值、类型时借它。 */
|
||||||
expression: (node: Node, document: SourceDocument) => Inline[]
|
expression: (node: Node, document: SourceDocument) => Inline[]
|
||||||
/** 一段说明文字(注释、docstring、当说明用的字符串)去掉注释符号、引号和共同缩进后的 Markdown 原文,同属性表的「说明」;没有文字时没有。 */
|
/**
|
||||||
doc: (node: Node, document: SourceDocument) => Markdown | undefined
|
* 一段说明文字(注释、docstring、当说明用的字符串)读成说明(见 Doc):去掉注释符号、引号和共同缩进后的 Markdown 原文,
|
||||||
|
* 拆出块标签;正文同属性表的「说明」。几条行文档注释连成一段的(Rust 的 `///`),node 是第一条,翻译插件自己往下连。没有文字时没有。
|
||||||
|
*/
|
||||||
|
doc: (node: Node, document: SourceDocument) => Doc | undefined
|
||||||
/** 筛选属性和值叫什么,和属性表里的属性名、值用同一套叫法;不给就都显示 id。 */
|
/** 筛选属性和值叫什么,和属性表里的属性名、值用同一套叫法;不给就都显示 id。 */
|
||||||
attributeNames?: AttributeNames
|
attributeNames?: AttributeNames
|
||||||
}
|
}
|
||||||
@@ -957,8 +993,8 @@ export interface RuleContext {
|
|||||||
displayLanguage?: string
|
displayLanguage?: string
|
||||||
/** 用当前翻译插件把一个表达式或类型节点翻成一行译文(Translator.expression):名字带源码下标,能跳到定义、高亮各处出现;没有翻译插件时照抄源码。 */
|
/** 用当前翻译插件把一个表达式或类型节点翻成一行译文(Translator.expression):名字带源码下标,能跳到定义、高亮各处出现;没有翻译插件时照抄源码。 */
|
||||||
translate: (node: Node) => Inline[]
|
translate: (node: Node) => Inline[]
|
||||||
/** 用当前翻译插件取一段说明文字(Translator.doc);没有翻译插件、没有文字时没有。 */
|
/** 用当前翻译插件读一段说明文字(Translator.doc):正文和块标签;没有翻译插件、没有文字时没有。 */
|
||||||
doc: (node: Node) => Markdown | undefined
|
doc: (node: Node) => Doc | undefined
|
||||||
/**
|
/**
|
||||||
* 源码里 name 这个名字(例如 raise 后面的异常类名)指的定义在 index 里认出的那一项,规则从定义处读出的值在 IndexEntry.value 里:
|
* 源码里 name 这个名字(例如 raise 后面的异常类名)指的定义在 index 里认出的那一项,规则从定义处读出的值在 IndexEntry.value 里:
|
||||||
* 定义由语言扩展查(同 ⌘ + 点击),落在 index 认出的某一项的名字上才算;同一份源码里同名的名字按一处查。
|
* 定义由语言扩展查(同 ⌘ + 点击),落在 index 认出的某一项的名字上才算;同一份源码里同名的名字按一处查。
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ export interface ItemDiff {
|
|||||||
/** 整行是新加的(它下面各层也都是) */
|
/** 整行是新加的(它下面各层也都是) */
|
||||||
added?: true
|
added?: true
|
||||||
/** 按列:这一格改成了什么样 */
|
/** 按列:这一格改成了什么样 */
|
||||||
cells?: Record<number, InlinePart[]>
|
cells?: Record<number, TextDiff>
|
||||||
items?: ItemsDiff
|
items?: ItemsDiff
|
||||||
table?: ItemsDiff
|
table?: ItemsDiff
|
||||||
}
|
}
|
||||||
@@ -49,6 +49,8 @@ export interface RowDiff {
|
|||||||
/** 整行是新加的(它下面的子行也都是) */
|
/** 整行是新加的(它下面的子行也都是) */
|
||||||
added?: true
|
added?: true
|
||||||
value?: TextDiff
|
value?: TextDiff
|
||||||
|
/** 跟在值后面的说明(PropertyRow.note) */
|
||||||
|
note?: TextDiff
|
||||||
items?: ItemsDiff
|
items?: ItemsDiff
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -79,7 +81,7 @@ function textWithoutSource(text: PreviewText): PreviewText {
|
|||||||
|
|
||||||
function itemWithoutSource(item: PropertyItem): PropertyItem {
|
function itemWithoutSource(item: PropertyItem): PropertyItem {
|
||||||
return {
|
return {
|
||||||
cells: item.cells.map(withoutSource),
|
cells: item.cells.map(textWithoutSource),
|
||||||
...(item.items && { items: item.items.map(itemWithoutSource) }),
|
...(item.items && { items: item.items.map(itemWithoutSource) }),
|
||||||
...(item.table && { table: tableWithoutSource(item.table) }),
|
...(item.table && { table: tableWithoutSource(item.table) }),
|
||||||
}
|
}
|
||||||
@@ -93,6 +95,7 @@ function rowWithoutSource(row: PropertyRow): PropertyRow {
|
|||||||
return {
|
return {
|
||||||
label: row.label,
|
label: row.label,
|
||||||
value: textWithoutSource(row.value),
|
value: textWithoutSource(row.value),
|
||||||
|
...(row.note && { note: row.note }),
|
||||||
...(row.items && { items: row.items.map(itemWithoutSource) }),
|
...(row.items && { items: row.items.map(itemWithoutSource) }),
|
||||||
...(row.columns && { columns: row.columns }),
|
...(row.columns && { columns: row.columns }),
|
||||||
}
|
}
|
||||||
@@ -175,14 +178,18 @@ export function markdownDiff(before: string, after: string): MarkdownPart[] | un
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 一段预览文字的字:着色的词拼起来,Markdown 取原文。 */
|
||||||
|
function plainText(text: PreviewText): string {
|
||||||
|
return 'markdown' in text ? text.markdown : inlineText(text)
|
||||||
|
}
|
||||||
|
|
||||||
/** 一段预览文字前后两版比:都是着色的词逐截比,都是 Markdown 逐词比;一种换成了另一种时按字逐词比。一样时是 undefined。 */
|
/** 一段预览文字前后两版比:都是着色的词逐截比,都是 Markdown 逐词比;一种换成了另一种时按字逐词比。一样时是 undefined。 */
|
||||||
export function textDiff(before: PreviewText, after: PreviewText): TextDiff | undefined {
|
export function textDiff(before: PreviewText, after: PreviewText): TextDiff | undefined {
|
||||||
if (!('markdown' in before) && !('markdown' in after)) {
|
if (!('markdown' in before) && !('markdown' in after)) {
|
||||||
const inline = inlineDiff(before, after)
|
const inline = inlineDiff(before, after)
|
||||||
return inline && { inline }
|
return inline && { inline }
|
||||||
}
|
}
|
||||||
const plain = (text: PreviewText): string => 'markdown' in text ? text.markdown : inlineText(text)
|
const markdown = markdownDiff(plainText(before), plainText(after))
|
||||||
const markdown = markdownDiff(plain(before), plain(after))
|
|
||||||
return markdown && { markdown }
|
return markdown && { markdown }
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -232,13 +239,13 @@ function addedItem(item: PropertyItem): ItemDiff {
|
|||||||
return { added: true, ...(items && { items }), ...(table && { table }) }
|
return { added: true, ...(items && { items }), ...(table && { table }) }
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 配上的两个子行比:各格逐截比,下一层、套着的表逐行比。一样时是 undefined。 */
|
/** 配上的两个子行比:各格逐截 / 逐词比,下一层、套着的表逐行比。一样时是 undefined。 */
|
||||||
function itemDiff(before: PropertyItem, after: PropertyItem): ItemDiff | undefined {
|
function itemDiff(before: PropertyItem, after: PropertyItem): ItemDiff | undefined {
|
||||||
const cells: Record<number, InlinePart[]> = {}
|
const cells: Record<number, TextDiff> = {}
|
||||||
after.cells.forEach((cell, index) => {
|
after.cells.forEach((cell, index) => {
|
||||||
const parts = inlineDiff(before.cells[index] ?? [], cell)
|
const diff = textDiff(before.cells[index] ?? [], cell)
|
||||||
if (parts)
|
if (diff)
|
||||||
cells[index] = parts
|
cells[index] = diff
|
||||||
})
|
})
|
||||||
const items = itemsDiff(before.items, after.items)
|
const items = itemsDiff(before.items, after.items)
|
||||||
const table = itemsDiff(before.table?.items, after.table?.items)
|
const table = itemsDiff(before.table?.items, after.table?.items)
|
||||||
@@ -257,7 +264,7 @@ function itemDiff(before: PropertyItem, after: PropertyItem): ItemDiff | undefin
|
|||||||
export function itemsDiff(before: readonly PropertyItem[] | undefined, after: readonly PropertyItem[] | undefined): ItemsDiff | undefined {
|
export function itemsDiff(before: readonly PropertyItem[] | undefined, after: readonly PropertyItem[] | undefined): ItemsDiff | undefined {
|
||||||
const old = before ?? []
|
const old = before ?? []
|
||||||
const now = after ?? []
|
const now = after ?? []
|
||||||
const { toOld, toNew } = pairByKey(old, now, item => inlineText(item.cells[0] ?? []))
|
const { toOld, toNew } = pairByKey(old, now, item => plainText(item.cells[0] ?? []))
|
||||||
const changed: Record<number, ItemDiff> = {}
|
const changed: Record<number, ItemDiff> = {}
|
||||||
now.forEach((item, index) => {
|
now.forEach((item, index) => {
|
||||||
const paired = toOld[index]
|
const paired = toOld[index]
|
||||||
@@ -270,15 +277,15 @@ export function itemsDiff(before: readonly PropertyItem[] | undefined, after: re
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 配上的两行比:下面的子行逐行比,值逐截 / 逐词比。子行改了时只标子行:带子行的行,值是子行的汇总(「3 个」「字典 2 项」),
|
* 配上的两行比:下面的子行逐行比,值逐截 / 逐词比,跟在值后面的说明逐词比。子行改了时不标值:带子行的行,值是子行的汇总
|
||||||
* 跟着子行变,不另标。一样时是 undefined。
|
* (「3 个」「字典 2 项」),跟着子行变,不另标。一样时是 undefined。
|
||||||
*/
|
*/
|
||||||
function rowDiff(before: PropertyRow, after: PropertyRow): RowDiff | undefined {
|
function rowDiff(before: PropertyRow, after: PropertyRow): RowDiff | undefined {
|
||||||
const items = itemsDiff(before.items, after.items)
|
const items = itemsDiff(before.items, after.items)
|
||||||
if (items)
|
const note = textDiff(before.note ?? { markdown: '' }, after.note ?? { markdown: '' })
|
||||||
return { items }
|
const value = items ? undefined : textDiff(before.value, after.value)
|
||||||
const value = textDiff(before.value, after.value)
|
const diff: RowDiff = { ...(items && { items }), ...(value && { value }), ...(note && { note }) }
|
||||||
return value && { value }
|
return Object.keys(diff).length > 0 ? diff : undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 属性表前后两版比:按属性名配对,配上的比值和子行,多出来的行算新加的,没了的行算删掉的、排回原来的位置。一样时是 undefined。 */
|
/** 属性表前后两版比:按属性名配对,配上的比值和子行,多出来的行算新加的,没了的行算删掉的、排回原来的位置。一样时是 undefined。 */
|
||||||
|
|||||||
@@ -0,0 +1,87 @@
|
|||||||
|
import { describe, expect, it } from 'vitest'
|
||||||
|
import { readDoc } from './docTags'
|
||||||
|
|
||||||
|
describe('说明拆成正文和块标签(KDoc)', () => {
|
||||||
|
it('没有标签时原样是正文', () => {
|
||||||
|
expect(readDoc('第一句。\n\n第二段,`code` 和 [Name]。')).toEqual({
|
||||||
|
body: { markdown: '第一句。\n\n第二段,`code` 和 [Name]。' },
|
||||||
|
params: [],
|
||||||
|
throws: [],
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('一行以标签开头算一条,后面接着的行续上,空行结束这条;标签前后的正文都留着', () => {
|
||||||
|
const doc = readDoc([
|
||||||
|
'摘要。',
|
||||||
|
'',
|
||||||
|
'@param messages 对话历史',
|
||||||
|
' 接着写的一行',
|
||||||
|
'@param summary 摘要',
|
||||||
|
'',
|
||||||
|
'标签后面的正文。',
|
||||||
|
'@return 转写',
|
||||||
|
].join('\n'))
|
||||||
|
expect(doc.body).toEqual({ markdown: '摘要。\n\n标签后面的正文。' })
|
||||||
|
expect(doc.params).toEqual([
|
||||||
|
{ name: 'messages', text: { markdown: '对话历史\n接着写的一行' } },
|
||||||
|
{ name: 'summary', text: { markdown: '摘要' } },
|
||||||
|
])
|
||||||
|
expect(doc.returns).toEqual({ markdown: '转写' })
|
||||||
|
})
|
||||||
|
|
||||||
|
it('`@returns` 同 `@return`,`@exception` 同 `@throws`', () => {
|
||||||
|
const doc = readDoc('@returns 结果\n@exception ValueError 参数不对')
|
||||||
|
expect(doc.body).toBeUndefined()
|
||||||
|
expect(doc.returns).toEqual({ markdown: '结果' })
|
||||||
|
expect(doc.throws).toEqual([{ exception: { markdown: '[ValueError]' }, when: { markdown: '参数不对' } }])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('认 JSDoc 写法:`{类型}` 和名字后面的横杠去掉,`[名字=默认值]` 取名字', () => {
|
||||||
|
const doc = readDoc([
|
||||||
|
'@param {string} path - 文件路径',
|
||||||
|
'@param {{ a: number }} [options={}] 选项',
|
||||||
|
'@param [retries] - 重试次数',
|
||||||
|
'@returns {Promise<Config>} 配置',
|
||||||
|
'@throws {TypeError} 路径不是字符串',
|
||||||
|
].join('\n'))
|
||||||
|
expect(doc.params).toEqual([
|
||||||
|
{ name: 'path', text: { markdown: '文件路径' } },
|
||||||
|
{ name: 'options', text: { markdown: '选项' } },
|
||||||
|
{ name: 'retries', text: { markdown: '重试次数' } },
|
||||||
|
])
|
||||||
|
expect(doc.returns).toEqual({ markdown: '配置' })
|
||||||
|
expect(doc.throws).toEqual([{ exception: { markdown: '[TypeError]' }, when: { markdown: '路径不是字符串' } }])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('异常写成引用:单写名字的加方括号,`[Foo]`、完整链接照写', () => {
|
||||||
|
const doc = readDoc([
|
||||||
|
'@throws [DesktopOnly](backend/src/visibility.py#DesktopOnly) 请求不是桌面端发来的',
|
||||||
|
'@throws [LocalRuntimeDisabled] 总开关关着',
|
||||||
|
'@throws errors.NotFound 没有这个人',
|
||||||
|
].join('\n'))
|
||||||
|
expect(doc.throws).toEqual([
|
||||||
|
{ exception: { markdown: '[DesktopOnly](backend/src/visibility.py#DesktopOnly)' }, when: { markdown: '请求不是桌面端发来的' } },
|
||||||
|
{ exception: { markdown: '[LocalRuntimeDisabled]' }, when: { markdown: '总开关关着' } },
|
||||||
|
{ exception: { markdown: '[errors.NotFound]' }, when: { markdown: '没有这个人' } },
|
||||||
|
])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('别的标签、围栏代码块里的行留在正文;以 @ 开头的行结束上一条标签', () => {
|
||||||
|
const doc = readDoc([
|
||||||
|
'正文。',
|
||||||
|
'@param a 甲',
|
||||||
|
'@see other',
|
||||||
|
'```',
|
||||||
|
'@param inside 代码里的',
|
||||||
|
'```',
|
||||||
|
].join('\n'))
|
||||||
|
expect(doc.params).toEqual([{ name: 'a', text: { markdown: '甲' } }])
|
||||||
|
expect(doc.body).toEqual({ markdown: '正文。\n@see other\n```\n@param inside 代码里的\n```' })
|
||||||
|
})
|
||||||
|
|
||||||
|
it('`*args`、`**kwargs` 照写名字;几条 `@return` 连成一段', () => {
|
||||||
|
const doc = readDoc('@param *args 位置参数\n@param **kwargs 关键字参数\n@return 一\n@return 二')
|
||||||
|
expect(doc.params.map(param => param.name)).toEqual(['*args', '**kwargs'])
|
||||||
|
expect(doc.returns).toEqual({ markdown: '一\n\n二' })
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -0,0 +1,137 @@
|
|||||||
|
import type { Doc, DocParam, DocThrows, Markdown } from './contract'
|
||||||
|
|
||||||
|
type TagKind = 'param' | 'return' | 'throws'
|
||||||
|
|
||||||
|
/** 认的块标签:`@returns` 同 `@return`,`@exception` 同 `@throws`。 */
|
||||||
|
const TAGS: Record<string, TagKind> = { param: 'param', return: 'return', returns: 'return', throws: 'throws', exception: 'throws' }
|
||||||
|
|
||||||
|
/** 一行以块标签开头:标签名,和它后面的文字。 */
|
||||||
|
const TAG_LINE = /^\s*@(\w+)(?:\s(.*))?$/
|
||||||
|
|
||||||
|
/** 围栏代码块的开头(三个以上的反引号或波浪线,前面最多三个空格):里面的行照原样留在正文里。 */
|
||||||
|
const FENCE_OPEN = /^\s{0,3}(`{3,}|~{3,})/
|
||||||
|
|
||||||
|
/** 一个名字或点分的几个名字(`ValueError`、`errors.NotFound`):写成不带路径的引用。 */
|
||||||
|
const NAME_PATH = /^[\p{L}_$][\p{L}\p{N}_$]*(?:\.[\p{L}_$][\p{L}\p{N}_$]*)*$/u
|
||||||
|
|
||||||
|
interface Tag {
|
||||||
|
kind: TagKind
|
||||||
|
/** 标签名后面那一行的文字,和续在后面的各行 */
|
||||||
|
lines: string[]
|
||||||
|
}
|
||||||
|
|
||||||
|
function blank(line: string): boolean {
|
||||||
|
return line.trim() === ''
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 围栏代码块到这一行结束:和开头同一种字符、不少于开头的个数,后面只有空白。 */
|
||||||
|
function closesFence(line: string, fence: string): boolean {
|
||||||
|
const match = /^\s{0,3}(`{3,}|~{3,})\s*$/.exec(line)
|
||||||
|
return !!match && match[1][0] === fence[0] && match[1].length >= fence.length
|
||||||
|
}
|
||||||
|
|
||||||
|
/** text 开头 open 那个括号到配对的 close 为止(含两头),不配对时没有。 */
|
||||||
|
function bracketed(text: string, open: string, close: string): string | undefined {
|
||||||
|
if (text[0] !== open)
|
||||||
|
return undefined
|
||||||
|
let depth = 0
|
||||||
|
for (let i = 0; i < text.length; i++) {
|
||||||
|
if (text[i] === open)
|
||||||
|
depth++
|
||||||
|
else if (text[i] === close && --depth === 0)
|
||||||
|
return text.slice(0, i + 1)
|
||||||
|
}
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
/** JSDoc 写在开头的 `{类型}`:拿掉它,交出类型和剩下的。 */
|
||||||
|
function takeType(text: string): { type?: string, rest: string } {
|
||||||
|
const type = bracketed(text, '{', '}')
|
||||||
|
return type === undefined ? { rest: text } : { type: type.slice(1, -1).trim(), rest: text.slice(type.length).trimStart() }
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 名字后面 JSDoc 写的 `- `:拿掉。 */
|
||||||
|
function withoutDash(text: string): string {
|
||||||
|
return text.replace(/^-(?:\s+|$)/, '')
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 标签的说明:名字后面剩下的那一段接上续写的各行,空的不要。 */
|
||||||
|
function joinText(first: string, more: readonly string[]): Markdown {
|
||||||
|
return { markdown: [first, ...more].filter(line => line !== '').join('\n') }
|
||||||
|
}
|
||||||
|
|
||||||
|
/** `@param 名字 说明`、`@param {类型} 名字 - 说明`、`@param [名字=默认值] 说明`。 */
|
||||||
|
function paramOf([first, ...more]: string[]): DocParam | undefined {
|
||||||
|
const { rest } = takeType(first)
|
||||||
|
const optional = bracketed(rest, '[', ']')
|
||||||
|
const name = optional === undefined ? /^\S*/.exec(rest)![0] : optional.slice(1, -1).split('=')[0].trim()
|
||||||
|
const text = joinText(withoutDash(rest.slice(optional?.length ?? name.length).trimStart()), more)
|
||||||
|
return name === '' && text.markdown === '' ? undefined : { name, text }
|
||||||
|
}
|
||||||
|
|
||||||
|
/** `@return 说明`、`@returns {类型} 说明`。 */
|
||||||
|
function returnOf([first, ...more]: string[]): Markdown {
|
||||||
|
return joinText(withoutDash(takeType(first).rest), more)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 异常名写成 Markdown:只是名字的写成不带路径的引用 `[名字]`,别的(`[Foo]`、`[说法](路径#Foo)`、反引号)照写。 */
|
||||||
|
function exceptionMarkdown(text: string): Markdown {
|
||||||
|
return { markdown: NAME_PATH.test(text) ? `[${text}]` : text }
|
||||||
|
}
|
||||||
|
|
||||||
|
/** `@throws 异常 什么时候抛`:异常是一个名字、`[Foo]`、`[说法](路径#Foo)`,JSDoc 是 `{Foo}`。 */
|
||||||
|
function throwsOf([first, ...more]: string[]): DocThrows | undefined {
|
||||||
|
const { type, rest } = takeType(first)
|
||||||
|
let exception = type
|
||||||
|
let after = rest
|
||||||
|
if (exception === undefined) {
|
||||||
|
const label = bracketed(rest, '[', ']')
|
||||||
|
const target = label === undefined ? undefined : bracketed(rest.slice(label.length), '(', ')')
|
||||||
|
exception = label === undefined ? /^\S*/.exec(rest)![0] : label + (target ?? '')
|
||||||
|
after = rest.slice(exception.length).trimStart()
|
||||||
|
}
|
||||||
|
const when = joinText(withoutDash(after), more)
|
||||||
|
return exception === '' && when.markdown === '' ? undefined : { exception: exceptionMarkdown(exception), when }
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 说明的 Markdown 原文按 KDoc 拆成正文和块标签(规则见 contract 的 Doc)。正文去掉首尾空行,标签拿掉后连着的几个空行并成一个。
|
||||||
|
*/
|
||||||
|
export function readDoc(markdown: string): Doc {
|
||||||
|
const body: string[] = []
|
||||||
|
const tags: Tag[] = []
|
||||||
|
let current: Tag | undefined
|
||||||
|
let fence: string | undefined
|
||||||
|
for (const line of markdown.split('\n')) {
|
||||||
|
if (fence !== undefined) {
|
||||||
|
body.push(line)
|
||||||
|
if (closesFence(line, fence))
|
||||||
|
fence = undefined
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
const tag = TAG_LINE.exec(line)
|
||||||
|
const kind = tag && TAGS[tag[1]]
|
||||||
|
if (kind) {
|
||||||
|
current = { kind, lines: [(tag[2] ?? '').trim()] }
|
||||||
|
tags.push(current)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if (current && !blank(line) && !line.trimStart().startsWith('@')) {
|
||||||
|
current.lines.push(line.trim())
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
current = undefined
|
||||||
|
fence = FENCE_OPEN.exec(line)?.[1]
|
||||||
|
if (!(blank(line) && (body.length === 0 || blank(body.at(-1)!))))
|
||||||
|
body.push(line)
|
||||||
|
}
|
||||||
|
while (body.length > 0 && blank(body.at(-1)!))
|
||||||
|
body.pop()
|
||||||
|
const returns = tags.filter(tag => tag.kind === 'return').map(tag => returnOf(tag.lines)).filter(text => text.markdown !== '')
|
||||||
|
return {
|
||||||
|
...(body.length > 0 && { body: { markdown: body.join('\n') } }),
|
||||||
|
params: tags.flatMap(tag => tag.kind === 'param' ? paramOf(tag.lines) ?? [] : []),
|
||||||
|
...(returns.length > 0 && { returns: { markdown: returns.map(text => text.markdown).join('\n\n') } }),
|
||||||
|
throws: tags.flatMap(tag => tag.kind === 'throws' ? throwsOf(tag.lines) ?? [] : []),
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
"""把对话历史转写成一段文字, 交给不共享历史、每次都从零开始的子代理.
|
||||||
|
|
||||||
|
更早的对话已经被压缩成摘要 (`context_compaction["summary"]`, 可能为空),
|
||||||
|
`messages` 只剩近期窗口, 所以转写拼两段: 前情摘要 + 近期消息 (近期过长时只留尾部).
|
||||||
|
只转写用户与助手说过的文字: 工具往来与系统消息是执行细节, 子代理要看代码自己读.
|
||||||
|
平台注入的消息也不转写: 带 `UI_HIDDEN_KEY` 的运行通知 (任务日志 / 批准握手) 不是
|
||||||
|
谁说过的话, 握手里的「不要再讨论计划」还会和规划任务冲突; 带 `PLATFORM_ERROR_KEY`
|
||||||
|
的错误回复不是模型说的, 任何模型输入都不能带它.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from collections.abc import Collection, Sequence
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from agent_compiler_deepagents.middleware import PLATFORM_ERROR_KEY, UI_HIDDEN_KEY
|
||||||
|
|
||||||
|
BRIEF_MAX_LINES = 30
|
||||||
|
|
||||||
|
USER = "用户"
|
||||||
|
ASSISTANT = "助手"
|
||||||
|
_SPEAKER = {"HumanMessage": USER, "AIMessage": ASSISTANT}
|
||||||
|
|
||||||
|
|
||||||
|
def render_conversation_brief(
|
||||||
|
messages: Sequence[Any],
|
||||||
|
summary: str | None,
|
||||||
|
*,
|
||||||
|
speakers: Collection[str] = (USER, ASSISTANT),
|
||||||
|
max_lines: int = BRIEF_MAX_LINES,
|
||||||
|
) -> str:
|
||||||
|
"""把对话转写成「前情摘要 + 近期消息」一段文字; 什么都没有时返回空串.
|
||||||
|
|
||||||
|
@param messages 对话历史 (`messages` 状态).
|
||||||
|
@param summary 更早对话的压缩摘要, 没有就传 `None`.
|
||||||
|
@param speakers 转写谁说的话: [USER] / [ASSISTANT]; 只要用户原话时只传 [USER].
|
||||||
|
@param max_lines 近期消息最多留几行, 多了从头丢弃并注明.
|
||||||
|
@return 以 `[会话进展]` 开头的转写.
|
||||||
|
"""
|
||||||
|
lines = [t for m in messages if (t := _line_of(m, speakers))]
|
||||||
|
truncated = len(lines) > max_lines
|
||||||
|
if truncated:
|
||||||
|
lines = lines[-max_lines:]
|
||||||
|
summary_text = (summary or "").strip()
|
||||||
|
if not summary_text and not lines:
|
||||||
|
return ""
|
||||||
|
parts = ["[会话进展]" + ("\n(更早对话已省略)" if truncated else "")]
|
||||||
|
if summary_text:
|
||||||
|
parts += ["[前情摘要]", summary_text]
|
||||||
|
parts.extend(lines)
|
||||||
|
return "\n".join(parts)
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
"""只放行从[桌面端](backend/src/luffy_agent/sandbox/local_runtime/visibility.py#require_desktop_client)发来、且[「我的电脑」总开关](backend/src/luffy_agent/config/services.py#get_local_runtime_enabled)开着的请求,因为网页端没有本机可用,这个功能也还没对所有人开放。"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from fastapi import Request
|
||||||
|
|
||||||
|
from luffy_agent.config.services import get_local_runtime_enabled
|
||||||
|
from luffy_agent.core.exceptions import AppException
|
||||||
|
from luffy_agent.sandbox.local_runtime.visibility import require_desktop_client
|
||||||
|
|
||||||
|
|
||||||
|
class LocalRuntimeDisabled(AppException):
|
||||||
|
status_code = 403
|
||||||
|
error_code = "LOCAL_RUNTIME_DISABLED"
|
||||||
|
message = "「我的电脑」运行时暂未开放"
|
||||||
|
|
||||||
|
|
||||||
|
async def require_local_runtime_open(request: Request) -> None:
|
||||||
|
"""只放行桌面端发来、且 [「我的电脑」总开关](backend/src/luffy_agent/config/services.py#get_local_runtime_enabled) 开着的请求,因为只有桌面端能在用户自己的电脑上跑应用,总开关让平台能把这个功能整体关掉。
|
||||||
|
|
||||||
|
@throws [DesktopOnly](backend/src/luffy_agent/sandbox/local_runtime/visibility.py#DesktopOnly) 请求不是桌面端发来的
|
||||||
|
@throws [LocalRuntimeDisabled] 总开关关着
|
||||||
|
"""
|
||||||
|
require_desktop_client(request)
|
||||||
|
if not await get_local_runtime_enabled():
|
||||||
|
raise LocalRuntimeDisabled()
|
||||||
@@ -8,7 +8,7 @@ import { celeryRule } from '../rules/celery'
|
|||||||
import { dataclassRule } from '../rules/dataclass'
|
import { dataclassRule } from '../rules/dataclass'
|
||||||
import { enumRule } from '../rules/enum'
|
import { enumRule } from '../rules/enum'
|
||||||
import { sqlalchemyRule } from '../rules/sqlalchemy'
|
import { sqlalchemyRule } from '../rules/sqlalchemy'
|
||||||
import { flatSegments, outline, propertyLines, readFixture, runtime, WASM_DIR, withBlocks } from '../testSupport'
|
import { flatSegments, inlineOf, outline, propertyLines, readFixture, runtime, WASM_DIR, withBlocks } from '../testSupport'
|
||||||
import { pythonZhTranslator } from '../translators/pythonZh'
|
import { pythonZhTranslator } from '../translators/pythonZh'
|
||||||
import { typescriptZhTranslator } from '../translators/typescriptZh'
|
import { typescriptZhTranslator } from '../translators/typescriptZh'
|
||||||
import { createPythonAdapter } from './python'
|
import { createPythonAdapter } from './python'
|
||||||
@@ -55,7 +55,7 @@ function lines(list: readonly BlockView[], name: string): string[] {
|
|||||||
function linkedSegments(rows: PropertyRow[]): Segment[] {
|
function linkedSegments(rows: PropertyRow[]): Segment[] {
|
||||||
const pieces: Inline[] = rows.flatMap(row => [
|
const pieces: Inline[] = rows.flatMap(row => [
|
||||||
...('markdown' in row.value ? [] : row.value),
|
...('markdown' in row.value ? [] : row.value),
|
||||||
...(row.items ?? []).flatMap(item => item.cells.flat()),
|
...(row.items ?? []).flatMap(item => item.cells.flatMap(inlineOf)),
|
||||||
])
|
])
|
||||||
return flatSegments(pieces).filter(segment => segment.offset !== undefined)
|
return flatSegments(pieces).filter(segment => segment.offset !== undefined)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { toBlockViews } from '../blockView'
|
|||||||
import { blockMarks } from '../diff/viewDiff'
|
import { blockMarks } from '../diff/viewDiff'
|
||||||
import { rustImplRule } from '../rules/rustImpl'
|
import { rustImplRule } from '../rules/rustImpl'
|
||||||
import { isCrateRoot } from '../rules/rustTree'
|
import { isCrateRoot } from '../rules/rustTree'
|
||||||
import { readFixture, viewLines, WASM_DIR, withBlocks } from '../testSupport'
|
import { propertyLines, readFixture, viewLines, WASM_DIR, withBlocks } from '../testSupport'
|
||||||
import { rustZhTranslator } from '../translators/rustZh'
|
import { rustZhTranslator } from '../translators/rustZh'
|
||||||
import { createRustAdapter } from './rust'
|
import { createRustAdapter } from './rust'
|
||||||
|
|
||||||
@@ -249,6 +249,45 @@ describe('rust 注释', () => {
|
|||||||
expect(viewLines([list[2]!])).toContain(' 说明: 读出这个账号的凭证。')
|
expect(viewLines([list[2]!])).toContain(' 说明: 读出这个账号的凭证。')
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('连着几行的 /// 是一段说明,块标签拆开放:@param 进参数表、@return 跟在返回后面、@throws 成异常表;结构体的 @param 对到字段', async () => {
|
||||||
|
const source = [
|
||||||
|
'/// 读出这个账号的凭证。',
|
||||||
|
'///',
|
||||||
|
'/// @param account 账号',
|
||||||
|
'/// @param ttl 写错的名字',
|
||||||
|
'/// @return 没存过时是 None',
|
||||||
|
'/// @throws [StoreError] 存储读不了',
|
||||||
|
'pub fn load(account: &str) -> Option<String> { None }',
|
||||||
|
'',
|
||||||
|
'/// 一个凭证。',
|
||||||
|
'/// @param token 访问令牌',
|
||||||
|
'pub struct Credential { pub token: String, scope: u8 }',
|
||||||
|
].join('\n')
|
||||||
|
const list = await views(source)
|
||||||
|
const fn = card(list, 'function')
|
||||||
|
const struct = card(list, 'class')
|
||||||
|
expect('properties' in fn && propertyLines(fn.properties)).toEqual([
|
||||||
|
'名称: load',
|
||||||
|
'说明: 读出这个账号的凭证。',
|
||||||
|
'对不上的参数: 1 个',
|
||||||
|
' ttl | 写错的名字',
|
||||||
|
'可见性: 公开',
|
||||||
|
'输入参数: 1 个',
|
||||||
|
' account | &str | 账号',
|
||||||
|
'返回: Option<String> — 没存过时是 None',
|
||||||
|
'异常: 1 个',
|
||||||
|
' [StoreError] | 存储读不了',
|
||||||
|
])
|
||||||
|
expect('properties' in struct && propertyLines(struct.properties)).toEqual([
|
||||||
|
'名称: Credential',
|
||||||
|
'说明: 一个凭证。',
|
||||||
|
'可见性: 公开',
|
||||||
|
'字段: 2 个',
|
||||||
|
' token | String | 公开 | 访问令牌',
|
||||||
|
' scope | u8 | 本模块 | ',
|
||||||
|
])
|
||||||
|
})
|
||||||
|
|
||||||
it('在 mod、impl 身体里同一套', async () => {
|
it('在 mod、impl 身体里同一套', async () => {
|
||||||
const source = [
|
const source = [
|
||||||
'mod store {',
|
'mod store {',
|
||||||
|
|||||||
@@ -116,7 +116,7 @@ function referencesInText(pieces: readonly Inline[]): Segment[] {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function referencesInItems(items: readonly PropertyItem[] | undefined): Segment[] {
|
function referencesInItems(items: readonly PropertyItem[] | undefined): Segment[] {
|
||||||
return (items ?? []).flatMap(item => [...item.cells.flatMap(referencesInText), ...referencesInItems(item.items)])
|
return (items ?? []).flatMap(item => [...item.cells.flatMap(cell => 'markdown' in cell ? [] : referencesInText(cell)), ...referencesInItems(item.items)])
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 属性表里(值、各层子行的格子)带引用的段。 */
|
/** 属性表里(值、各层子行的格子)带引用的段。 */
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ export function renderedNames(blocks: readonly BlockView[]): Map<number, string>
|
|||||||
}
|
}
|
||||||
const items = (list: readonly PropertyItem[] | undefined): void => {
|
const items = (list: readonly PropertyItem[] | undefined): void => {
|
||||||
for (const item of list ?? []) {
|
for (const item of list ?? []) {
|
||||||
item.cells.forEach(inline)
|
item.cells.forEach(text)
|
||||||
items(item.items)
|
items(item.items)
|
||||||
items(item.table?.items)
|
items(item.table?.items)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import type { FunctionBlock, Inline, KindBadge, LibraryRule, Node, PropertyItem, PropertyRow, RuleCard, ThemeColor } from '../contract'
|
import type { FunctionBlock, Inline, KindBadge, LibraryRule, Node, PropertyItem, PropertyRow, RuleCard, RuleContext, ThemeColor } from '../contract'
|
||||||
import { attributeName, docSentence, named, oneLine, returnText, stringContent } from './rustTree'
|
import { returnNote, throwsRows, writtenParamRows } from './docRows'
|
||||||
|
import { attributeName, docSummary, named, oneLine, returnText, stringContent } from './rustTree'
|
||||||
|
|
||||||
/** 和 FastAPI 接口同一个蓝。 */
|
/** 和 FastAPI 接口同一个蓝。 */
|
||||||
const COLOR: ThemeColor = { dark: '#61afef', light: '#1a6fb5' }
|
const COLOR: ThemeColor = { dark: '#61afef', light: '#1a6fb5' }
|
||||||
@@ -16,14 +17,14 @@ export const axumRule: LibraryRule = {
|
|||||||
label: 'axum 接口',
|
label: 'axum 接口',
|
||||||
languageIds: ['rust'],
|
languageIds: ['rust'],
|
||||||
libraries: ['axum'],
|
libraries: ['axum'],
|
||||||
claim: (block) => {
|
claim: (block, context) => {
|
||||||
if (block.kind === 'function') {
|
if (block.kind === 'function') {
|
||||||
const route = attributeRoute(block)
|
const route = attributeRoute(block)
|
||||||
return route ? card(block, route) : undefined
|
return route ? card(block, route, context) : undefined
|
||||||
}
|
}
|
||||||
if (block.kind === 'statement') {
|
if (block.kind === 'statement') {
|
||||||
const [route] = routesOn(block.node).filter(item => !handled(block.node, item.handler))
|
const [route] = routesOn(block.node).filter(item => !handled(block.node, item.handler))
|
||||||
return route ? routerCard(block.node, route) : undefined
|
return route ? routerCard(block.node, route, context) : undefined
|
||||||
}
|
}
|
||||||
return undefined
|
return undefined
|
||||||
},
|
},
|
||||||
@@ -48,15 +49,18 @@ function attributeRoute(block: FunctionBlock): Route | undefined {
|
|||||||
return undefined
|
return undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
function card(block: FunctionBlock, route: Route): RuleCard {
|
function card(block: FunctionBlock, route: Route, context: RuleContext): RuleCard {
|
||||||
const doc = docSentence(block.facts.doc)
|
const doc = block.facts.doc && context.doc(block.facts.doc)
|
||||||
|
const summary = docSummary(doc)
|
||||||
const params = pathParams(route.path, block.node)
|
const params = pathParams(route.path, block.node)
|
||||||
const rows: PropertyRow[] = [
|
const rows: PropertyRow[] = [
|
||||||
nameRow(block.facts.name),
|
nameRow(block.facts.name),
|
||||||
...(doc ? [{ label: '说明', value: { markdown: doc } }] : []),
|
...(summary ? [{ label: '说明', value: { markdown: summary } }] : []),
|
||||||
...(block.facts.async ? [{ label: '异步', value: [keyword('是')] }] : []),
|
...(block.facts.async ? [{ label: '异步', value: [keyword('是')] }] : []),
|
||||||
requestRow(route, params),
|
requestRow(route, params),
|
||||||
{ label: '返回', value: [plain(returnText(block.node))] },
|
...writtenParamRows(doc?.params ?? [], '参数'),
|
||||||
|
{ label: '返回', value: [plain(returnText(block.node))], ...returnNote(doc) },
|
||||||
|
...throwsRows(doc),
|
||||||
]
|
]
|
||||||
return {
|
return {
|
||||||
kind: 'axum.route',
|
kind: 'axum.route',
|
||||||
@@ -80,10 +84,11 @@ function handled(node: Node, handler: string | undefined): boolean {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
function routerCard(node: Node, route: Route & { handlerNode?: Node }): RuleCard {
|
function routerCard(node: Node, route: Route & { handlerNode?: Node }, context: RuleContext): RuleCard {
|
||||||
const fn = route.handler ? functionNamed(node, route.handler) : undefined
|
const fn = route.handler ? functionNamed(node, route.handler) : undefined
|
||||||
const params = fn ? pathParams(route.path, fn) : []
|
const params = fn ? pathParams(route.path, fn) : []
|
||||||
const doc = fn ? docSentence(leadingDoc(fn)) : undefined
|
const comment = fn && leadingDoc(fn)
|
||||||
|
const doc = docSummary(comment && context.doc(comment))
|
||||||
return {
|
return {
|
||||||
kind: 'axum.route',
|
kind: 'axum.route',
|
||||||
badge: BADGE,
|
badge: BADGE,
|
||||||
@@ -184,11 +189,18 @@ function functionNamed(node: Node, name: string): Node | undefined {
|
|||||||
return named(node.tree.rootNode).find(item => item.type === 'function_item' && item.childForFieldName('name')?.text === name)
|
return named(node.tree.rootNode).find(item => item.type === 'function_item' && item.childForFieldName('name')?.text === name)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 函数上面的文档注释(中间可以隔着属性):连着几行的 `///` 取第一条,读说明时从它往下连。 */
|
||||||
|
|
||||||
function leadingDoc(fn: Node): Node | undefined {
|
function leadingDoc(fn: Node): Node | undefined {
|
||||||
let prev = fn.previousNamedSibling
|
let prev = fn.previousNamedSibling
|
||||||
while (prev?.type === 'attribute_item')
|
while (prev?.type === 'attribute_item')
|
||||||
prev = prev.previousNamedSibling
|
prev = prev.previousNamedSibling
|
||||||
return prev && (prev.type === 'line_comment' || prev.type === 'block_comment') ? prev : undefined
|
if (!prev || (prev.type !== 'line_comment' && prev.type !== 'block_comment'))
|
||||||
|
return undefined
|
||||||
|
let first = prev
|
||||||
|
for (let before = first.previousNamedSibling; first.type === 'line_comment' && before?.type === 'line_comment' && before.startPosition.row === first.startPosition.row - 1; before = before.previousNamedSibling)
|
||||||
|
first = before
|
||||||
|
return first
|
||||||
}
|
}
|
||||||
|
|
||||||
function attributeString(attributeItem: Node): string | undefined {
|
function attributeString(attributeItem: Node): string | undefined {
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import type { Inline, PropertyRow, Segment } from '../contract'
|
|||||||
import { describe, expect, it } from 'vitest'
|
import { describe, expect, it } from 'vitest'
|
||||||
import { toBlockViews } from '../blockView'
|
import { toBlockViews } from '../blockView'
|
||||||
import { createPythonAdapter } from '../languages/python'
|
import { createPythonAdapter } from '../languages/python'
|
||||||
import { flatSegments, MemoryIndexHost, propertyLines, readFixture, WASM_DIR, withBlocks } from '../testSupport'
|
import { flatSegments, inlineOf, MemoryIndexHost, propertyLines, readFixture, WASM_DIR, withBlocks } from '../testSupport'
|
||||||
import { pythonZhTranslator } from '../translators/pythonZh'
|
import { pythonZhTranslator } from '../translators/pythonZh'
|
||||||
import { WorkspaceIndexes } from '../workspaceIndex'
|
import { WorkspaceIndexes } from '../workspaceIndex'
|
||||||
import { celeryRule } from './celery'
|
import { celeryRule } from './celery'
|
||||||
@@ -38,7 +38,7 @@ async function beatLines(entries: string, header = 'from celery import Celery\nf
|
|||||||
function segmentsOf(rows: PropertyRow[]): Segment[] {
|
function segmentsOf(rows: PropertyRow[]): Segment[] {
|
||||||
const pieces: Inline[] = rows.flatMap(row => [
|
const pieces: Inline[] = rows.flatMap(row => [
|
||||||
...('markdown' in row.value ? [] : row.value),
|
...('markdown' in row.value ? [] : row.value),
|
||||||
...(row.items ?? []).flatMap(item => item.cells.flat()),
|
...(row.items ?? []).flatMap(item => item.cells.flatMap(inlineOf)),
|
||||||
])
|
])
|
||||||
return flatSegments(pieces)
|
return flatSegments(pieces)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -454,7 +454,7 @@ function optionsCell(options: Option[], env: Env): Inline[] {
|
|||||||
|
|
||||||
/** 注释的原文(翻译插件去掉 #),几条连成一段。 */
|
/** 注释的原文(翻译插件去掉 #),几条连成一段。 */
|
||||||
function commentText(comments: Node[], env: Env): Inline[] {
|
function commentText(comments: Node[], env: Env): Inline[] {
|
||||||
const text = comments.map(comment => env.context.doc(comment)?.markdown).filter(Boolean).join(' ')
|
const text = comments.map(comment => env.context.doc(comment)?.body?.markdown).filter(Boolean).join(' ')
|
||||||
return text ? [{ text, role: 'comment' }] : []
|
return text ? [{ text, role: 'comment' }] : []
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -490,7 +490,7 @@ function beatBlock(app: Node, schedule: Node, settings: [string, Node][], doc: N
|
|||||||
const entries = entriesOf(schedule)
|
const entries = entriesOf(schedule)
|
||||||
const spreads = entries.filter(entry => entry.spread).length
|
const spreads = entries.filter(entry => entry.spread).length
|
||||||
const withOptions = entries.some(entry => extraOptions(entry).length > 0)
|
const withOptions = entries.some(entry => extraOptions(entry).length > 0)
|
||||||
const markdown = doc && context.doc(doc)
|
const markdown = doc && context.doc(doc)?.body
|
||||||
const rows: PropertyRow[] = [
|
const rows: PropertyRow[] = [
|
||||||
{ label: labels.app, value: context.translate(app) },
|
{ label: labels.app, value: context.translate(app) },
|
||||||
...(markdown ? [{ label: labels.doc, value: markdown }] : []),
|
...(markdown ? [{ label: labels.doc, value: markdown }] : []),
|
||||||
@@ -515,7 +515,7 @@ function appBlock(target: Node, call: Call, doc: Node | undefined, env: Env): Ru
|
|||||||
call.positional.forEach((arg, i) => parameters.set(APP_PARAMETERS[i] ?? String(i), arg))
|
call.positional.forEach((arg, i) => parameters.set(APP_PARAMETERS[i] ?? String(i), arg))
|
||||||
for (const [key, value] of call.keywords)
|
for (const [key, value] of call.keywords)
|
||||||
parameters.set(key, value)
|
parameters.set(key, value)
|
||||||
const markdown = doc && context.doc(doc)
|
const markdown = doc && context.doc(doc)?.body
|
||||||
const named = new Set<string>(APP_NAMED)
|
const named = new Set<string>(APP_NAMED)
|
||||||
const rows: PropertyRow[] = [
|
const rows: PropertyRow[] = [
|
||||||
{ label: labels.name, value: target.type === 'identifier' ? [name(target, 'variable')] : context.translate(target) },
|
{ label: labels.name, value: target.type === 'identifier' ? [name(target, 'variable')] : context.translate(target) },
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { fileAttributes } from '../attributes'
|
|||||||
import { toBlockViews } from '../blockView'
|
import { toBlockViews } from '../blockView'
|
||||||
import { replaceLines } from '../editText'
|
import { replaceLines } from '../editText'
|
||||||
import { createPythonAdapter } from '../languages/python'
|
import { createPythonAdapter } from '../languages/python'
|
||||||
import { flatSegments, propertyLines, readFixture, WASM_DIR, withBlocks } from '../testSupport'
|
import { flatSegments, inlineOf, propertyLines, readFixture, WASM_DIR, withBlocks } from '../testSupport'
|
||||||
import { pythonZhTranslator } from '../translators/pythonZh'
|
import { pythonZhTranslator } from '../translators/pythonZh'
|
||||||
import { celeryRule } from './celery'
|
import { celeryRule } from './celery'
|
||||||
import { dataclassRule } from './dataclass'
|
import { dataclassRule } from './dataclass'
|
||||||
@@ -39,7 +39,7 @@ function named(list: BlockView[], name: string): BlockView {
|
|||||||
function linkedSegments(rows: PropertyRow[]): Segment[] {
|
function linkedSegments(rows: PropertyRow[]): Segment[] {
|
||||||
const pieces: Inline[] = rows.flatMap(row => [
|
const pieces: Inline[] = rows.flatMap(row => [
|
||||||
...('markdown' in row.value ? [] : row.value),
|
...('markdown' in row.value ? [] : row.value),
|
||||||
...(row.items ?? []).flatMap(item => item.cells.flat()),
|
...(row.items ?? []).flatMap(item => item.cells.flatMap(inlineOf)),
|
||||||
])
|
])
|
||||||
return flatSegments(pieces).filter(segment => segment.offset !== undefined)
|
return flatSegments(pieces).filter(segment => segment.offset !== undefined)
|
||||||
}
|
}
|
||||||
@@ -260,4 +260,34 @@ describe('dataclasses 库规则(Python)', () => {
|
|||||||
const list = await views('@dataclass\nclass A:\n x: int\n')
|
const list = await views('@dataclass\nclass A:\n x: int\n')
|
||||||
expect(list.map(view => view.kind)).toEqual(['class'])
|
expect(list.map(view => view.kind)).toEqual(['class'])
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('类说明里的 @param 补上字段的说明(字段自己有注释的不换),对不上的排在说明下面', async () => {
|
||||||
|
const source = [
|
||||||
|
'from dataclasses import dataclass',
|
||||||
|
'',
|
||||||
|
'@dataclass',
|
||||||
|
'class Point:',
|
||||||
|
' """平面上的点。',
|
||||||
|
'',
|
||||||
|
' @param x 横坐标',
|
||||||
|
' @param y 不会盖过字段自己的注释',
|
||||||
|
' @param z 写错的名字',
|
||||||
|
' """',
|
||||||
|
' x: int',
|
||||||
|
' # 纵坐标',
|
||||||
|
' y: int = 0',
|
||||||
|
].join('\n')
|
||||||
|
const view = named(await views(source), 'Point')
|
||||||
|
expect(propertyLines(properties(view))).toEqual([
|
||||||
|
'名称: Point',
|
||||||
|
'说明: 平面上的点。',
|
||||||
|
'对不上的参数: 1 个',
|
||||||
|
' z | 写错的名字',
|
||||||
|
'访问级别: 公共',
|
||||||
|
'字段: 2 个',
|
||||||
|
' x | 整数 | | 横坐标',
|
||||||
|
' y | 整数 | 0 | 纵坐标',
|
||||||
|
])
|
||||||
|
expect(properties(view).find(row => row.label === '字段')?.items?.[0].cells[3]).toEqual({ markdown: '横坐标' })
|
||||||
|
})
|
||||||
})
|
})
|
||||||
@@ -1,6 +1,8 @@
|
|||||||
import type { Access, BlockConcept, ClassBlock, CodeBlock, Inline, KindIconName, LibraryRule, Node, PropertyItem, PropertyRow, RuleCard, RuleContext, ThemeColor } from '../contract'
|
import type { Access, BlockConcept, ClassBlock, CodeBlock, Inline, KindIconName, LibraryRule, Node, PreviewText, PropertyItem, PropertyRow, RuleCard, RuleContext, ThemeColor } from '../contract'
|
||||||
|
import type { ParamDocs } from './docRows'
|
||||||
import type { Call, LibraryNames } from './pythonTree'
|
import type { Call, LibraryNames } from './pythonTree'
|
||||||
import { annotatedAssignments, memberDocs, withLeadingComments } from './children'
|
import { annotatedAssignments, memberDocs, withLeadingComments } from './children'
|
||||||
|
import { docBodyRows, paramDocs, throwsRows, unmatchedParamRows } from './docRows'
|
||||||
import { aliasesOf, callOf, canonicalName, genericOf, libraryName, libraryNames, stringValue, syntaxChildren, unwrapType } from './pythonTree'
|
import { aliasesOf, callOf, canonicalName, genericOf, libraryName, libraryNames, stringValue, syntaxChildren, unwrapType } from './pythonTree'
|
||||||
import { hint, joined, name, plain, range, word } from './segments'
|
import { hint, joined, name, plain, range, word } from './segments'
|
||||||
|
|
||||||
@@ -209,16 +211,22 @@ function defaultCell(field: Field, call: Call | undefined, env: Env): Inline[] {
|
|||||||
return joined(parts, hint(' · '))
|
return joined(parts, hint(' · '))
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 说明一格:`field(doc="…")` 的字符串,没写时是讲它的注释、属性 docstring 的原文(用空格连起来);InitVar 的字段前面写「仅初始化时使用」。 */
|
/**
|
||||||
function docCell(field: Field, children: readonly CodeBlock[], call: Call | undefined, initOnly: boolean, env: Env): Inline[] {
|
* 说明一格:`field(doc="…")` 的字符串,没写时是讲它的注释、属性 docstring 的原文(用空格连起来),都没有时是类的说明里讲它的 `@param`
|
||||||
|
* (字段就是构造参数);InitVar 的字段前面写「仅初始化时使用」。
|
||||||
|
*/
|
||||||
|
function docCell(field: Field, children: readonly CodeBlock[], call: Call | undefined, initOnly: boolean, docs: ParamDocs, env: Env): PreviewText {
|
||||||
const explicit = stringValue(call?.keywords.get('doc'))
|
const explicit = stringValue(call?.keywords.get('doc'))
|
||||||
const text = explicit ?? memberDocs(children, field.block).map(node => env.context.doc(node)?.markdown).filter(Boolean).join(' ')
|
const text = explicit ?? memberDocs(children, field.block).map(node => env.context.doc(node)?.body?.markdown).filter(Boolean).join(' ')
|
||||||
|
const written = text ? undefined : docs.of(field.name.text)
|
||||||
|
if (written)
|
||||||
|
return initOnly ? { markdown: `${env.words.initOnly} · ${written.markdown}` } : written
|
||||||
const doc: Inline[] = text ? [{ text, role: 'comment' }] : []
|
const doc: Inline[] = text ? [{ text, role: 'comment' }] : []
|
||||||
return initOnly ? joined([[word(env.words.initOnly)], ...(text ? [doc] : [])], hint(' · ')) : doc
|
return initOnly ? joined([[word(env.words.initOnly)], ...(text ? [doc] : [])], hint(' · ')) : doc
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 字段表的一行:字段 | 类型 | 默认值 | 说明。讲的定义是字段名。 */
|
/** 字段表的一行:字段 | 类型 | 默认值 | 说明。讲的定义是字段名。 */
|
||||||
function fieldItem(field: Field, children: readonly CodeBlock[], env: Env): PropertyItem {
|
function fieldItem(field: Field, children: readonly CodeBlock[], docs: ParamDocs, env: Env): PropertyItem {
|
||||||
const call = fieldCall(field.value, env)
|
const call = fieldCall(field.value, env)
|
||||||
const initOnly = annotationName(field.annotation, env) === 'InitVar'
|
const initOnly = annotationName(field.annotation, env) === 'InitVar'
|
||||||
const type = initOnly ? genericOf(field.annotation)?.args[0] : undefined
|
const type = initOnly ? genericOf(field.annotation)?.args[0] : undefined
|
||||||
@@ -227,7 +235,7 @@ function fieldItem(field: Field, children: readonly CodeBlock[], env: Env): Prop
|
|||||||
[name(field.name, 'variable')],
|
[name(field.name, 'variable')],
|
||||||
env.context.translate(type ?? field.annotation),
|
env.context.translate(type ?? field.annotation),
|
||||||
defaultCell(field, call, env),
|
defaultCell(field, call, env),
|
||||||
docCell(field, children, call, initOnly, env),
|
docCell(field, children, call, initOnly, docs, env),
|
||||||
],
|
],
|
||||||
defines: range(field.name),
|
defines: range(field.name),
|
||||||
}
|
}
|
||||||
@@ -246,11 +254,13 @@ function dataclassCard(block: ClassBlock, decorator: Node, call: Call | undefine
|
|||||||
const { facts, children } = block
|
const { facts, children } = block
|
||||||
const { fields, markers } = fieldsOf(children, env)
|
const { fields, markers } = fieldsOf(children, env)
|
||||||
const doc = facts.doc && context.doc(facts.doc)
|
const doc = facts.doc && context.doc(facts.doc)
|
||||||
|
const docs = paramDocs(doc, fields.map(field => field.name.text))
|
||||||
const decorators = facts.decorators.filter(other => other !== decorator)
|
const decorators = facts.decorators.filter(other => other !== decorator)
|
||||||
const options = optionsText(call, env)
|
const options = optionsText(call, env)
|
||||||
const rows: PropertyRow[] = [
|
const rows: PropertyRow[] = [
|
||||||
...(facts.name ? [{ label: labels.name, value: [name(facts.name, 'type')] }] : []),
|
...(facts.name ? [{ label: labels.name, value: [name(facts.name, 'type')] }] : []),
|
||||||
...(doc ? [{ label: labels.doc, value: doc }] : []),
|
...docBodyRows(doc, labels.doc),
|
||||||
|
...unmatchedParamRows(docs.unmatched),
|
||||||
...(facts.access ? [{ label: labels.access, value: [{ text: words.access[facts.access], role: 'keyword' as const }] }] : []),
|
...(facts.access ? [{ label: labels.access, value: [{ text: words.access[facts.access], role: 'keyword' as const }] }] : []),
|
||||||
...(facts.bases.length > 0 ? [{ label: labels.bases, value: joined(facts.bases.map(base => context.translate(base.value)), plain(', ')) }] : []),
|
...(facts.bases.length > 0 ? [{ label: labels.bases, value: joined(facts.bases.map(base => context.translate(base.value)), plain(', ')) }] : []),
|
||||||
...(decorators.length > 0 ? [{ label: labels.decorators, value: joined(decorators.map(other => context.translate(other)), plain('\n')) }] : []),
|
...(decorators.length > 0 ? [{ label: labels.decorators, value: joined(decorators.map(other => context.translate(other)), plain('\n')) }] : []),
|
||||||
@@ -259,8 +269,9 @@ function dataclassCard(block: ClassBlock, decorator: Node, call: Call | undefine
|
|||||||
label: labels.fields,
|
label: labels.fields,
|
||||||
value: [hint(words.count(fields.length))],
|
value: [hint(words.count(fields.length))],
|
||||||
columns: words.heads,
|
columns: words.heads,
|
||||||
items: fields.map(field => fieldItem(field, children, env)),
|
items: fields.map(field => fieldItem(field, children, docs, env)),
|
||||||
},
|
},
|
||||||
|
...throwsRows(doc),
|
||||||
]
|
]
|
||||||
const concepts = frozenConcept(call, words)
|
const concepts = frozenConcept(call, words)
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -0,0 +1,95 @@
|
|||||||
|
import type { Doc, DocParam, Markdown, PreviewText, PropertyRow } from '../contract'
|
||||||
|
import { hint } from './segments'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 卡片里讲说明(Doc)的几行,语言默认的卡片(中文直译)和库规则的卡片共用:说明的正文、说明里写了参数表里却没有的参数、
|
||||||
|
* 参数表里的「说明」一格、输出那一行后面的 `@return`、「异常」一行。用词是中文。
|
||||||
|
*/
|
||||||
|
|
||||||
|
/** 参数名去掉前缀、后缀再对:`*args`、`**kwargs`、`...rest` 的名字是 args、kwargs、rest,可省略参数 `x?` 是 x。 */
|
||||||
|
export function paramKey(name: string): string {
|
||||||
|
return name.replace(/^(?:\*{1,2}|\.{3})/, '').replace(/\?$/, '')
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 说明里的参数按名字对到参数表:of 取一个参数的说明,described 是表里有参数对上了说明,unmatched 是表里没有的那几个。 */
|
||||||
|
export interface ParamDocs {
|
||||||
|
of: (name: string) => Markdown | undefined
|
||||||
|
described: boolean
|
||||||
|
unmatched: DocParam[]
|
||||||
|
}
|
||||||
|
|
||||||
|
/** names 是参数表里的参数名(照写,`*args` 带着星号);同一个参数写了几条时说明连起来。 */
|
||||||
|
export function paramDocs(doc: Doc | undefined, names: readonly string[]): ParamDocs {
|
||||||
|
const keys = new Set(names.map(paramKey))
|
||||||
|
const texts = new Map<string, string>()
|
||||||
|
const unmatched: DocParam[] = []
|
||||||
|
for (const param of doc?.params ?? []) {
|
||||||
|
const key = paramKey(param.name)
|
||||||
|
if (!keys.has(key))
|
||||||
|
unmatched.push(param)
|
||||||
|
else if (param.text.markdown !== '')
|
||||||
|
texts.set(key, [texts.get(key), param.text.markdown].filter(Boolean).join('\n\n'))
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
of: (name) => {
|
||||||
|
const text = texts.get(paramKey(name))
|
||||||
|
return text === undefined ? undefined : { markdown: text }
|
||||||
|
},
|
||||||
|
described: texts.size > 0,
|
||||||
|
unmatched,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 参数表的「说明」一格:对上了的说明,没有时留空。 */
|
||||||
|
export function paramDocCell(docs: ParamDocs, name: string): PreviewText {
|
||||||
|
return docs.of(name) ?? []
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 「说明」一行:说明的正文;只写了块标签、没有正文时没有。 */
|
||||||
|
export function docBodyRows(doc: Doc | undefined, label = '说明'): PropertyRow[] {
|
||||||
|
return doc?.body ? [{ label, value: doc.body }] : []
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 排在「说明」下面:说明里写了、参数表里没有的参数(名字写错了),名称是提示色、悬停「没有这个参数」,后面是它的说明。 */
|
||||||
|
export function unmatchedParamRows(unmatched: readonly DocParam[]): PropertyRow[] {
|
||||||
|
if (unmatched.length === 0)
|
||||||
|
return []
|
||||||
|
return [{
|
||||||
|
label: '对不上的参数',
|
||||||
|
value: [hint(`${unmatched.length} 个`)],
|
||||||
|
columns: ['名称', '说明'],
|
||||||
|
items: unmatched.map(param => ({ cells: [[{ text: param.name, role: 'hint', tooltip: '没有这个参数' }], param.text] })),
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 卡片上没有参数表可对(类只有说明、没写构造函数)时,说明里写的参数照写成一张「名称 | 说明」表;一个都没写时没有。
|
||||||
|
*/
|
||||||
|
export function writtenParamRows(params: readonly DocParam[], label: string): PropertyRow[] {
|
||||||
|
if (params.length === 0)
|
||||||
|
return []
|
||||||
|
return [{
|
||||||
|
label,
|
||||||
|
value: [hint(`${params.length} 个`)],
|
||||||
|
columns: ['名称', '说明'],
|
||||||
|
items: params.map(param => ({ cells: [[{ text: param.name, role: 'variable' }], param.text] })),
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 输出那一行跟在值后面的说明(`@return`);没写时没有。 */
|
||||||
|
export function returnNote(doc: Doc | undefined): Pick<PropertyRow, 'note'> {
|
||||||
|
return doc?.returns ? { note: doc.returns } : {}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 「异常」一行:说明里的 `@throws`,下面一张「异常 | 触发条件」表,一条一行(异常名能点);没写时没有。 */
|
||||||
|
export function throwsRows(doc: Doc | undefined): PropertyRow[] {
|
||||||
|
const throws = doc?.throws ?? []
|
||||||
|
if (throws.length === 0)
|
||||||
|
return []
|
||||||
|
return [{
|
||||||
|
label: '异常',
|
||||||
|
value: [hint(`${throws.length} 个`)],
|
||||||
|
columns: ['异常', '触发条件'],
|
||||||
|
items: throws.map(item => ({ cells: [item.exception, item.when] })),
|
||||||
|
}]
|
||||||
|
}
|
||||||
@@ -6,7 +6,7 @@ import { toBlockViews } from '../blockView'
|
|||||||
import { replaceLines } from '../editText'
|
import { replaceLines } from '../editText'
|
||||||
import { createPythonAdapter } from '../languages/python'
|
import { createPythonAdapter } from '../languages/python'
|
||||||
import { createTypeScriptAdapter } from '../languages/typescript'
|
import { createTypeScriptAdapter } from '../languages/typescript'
|
||||||
import { flatSegments, propertyLines, readFixture, WASM_DIR, withBlocks } from '../testSupport'
|
import { flatSegments, inlineOf, propertyLines, readFixture, WASM_DIR, withBlocks } from '../testSupport'
|
||||||
import { pythonZhTranslator } from '../translators/pythonZh'
|
import { pythonZhTranslator } from '../translators/pythonZh'
|
||||||
import { typescriptZhTranslator } from '../translators/typescriptZh'
|
import { typescriptZhTranslator } from '../translators/typescriptZh'
|
||||||
import { celeryRule } from './celery'
|
import { celeryRule } from './celery'
|
||||||
@@ -45,7 +45,7 @@ function named(list: BlockView[], name: string): BlockView {
|
|||||||
function linkedSegments(rows: PropertyRow[]): Segment[] {
|
function linkedSegments(rows: PropertyRow[]): Segment[] {
|
||||||
const pieces: Inline[] = rows.flatMap(row => [
|
const pieces: Inline[] = rows.flatMap(row => [
|
||||||
...('markdown' in row.value ? [] : row.value),
|
...('markdown' in row.value ? [] : row.value),
|
||||||
...(row.items ?? []).flatMap(item => item.cells.flat()),
|
...(row.items ?? []).flatMap(item => item.cells.flatMap(inlineOf)),
|
||||||
])
|
])
|
||||||
return flatSegments(pieces).filter(segment => segment.offset !== undefined)
|
return flatSegments(pieces).filter(segment => segment.offset !== undefined)
|
||||||
}
|
}
|
||||||
@@ -224,7 +224,7 @@ describe('枚举:TypeScript 和 Python 同一张卡片', () => {
|
|||||||
attributes: view.attributes,
|
attributes: view.attributes,
|
||||||
lines: propertyLines(rows),
|
lines: propertyLines(rows),
|
||||||
columns: rows.map(row => row.columns),
|
columns: rows.map(row => row.columns),
|
||||||
roles: rows.at(-1)?.items?.map(item => item.cells.map(cell => flatSegments(cell).map(segment => segment.role))),
|
roles: rows.at(-1)?.items?.map(item => item.cells.map(cell => flatSegments(inlineOf(cell)).map(segment => segment.role))),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
expect(shape(tsEnum)).toEqual(shape(pyEnum))
|
expect(shape(tsEnum)).toEqual(shape(pyEnum))
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { describe, expect, it, vi } from 'vitest'
|
|||||||
import { toBlockViews } from '../blockView'
|
import { toBlockViews } from '../blockView'
|
||||||
import { createPythonAdapter } from '../languages/python'
|
import { createPythonAdapter } from '../languages/python'
|
||||||
import { DocumentLookups } from '../ruleLookups'
|
import { DocumentLookups } from '../ruleLookups'
|
||||||
import { flatSegments, MemoryIndexHost, propertyLines, readFixture, WASM_DIR, withBlocks } from '../testSupport'
|
import { flatSegments, inlineOf, MemoryIndexHost, propertyLines, readFixture, WASM_DIR, withBlocks } from '../testSupport'
|
||||||
import { pythonZhTranslator } from '../translators/pythonZh'
|
import { pythonZhTranslator } from '../translators/pythonZh'
|
||||||
import { WorkspaceIndexes } from '../workspaceIndex'
|
import { WorkspaceIndexes } from '../workspaceIndex'
|
||||||
import { fastapiRule } from './fastapi'
|
import { fastapiRule } from './fastapi'
|
||||||
@@ -47,7 +47,7 @@ async function route(source: string, fn: string, lookup?: RuleContext['lookup'])
|
|||||||
function segmentsOf(row: PropertyRow | undefined): Segment[] {
|
function segmentsOf(row: PropertyRow | undefined): Segment[] {
|
||||||
const pieces: Inline[] = [
|
const pieces: Inline[] = [
|
||||||
...(!row || 'markdown' in row.value ? [] : row.value),
|
...(!row || 'markdown' in row.value ? [] : row.value),
|
||||||
...(row?.items ?? []).flatMap(item => item.cells.flat()),
|
...(row?.items ?? []).flatMap(item => item.cells.flatMap(inlineOf)),
|
||||||
]
|
]
|
||||||
return flatSegments(pieces)
|
return flatSegments(pieces)
|
||||||
}
|
}
|
||||||
@@ -97,20 +97,20 @@ describe('fastapi 库规则:接口卡片', () => {
|
|||||||
const rows = properties(await route(ROUTES, 'update_user', known({ Forbidden: '403' })))
|
const rows = properties(await route(ROUTES, 'update_user', known({ Forbidden: '403' })))
|
||||||
const request = rows.find(row => row.label === '请求')
|
const request = rows.find(row => row.label === '请求')
|
||||||
const [url, method] = request?.items ?? []
|
const [url, method] = request?.items ?? []
|
||||||
expect(flatSegments(url.cells[1]).map(piece => [piece.text, piece.role])).toEqual([['/users', 'string'], ['/', 'string'], ['{', 'variable'], ['user_id', 'variable'], ['}', 'variable']])
|
expect(flatSegments(inlineOf(url.cells[1])).map(piece => [piece.text, piece.role])).toEqual([['/users', 'string'], ['/', 'string'], ['{', 'variable'], ['user_id', 'variable'], ['}', 'variable']])
|
||||||
for (const piece of flatSegments(url.cells[1]))
|
for (const piece of flatSegments(inlineOf(url.cells[1])))
|
||||||
expect(ROUTES.slice(piece.offset, (piece.offset ?? 0) + piece.text.length)).toBe(piece.text)
|
expect(ROUTES.slice(piece.offset, (piece.offset ?? 0) + piece.text.length)).toBe(piece.text)
|
||||||
// 路径里的 user_id 讲的是 update_user 的参数 user_id:高亮参数的各处出现时跟着亮,⌘ + 点击跳到参数
|
// 路径里的 user_id 讲的是 update_user 的参数 user_id:高亮参数的各处出现时跟着亮,⌘ + 点击跳到参数
|
||||||
const param = ROUTES.indexOf('user_id: Int4Path', ROUTES.indexOf('def update_user'))
|
const param = ROUTES.indexOf('user_id: Int4Path', ROUTES.indexOf('def update_user'))
|
||||||
expect(flatSegments(url.cells[1]).find(piece => piece.text === 'user_id')?.refers).toEqual({ startIndex: param, endIndex: param + 'user_id'.length })
|
expect(flatSegments(inlineOf(url.cells[1])).find(piece => piece.text === 'user_id')?.refers).toEqual({ startIndex: param, endIndex: param + 'user_id'.length })
|
||||||
expect(method.cells[1]).toEqual([{ text: 'PUT', role: 'keyword', color: { dark: '#fca130', light: '#b35f00' } }])
|
expect(method.cells[1]).toEqual([{ text: 'PUT', role: 'keyword', color: { dark: '#fca130', light: '#b35f00' } }])
|
||||||
const [ok, forbidden, validation] = rows.find(row => row.label === '响应')?.items ?? []
|
const [ok, forbidden, validation] = rows.find(row => row.label === '响应')?.items ?? []
|
||||||
expect(ok.cells[0]).toEqual([{ text: '200', role: 'number', color: { dark: '#2ea043', light: '#1a7f37' } }])
|
expect(ok.cells[0]).toEqual([{ text: '200', role: 'number', color: { dark: '#2ea043', light: '#1a7f37' } }])
|
||||||
const model = ROUTES.indexOf('response_model=UserDetailResponse', ROUTES.indexOf('@router.put')) + 'response_model='.length
|
const model = ROUTES.indexOf('response_model=UserDetailResponse', ROUTES.indexOf('@router.put')) + 'response_model='.length
|
||||||
expect(ok.cells[1]).toEqual([{ text: 'UserDetailResponse', role: 'type', offset: model }])
|
expect(ok.cells[1]).toEqual([{ text: 'UserDetailResponse', role: 'type', offset: model }])
|
||||||
expect(forbidden.cells[0]).toEqual([{ text: '403', role: 'string' }])
|
expect(forbidden.cells[0]).toEqual([{ text: '403', role: 'string' }])
|
||||||
expect(flatSegments(forbidden.cells[1])).toEqual([{ text: 'Forbidden', role: 'type', offset: ROUTES.indexOf('Forbidden("无权更新') }])
|
expect(flatSegments(inlineOf(forbidden.cells[1]))).toEqual([{ text: 'Forbidden', role: 'type', offset: ROUTES.indexOf('Forbidden("无权更新') }])
|
||||||
expect(flatSegments([...validation.cells[1], ...validation.cells[2]]).map(piece => piece.role)).toEqual(['hint', 'hint'])
|
expect(flatSegments([...inlineOf(validation.cells[1]), ...inlineOf(validation.cells[2])]).map(piece => piece.role)).toEqual(['hint', 'hint'])
|
||||||
// 名字都带下标、指回源码原文
|
// 名字都带下标、指回源码原文
|
||||||
for (const piece of rows.flatMap(segmentsOf).filter(piece => piece.offset !== undefined && piece.role !== 'string' && piece.role !== 'variable'))
|
for (const piece of rows.flatMap(segmentsOf).filter(piece => piece.offset !== undefined && piece.role !== 'string' && piece.role !== 'variable'))
|
||||||
expect(ROUTES.slice(piece.offset, (piece.offset ?? 0) + piece.text.length)).toBe(piece.text)
|
expect(ROUTES.slice(piece.offset, (piece.offset ?? 0) + piece.text.length)).toBe(piece.text)
|
||||||
@@ -316,6 +316,49 @@ async function routerCard(source: string, name: string): Promise<BlockView> {
|
|||||||
return found
|
return found
|
||||||
}
|
}
|
||||||
|
|
||||||
|
describe('fastapi 库规则:说明里的块标签', () => {
|
||||||
|
const SOURCE = [
|
||||||
|
'from fastapi import APIRouter, Depends, Query',
|
||||||
|
'',
|
||||||
|
'router = APIRouter(prefix="/apps")',
|
||||||
|
'',
|
||||||
|
'@router.get("/{app_id}")',
|
||||||
|
'async def get_app(app_id: int, q: str | None = Query(None), db = Depends(get_db)) -> AppOut:',
|
||||||
|
' """取一个应用。',
|
||||||
|
'',
|
||||||
|
' @param app_id 应用 id',
|
||||||
|
' @param q 按名字搜',
|
||||||
|
' @param db 数据库会话',
|
||||||
|
' @param qq 写错的名字',
|
||||||
|
' @return 应用详情',
|
||||||
|
' @throws [AppNotFound] 没有这个应用',
|
||||||
|
' """',
|
||||||
|
].join('\n')
|
||||||
|
|
||||||
|
it('@param 对到请求表(一个参数一行,多一列说明),依赖参数不算对不上;@return 跟在响应后面;@throws 成异常表', async () => {
|
||||||
|
const lines = propertyLines(properties(await route(SOURCE, 'get_app')))
|
||||||
|
expect(lines).toEqual([
|
||||||
|
'名称: get_app',
|
||||||
|
'说明: 取一个应用。',
|
||||||
|
'对不上的参数: 1 个',
|
||||||
|
' qq | 写错的名字',
|
||||||
|
'路由器: router',
|
||||||
|
'异步: 是',
|
||||||
|
'依赖: get_db(db)',
|
||||||
|
'请求: GET /apps/{app_id}',
|
||||||
|
' URL | /apps/{app_id} | ',
|
||||||
|
' 方法 | GET | ',
|
||||||
|
' 路径参数 | app_id: 整数 | 应用 id',
|
||||||
|
' 查询参数 | q: 字符串 | 空 | 按名字搜',
|
||||||
|
'响应: 200 / 422 — 应用详情',
|
||||||
|
' 200 | AppOut | 成功',
|
||||||
|
' 422 | 参数校验失败 | FastAPI 自动返回',
|
||||||
|
'异常: 1 个',
|
||||||
|
' [AppNotFound] | 没有这个应用',
|
||||||
|
])
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
describe('fastapi 库规则:路由器卡片', () => {
|
describe('fastapi 库规则:路由器卡片', () => {
|
||||||
it('luffy 的 router:名称 / 前缀 / 标签,接口表列这份文件里挂在它上面的接口(方法 / 完整路径 / 函数 / docstring 第一行)', async () => {
|
it('luffy 的 router:名称 / 前缀 / 标签,接口表列这份文件里挂在它上面的接口(方法 / 完整路径 / 函数 / docstring 第一行)', async () => {
|
||||||
const card = await routerCard(ROUTES, 'router')
|
const card = await routerCard(ROUTES, 'router')
|
||||||
@@ -339,7 +382,7 @@ describe('fastapi 库规则:路由器卡片', () => {
|
|||||||
expect(getUser.cells[2]).toEqual([{ text: 'get_user', role: 'function', offset: ROUTES.indexOf('get_user(') }])
|
expect(getUser.cells[2]).toEqual([{ text: 'get_user', role: 'function', offset: ROUTES.indexOf('get_user(') }])
|
||||||
const paramOf = (fn: string): number => ROUTES.indexOf('user_id: Int4Path', ROUTES.indexOf(`def ${fn}`))
|
const paramOf = (fn: string): number => ROUTES.indexOf('user_id: Int4Path', ROUTES.indexOf(`def ${fn}`))
|
||||||
for (const [row, fn] of [[getUser, 'get_user'], [updateUser, 'update_user']] as const)
|
for (const [row, fn] of [[getUser, 'get_user'], [updateUser, 'update_user']] as const)
|
||||||
expect(flatSegments(row.cells[1]).find(piece => piece.text === 'user_id')?.refers).toEqual({ startIndex: paramOf(fn), endIndex: paramOf(fn) + 'user_id'.length })
|
expect(flatSegments(inlineOf(row.cells[1])).find(piece => piece.text === 'user_id')?.refers).toEqual({ startIndex: paramOf(fn), endIndex: paramOf(fn) + 'user_id'.length })
|
||||||
expect(updateUser.cells[0]).toEqual([{ text: 'PUT', role: 'keyword', color: { dark: '#fca130', light: '#b35f00' } }])
|
expect(updateUser.cells[0]).toEqual([{ text: 'PUT', role: 'keyword', color: { dark: '#fca130', light: '#b35f00' } }])
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
import type { FunctionBlock, ImportFacts, IndexEntry, Inline, KindIconName, LibraryRule, Node, ParameterFacts, PropertyItem, PropertyRow, RuleCard, RuleContext, Segment, ThemeColor, VariableBlock, WorkspaceIndex } from '../contract'
|
import type { FunctionBlock, ImportFacts, IndexEntry, Inline, KindIconName, LibraryRule, Node, ParameterFacts, PreviewText, PropertyItem, PropertyRow, RuleCard, RuleContext, Segment, ThemeColor, VariableBlock, WorkspaceIndex } from '../contract'
|
||||||
|
import type { ParamDocs } from './docRows'
|
||||||
import type { Call, LibraryNames } from './pythonTree'
|
import type { Call, LibraryNames } from './pythonTree'
|
||||||
|
import { docBodyRows, paramDocs, returnNote, throwsRows, unmatchedParamRows } from './docRows'
|
||||||
import { callOf, genericOf, lastName, libraryName, stringContent, syntaxChildren, unwrapType } from './pythonTree'
|
import { callOf, genericOf, lastName, libraryName, stringContent, syntaxChildren, unwrapType } from './pythonTree'
|
||||||
import { hint, joined, name, range, word } from './segments'
|
import { hint, joined, name, range, word } from './segments'
|
||||||
|
|
||||||
@@ -74,6 +76,8 @@ interface Words {
|
|||||||
no: string
|
no: string
|
||||||
count: (n: number) => string
|
count: (n: number) => string
|
||||||
requestHeads: string[]
|
requestHeads: string[]
|
||||||
|
/** 请求表里有参数写了说明(`@param`)时多出的那一列 */
|
||||||
|
requestDoc: string
|
||||||
responseHeads: string[]
|
responseHeads: string[]
|
||||||
/** 路由器卡片的接口表:方法 | 路径 | 函数 | 说明 */
|
/** 路由器卡片的接口表:方法 | 路径 | 函数 | 说明 */
|
||||||
routeHeads: string[]
|
routeHeads: string[]
|
||||||
@@ -113,6 +117,7 @@ const ZH: Words = {
|
|||||||
no: '否',
|
no: '否',
|
||||||
count: n => `${n} 个`,
|
count: n => `${n} 个`,
|
||||||
requestHeads: ['项', '值'],
|
requestHeads: ['项', '值'],
|
||||||
|
requestDoc: '说明',
|
||||||
responseHeads: ['状态码', '内容', '说明'],
|
responseHeads: ['状态码', '内容', '说明'],
|
||||||
routeHeads: ['方法', '路径', '函数', '说明'],
|
routeHeads: ['方法', '路径', '函数', '说明'],
|
||||||
items: { url: 'URL', method: '方法', path: '路径参数', query: '查询参数', header: '请求头', cookie: 'Cookie', body: '请求体' },
|
items: { url: 'URL', method: '方法', path: '路径参数', query: '查询参数', header: '请求头', cookie: 'Cookie', body: '请求体' },
|
||||||
@@ -325,11 +330,11 @@ function parametersOf(definition: Node): Map<string, Node> {
|
|||||||
return new Map(names.flatMap(param => param?.type === 'identifier' ? [[param.text, param] as const] : []))
|
return new Map(names.flatMap(param => param?.type === 'identifier' ? [[param.text, param] as const] : []))
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 函数 docstring 的第一行(翻译插件取的说明原文),没有 docstring 时没有。 */
|
/** 函数 docstring 正文的第一行(翻译插件读出的说明,块标签已经拿掉),没有 docstring 时没有。 */
|
||||||
function docFirstLine(definition: Node, env: Env): string | undefined {
|
function docFirstLine(definition: Node, env: Env): string | undefined {
|
||||||
const [first] = syntaxChildren(definition.childForFieldName('body'))
|
const [first] = syntaxChildren(definition.childForFieldName('body'))
|
||||||
const [doc] = first?.type === 'expression_statement' ? syntaxChildren(first) : []
|
const [doc] = first?.type === 'expression_statement' ? syntaxChildren(first) : []
|
||||||
const markdown = doc?.type === 'string' ? env.context.doc(doc)?.markdown : undefined
|
const markdown = doc?.type === 'string' ? env.context.doc(doc)?.body?.markdown : undefined
|
||||||
return markdown?.split('\n').map(line => line.trim()).find(line => line !== '')
|
return markdown?.split('\n').map(line => line.trim()).find(line => line !== '')
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -624,9 +629,37 @@ function methodText(method: string): Inline[] {
|
|||||||
return [{ text: method, role: 'keyword', ...(METHOD_COLORS[method] && { color: METHOD_COLORS[method] }) }]
|
return [{ text: method, role: 'keyword', ...(METHOD_COLORS[method] && { color: METHOD_COLORS[method] }) }]
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 请求表的一行:项 | 值。 */
|
/** 请求表的一行:项 | 值,表里有说明一列时再加说明(note,没有说明的是空的)。 */
|
||||||
function requestItem(label: string, value: Inline[]): PropertyItem {
|
function requestItem(label: string, value: Inline[], note?: PreviewText): PropertyItem {
|
||||||
return { cells: [[hint(label)], value] }
|
return { cells: [[hint(label)], value, ...(note ? [note] : [])] }
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 请求表的各项:URL、方法,然后路径参数、查询参数、请求头、Cookie、请求体,有才列。说明里有请求参数对上了 `@param`(docs)时
|
||||||
|
* described:表多一列说明,这几种参数一个参数一行、各带它的说明;没有时同一种参数逗号隔开写在一行。
|
||||||
|
*/
|
||||||
|
function requestItems(url: Inline[], method: string, placed: readonly Placed[], docs: ParamDocs, env: Env): { items: PropertyItem[], described: boolean } {
|
||||||
|
const { words, context } = env
|
||||||
|
const described = placed.some(item => item.place !== 'dependency' && docs.of(item.param.text))
|
||||||
|
const item = (label: string, value: Inline[], note?: PreviewText): PropertyItem => requestItem(label, value, described ? note ?? [] : undefined)
|
||||||
|
const at = (place: Place): Placed[] => placed.filter(one => one.place === place)
|
||||||
|
const listed = (place: Exclude<Place, 'dependency'>): PropertyItem[] => {
|
||||||
|
const found = at(place)
|
||||||
|
if (described)
|
||||||
|
return found.map(one => item(words.items[place], paramsText([one], env), docs.of(one.param.text)))
|
||||||
|
return found.length > 0 ? [item(words.items[place], paramsText(found, env))] : []
|
||||||
|
}
|
||||||
|
const body = at('body')
|
||||||
|
const [only] = body
|
||||||
|
return {
|
||||||
|
items: [
|
||||||
|
item(words.items.url, url),
|
||||||
|
item(words.items.method, methodText(method)),
|
||||||
|
...(['path', 'query', 'header', 'cookie'] as const).flatMap(listed),
|
||||||
|
...(body.length === 1 && only?.type ? [item(words.items.body, context.translate(only.type), docs.of(only.param.text))] : listed('body')),
|
||||||
|
],
|
||||||
|
described,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function routeCard(block: FunctionBlock, route: Route, env: Env): RuleCard {
|
function routeCard(block: FunctionBlock, route: Route, env: Env): RuleCard {
|
||||||
@@ -639,16 +672,12 @@ function routeCard(block: FunctionBlock, route: Route, env: Env): RuleCard {
|
|||||||
const inPath = pathNames([router.prefix, route.path])
|
const inPath = pathNames([router.prefix, route.path])
|
||||||
const placed = facts.parameters.flatMap(parameter => placeOf(parameter, inPath, env) ?? [])
|
const placed = facts.parameters.flatMap(parameter => placeOf(parameter, inPath, env) ?? [])
|
||||||
const at = (place: Place): Placed[] => placed.filter(item => item.place === place)
|
const at = (place: Place): Placed[] => placed.filter(item => item.place === place)
|
||||||
const body = at('body')
|
|
||||||
|
|
||||||
const params = new Map(facts.parameters.flatMap(({ name: param }) => param.type === 'identifier' ? [[param.text, param] as const] : []))
|
const params = new Map(facts.parameters.flatMap(({ name: param }) => param.type === 'identifier' ? [[param.text, param] as const] : []))
|
||||||
const url = [...pathSegments(router.prefix, params, env), ...pathSegments(route.path, params, env)]
|
const url = [...pathSegments(router.prefix, params, env), ...pathSegments(route.path, params, env)]
|
||||||
const request: PropertyItem[] = [
|
const doc = facts.doc && context.doc(facts.doc)
|
||||||
requestItem(words.items.url, url),
|
const docs = paramDocs(doc, facts.parameters.map(parameter => parameter.name.text))
|
||||||
requestItem(words.items.method, methodText(route.method)),
|
const request = requestItems(url, route.method, placed, docs, env)
|
||||||
...(['path', 'query', 'header', 'cookie'] as const).flatMap(place => at(place).length > 0 ? [requestItem(words.items[place], paramsText(at(place), env))] : []),
|
|
||||||
...(body.length > 0 ? [requestItem(words.items.body, body.length === 1 && body[0].type ? context.translate(body[0].type) : paramsText(body, env))] : []),
|
|
||||||
]
|
|
||||||
|
|
||||||
const statuses = new Statuses()
|
const statuses = new Statuses()
|
||||||
const success = call.keywords.get('status_code')
|
const success = call.keywords.get('status_code')
|
||||||
@@ -668,7 +697,6 @@ function routeCard(block: FunctionBlock, route: Route, env: Env): RuleCard {
|
|||||||
if (placed.some(item => item.place !== 'dependency'))
|
if (placed.some(item => item.place !== 'dependency'))
|
||||||
statuses.add(numericStatus('422'), [hint(words.validation)], [hint(words.automatic)])
|
statuses.add(numericStatus('422'), [hint(words.validation)], [hint(words.automatic)])
|
||||||
|
|
||||||
const doc = facts.doc && context.doc(facts.doc)
|
|
||||||
const tags = [...router.tags, ...listItems(call.keywords.get('tags'))].map(tag => tagText(tag, env))
|
const tags = [...router.tags, ...listItems(call.keywords.get('tags'))].map(tag => tagText(tag, env))
|
||||||
const decorators = facts.decorators.filter(other => other !== route.decorator)
|
const decorators = facts.decorators.filter(other => other !== route.decorator)
|
||||||
const dependencies = [
|
const dependencies = [
|
||||||
@@ -680,7 +708,8 @@ function routeCard(block: FunctionBlock, route: Route, env: Env): RuleCard {
|
|||||||
]
|
]
|
||||||
const rows: PropertyRow[] = [
|
const rows: PropertyRow[] = [
|
||||||
...(facts.name ? [{ label: labels.name, value: [name(facts.name, 'function')] }] : []),
|
...(facts.name ? [{ label: labels.name, value: [name(facts.name, 'function')] }] : []),
|
||||||
...(doc ? [{ label: labels.doc, value: doc }] : []),
|
...docBodyRows(doc, labels.doc),
|
||||||
|
...unmatchedParamRows(docs.unmatched),
|
||||||
{ label: labels.router, value: [name(router.target, 'variable')] },
|
{ label: labels.router, value: [name(router.target, 'variable')] },
|
||||||
...(tags.length > 0 ? [{ label: labels.tags, value: joined(tags, separator) }] : []),
|
...(tags.length > 0 ? [{ label: labels.tags, value: joined(tags, separator) }] : []),
|
||||||
{ label: labels.async, value: [word(facts.async ? words.yes : words.no)] },
|
{ label: labels.async, value: [word(facts.async ? words.yes : words.no)] },
|
||||||
@@ -689,10 +718,11 @@ function routeCard(block: FunctionBlock, route: Route, env: Env): RuleCard {
|
|||||||
{
|
{
|
||||||
label: labels.request,
|
label: labels.request,
|
||||||
value: [hint(`${route.method} ${textOf(url)}`)],
|
value: [hint(`${route.method} ${textOf(url)}`)],
|
||||||
columns: words.requestHeads,
|
columns: request.described ? [...words.requestHeads, words.requestDoc] : words.requestHeads,
|
||||||
items: request,
|
items: request.items,
|
||||||
},
|
},
|
||||||
responsesRow(labels.responses, statuses, env),
|
{ ...responsesRow(labels.responses, statuses, env), ...returnNote(doc) },
|
||||||
|
...throwsRows(doc),
|
||||||
]
|
]
|
||||||
return {
|
return {
|
||||||
kind: KIND,
|
kind: KIND,
|
||||||
@@ -712,7 +742,7 @@ function routerCard(block: VariableBlock, router: Router, routes: readonly Route
|
|||||||
const { context, words } = env
|
const { context, words } = env
|
||||||
const { labels } = words
|
const { labels } = words
|
||||||
const separator = hint(words.listSeparator)
|
const separator = hint(words.listSeparator)
|
||||||
const doc = block.facts.doc && context.doc(block.facts.doc)
|
const doc = block.facts.doc && context.doc(block.facts.doc)?.body
|
||||||
const tags = router.tags.map(tag => tagText(tag, env))
|
const tags = router.tags.map(tag => tagText(tag, env))
|
||||||
const dependencies = listedDependencies(router.dependencies, env)
|
const dependencies = listedDependencies(router.dependencies, env)
|
||||||
const statuses = new Statuses()
|
const statuses = new Statuses()
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { fileAttributes } from '../attributes'
|
|||||||
import { toBlockViews } from '../blockView'
|
import { toBlockViews } from '../blockView'
|
||||||
import { diffBlockTrees } from '../diff/blockDiff'
|
import { diffBlockTrees } from '../diff/blockDiff'
|
||||||
import { createPythonAdapter } from '../languages/python'
|
import { createPythonAdapter } from '../languages/python'
|
||||||
import { flatSegments, propertyLines, readFixture, WASM_DIR, withBlocks } from '../testSupport'
|
import { flatSegments, inlineOf, propertyLines, readFixture, WASM_DIR, withBlocks } from '../testSupport'
|
||||||
import { pythonZhTranslator } from '../translators/pythonZh'
|
import { pythonZhTranslator } from '../translators/pythonZh'
|
||||||
import { celeryRule } from './celery'
|
import { celeryRule } from './celery'
|
||||||
import { dataclassRule } from './dataclass'
|
import { dataclassRule } from './dataclass'
|
||||||
@@ -46,7 +46,7 @@ function named(list: BlockView[], name: string): BlockView {
|
|||||||
function linkedSegments(rows: PropertyRow[]): Segment[] {
|
function linkedSegments(rows: PropertyRow[]): Segment[] {
|
||||||
const pieces = rows.flatMap(row => [
|
const pieces = rows.flatMap(row => [
|
||||||
...('markdown' in row.value ? [] : row.value),
|
...('markdown' in row.value ? [] : row.value),
|
||||||
...(row.items ?? []).flatMap(item => item.cells.flat()),
|
...(row.items ?? []).flatMap(item => item.cells.flatMap(inlineOf)),
|
||||||
])
|
])
|
||||||
return flatSegments(pieces).filter(segment => segment.offset !== undefined)
|
return flatSegments(pieces).filter(segment => segment.offset !== undefined)
|
||||||
}
|
}
|
||||||
@@ -260,7 +260,8 @@ describe('pydantic 库规则(Python)', () => {
|
|||||||
expect(Object.keys(added ?? {}).filter(index => added?.[Number(index)].added)).toEqual(['7'])
|
expect(Object.keys(added ?? {}).filter(index => added?.[Number(index)].added)).toEqual(['7'])
|
||||||
|
|
||||||
const traceFields = properties(trace).findIndex(row => row.label === '字段')
|
const traceFields = properties(trace).findIndex(row => row.label === '字段')
|
||||||
const cell = traceChange.marks?.rows?.changed[traceFields].items?.changed[2].cells?.[2]
|
const marked = traceChange.marks?.rows?.changed[traceFields].items?.changed[2].cells?.[2]
|
||||||
|
const cell = marked && 'inline' in marked ? marked.inline : undefined
|
||||||
expect(cell?.map(part => part.change)).toEqual(['del', 'add'])
|
expect(cell?.map(part => part.change)).toEqual(['del', 'add'])
|
||||||
expect(flatSegments(cell?.[0].segments ?? []).map(segment => segment.text).join('')).toBe('空')
|
expect(flatSegments(cell?.[0].segments ?? []).map(segment => segment.text).join('')).toBe('空')
|
||||||
expect(flatSegments(cell?.[1].segments ?? []).map(segment => segment.text).join('')).toBe('""')
|
expect(flatSegments(cell?.[1].segments ?? []).map(segment => segment.text).join('')).toBe('""')
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import type { Access, ClassBlock, CodeBlock, ImportFacts, IndexEntry, Inline, KindIconName, LibraryRule, Node, PropertyItem, PropertyRow, RuleCard, RuleContext, ThemeColor, WorkspaceIndex } from '../contract'
|
import type { Access, ClassBlock, CodeBlock, ImportFacts, IndexEntry, Inline, KindIconName, LibraryRule, Node, PreviewText, PropertyItem, PropertyRow, RuleCard, RuleContext, ThemeColor, WorkspaceIndex } from '../contract'
|
||||||
|
import type { ParamDocs } from './docRows'
|
||||||
import type { Call, LibraryNames } from './pythonTree'
|
import type { Call, LibraryNames } from './pythonTree'
|
||||||
import { annotatedAssignments, memberDocs, withLeadingComments } from './children'
|
import { annotatedAssignments, memberDocs, withLeadingComments } from './children'
|
||||||
|
import { docBodyRows, paramDocs, throwsRows, unmatchedParamRows } from './docRows'
|
||||||
import { aliasesOf, callOf, canonicalName, genericOf, libraryName, libraryNames, stringValue, syntaxChildren, unwrapType } from './pythonTree'
|
import { aliasesOf, callOf, canonicalName, genericOf, libraryName, libraryNames, stringValue, syntaxChildren, unwrapType } from './pythonTree'
|
||||||
import { hint, joined, name, plain, range, word } from './segments'
|
import { hint, joined, name, plain, range, word } from './segments'
|
||||||
|
|
||||||
@@ -328,26 +330,26 @@ function defaultCell(field: Field, call: Call | undefined, env: Env): Inline[] {
|
|||||||
return joined(parts, hint(' · '))
|
return joined(parts, hint(' · '))
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 说明:`Field(description=...)` 优先;没有就用字段上方注释、行尾注释、属性 docstring。 */
|
/** 说明:`Field(description=...)` 优先;没有就用字段上方注释、行尾注释、属性 docstring;都没有时是类的说明里讲它的 `@param`。 */
|
||||||
function docCell(field: Field, children: readonly CodeBlock[], call: Call | undefined, env: Env): Inline[] {
|
function docCell(field: Field, children: readonly CodeBlock[], call: Call | undefined, docs: ParamDocs, env: Env): PreviewText {
|
||||||
const description = call?.keywords.get('description')
|
const description = call?.keywords.get('description')
|
||||||
const literal = stringValue(description)
|
const literal = stringValue(description)
|
||||||
if (literal)
|
if (literal)
|
||||||
return [{ text: literal, role: 'comment' }]
|
return [{ text: literal, role: 'comment' }]
|
||||||
if (description)
|
if (description)
|
||||||
return env.context.translate(description)
|
return env.context.translate(description)
|
||||||
const text = memberDocs(children, field.block).map(node => env.context.doc(node)?.markdown).filter(Boolean).join(' ')
|
const text = memberDocs(children, field.block).map(node => env.context.doc(node)?.body?.markdown).filter(Boolean).join(' ')
|
||||||
return text ? [{ text, role: 'comment' }] : []
|
return text ? [{ text, role: 'comment' }] : docs.of(field.name.text) ?? []
|
||||||
}
|
}
|
||||||
|
|
||||||
function fieldItem(field: Field, children: readonly CodeBlock[], env: Env): PropertyItem {
|
function fieldItem(field: Field, children: readonly CodeBlock[], docs: ParamDocs, env: Env): PropertyItem {
|
||||||
const call = namedCall(field.value, 'Field', env.names)
|
const call = namedCall(field.value, 'Field', env.names)
|
||||||
return {
|
return {
|
||||||
cells: [
|
cells: [
|
||||||
[name(field.name, 'variable')],
|
[name(field.name, 'variable')],
|
||||||
env.context.translate(field.annotation),
|
env.context.translate(field.annotation),
|
||||||
defaultCell(field, call, env),
|
defaultCell(field, call, env),
|
||||||
docCell(field, children, call, env),
|
docCell(field, children, call, docs, env),
|
||||||
],
|
],
|
||||||
defines: range(field.name),
|
defines: range(field.name),
|
||||||
}
|
}
|
||||||
@@ -360,10 +362,12 @@ function modelCard(block: ClassBlock, env: Env): RuleCard {
|
|||||||
const config = configOf(children)
|
const config = configOf(children)
|
||||||
const fields = fieldsOf(children, env)
|
const fields = fieldsOf(children, env)
|
||||||
const doc = facts.doc && context.doc(facts.doc)
|
const doc = facts.doc && context.doc(facts.doc)
|
||||||
|
const docs = paramDocs(doc, fields.map(field => field.name.text))
|
||||||
const options = optionsText(config?.value, env)
|
const options = optionsText(config?.value, env)
|
||||||
const rows: PropertyRow[] = [
|
const rows: PropertyRow[] = [
|
||||||
...(facts.name ? [{ label: labels.name, value: [name(facts.name, 'type')] }] : []),
|
...(facts.name ? [{ label: labels.name, value: [name(facts.name, 'type')] }] : []),
|
||||||
...(doc ? [{ label: labels.doc, value: doc }] : []),
|
...docBodyRows(doc, labels.doc),
|
||||||
|
...unmatchedParamRows(docs.unmatched),
|
||||||
...(facts.access ? [{ label: labels.access, value: [{ text: words.access[facts.access], role: 'keyword' as const }] }] : []),
|
...(facts.access ? [{ label: labels.access, value: [{ text: words.access[facts.access], role: 'keyword' as const }] }] : []),
|
||||||
...(facts.bases.length > 0 ? [{ label: labels.bases, value: joined(facts.bases.map(base => context.translate(base.value)), plain(', ')) }] : []),
|
...(facts.bases.length > 0 ? [{ label: labels.bases, value: joined(facts.bases.map(base => context.translate(base.value)), plain(', ')) }] : []),
|
||||||
...(options.length > 0 ? [{ label: labels.options, value: options }] : []),
|
...(options.length > 0 ? [{ label: labels.options, value: options }] : []),
|
||||||
@@ -372,9 +376,10 @@ function modelCard(block: ClassBlock, env: Env): RuleCard {
|
|||||||
label: labels.fields,
|
label: labels.fields,
|
||||||
value: [hint(words.count(fields.length))],
|
value: [hint(words.count(fields.length))],
|
||||||
columns: words.heads,
|
columns: words.heads,
|
||||||
items: fields.map(field => fieldItem(field, children, env)),
|
items: fields.map(field => fieldItem(field, children, docs, env)),
|
||||||
}]
|
}]
|
||||||
: []),
|
: []),
|
||||||
|
...throwsRows(doc),
|
||||||
]
|
]
|
||||||
return {
|
return {
|
||||||
kind: KIND,
|
kind: KIND,
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { describe, expect, it } from 'vitest'
|
|||||||
import { toBlockViews } from '../blockView'
|
import { toBlockViews } from '../blockView'
|
||||||
import { diffBlockTrees } from '../diff/blockDiff'
|
import { diffBlockTrees } from '../diff/blockDiff'
|
||||||
import { createTypeScriptAdapter } from '../languages/typescript'
|
import { createTypeScriptAdapter } from '../languages/typescript'
|
||||||
import { propertyLines, readFixture, segmentText, viewLines, WASM_DIR, withBlocks } from '../testSupport'
|
import { inlineOf, previewText, propertyLines, readFixture, viewLines, WASM_DIR, withBlocks } from '../testSupport'
|
||||||
import { typescriptZhTranslator } from '../translators/typescriptZh'
|
import { typescriptZhTranslator } from '../translators/typescriptZh'
|
||||||
import { reactRule } from './react'
|
import { reactRule } from './react'
|
||||||
|
|
||||||
@@ -186,6 +186,71 @@ describe('react 库规则:构造参数', () => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe('react 库规则:说明里的块标签', () => {
|
||||||
|
const SOURCE = [
|
||||||
|
'interface BadgeProps {',
|
||||||
|
' /** 显示的字 */',
|
||||||
|
' label: string',
|
||||||
|
' tone?: string',
|
||||||
|
'}',
|
||||||
|
'',
|
||||||
|
'/**',
|
||||||
|
' * 小徽标。',
|
||||||
|
' *',
|
||||||
|
' * @param props 徽标的属性',
|
||||||
|
' * @param props.tone 颜色',
|
||||||
|
' * @param label 不会盖过字段自己的注释',
|
||||||
|
' * @param size 写错的名字',
|
||||||
|
' * @throws [RangeError] 颜色不认识',
|
||||||
|
' */',
|
||||||
|
'export function Badge({ label, tone = \'gray\' }: BadgeProps) {',
|
||||||
|
' /**',
|
||||||
|
' * 点了复制。',
|
||||||
|
' * @param event 点击事件',
|
||||||
|
' * @return 复制成没成',
|
||||||
|
' */',
|
||||||
|
' function onCopy(event: MouseEvent) {',
|
||||||
|
' return true',
|
||||||
|
' }',
|
||||||
|
' return <span onClick={onCopy}>{label}</span>',
|
||||||
|
'}',
|
||||||
|
].join('\n')
|
||||||
|
|
||||||
|
it('组件:字段没注释时说明格子用 @param(props.tone 也算),讲整个参数的跟在构造参数后面,对不上的列在说明下面,@throws 成异常表', async () => {
|
||||||
|
const view = await card(SOURCE, 'Badge')
|
||||||
|
const rows = properties(view)
|
||||||
|
expect(lines(view)).toEqual([
|
||||||
|
'名称: Badge ⟦具名导出|⟧',
|
||||||
|
'说明: 小徽标。',
|
||||||
|
'对不上的参数: 1 个',
|
||||||
|
' size | 写错的名字',
|
||||||
|
'构造参数: 2 个 · 类型 BadgeProps — 徽标的属性',
|
||||||
|
' label | 字符串 | — | 是 | 显示的字',
|
||||||
|
' tone | 字符串 | \'gray\' | 否 | 颜色',
|
||||||
|
'异常: 1 个',
|
||||||
|
' [RangeError] | 颜色不认识',
|
||||||
|
])
|
||||||
|
const props = rows.find(row => row.label === '构造参数')
|
||||||
|
expect(props?.note).toEqual({ markdown: '徽标的属性' })
|
||||||
|
expect(props?.items?.map(item => item.cells[4])).toEqual([[{ text: '显示的字', role: 'comment' }], { markdown: '颜色' }])
|
||||||
|
expect(rows.find(row => row.label === '对不上的参数')?.items?.[0].cells[0]).toEqual([{ text: 'size', role: 'hint', tooltip: '没有这个参数' }])
|
||||||
|
expect(rows.find(row => row.label === '异常')?.items?.[0].cells).toEqual([{ markdown: '[RangeError]' }, { markdown: '颜色不认识' }])
|
||||||
|
expect(lines(view).join('\n')).not.toMatch(/@param|@throws/)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('事件处理函数:说明、@param 时参数排成带说明的表、@return 一行', async () => {
|
||||||
|
const handler = flatten(await views(SOURCE)).find(view => view.kind === 'react.handler')
|
||||||
|
expect(lines(handler)).toEqual([
|
||||||
|
'名称: onCopy',
|
||||||
|
'说明: 点了复制。',
|
||||||
|
'参数: 1 个',
|
||||||
|
' event | MouseEvent | — | 点击事件',
|
||||||
|
'返回: — 复制成没成',
|
||||||
|
'绑定事件: <span> 的 onClick',
|
||||||
|
])
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
describe('react 库规则:组件体一块一张卡片(luffy 的 PlanChecklist,原型「对照」)', () => {
|
describe('react 库规则:组件体一块一张卡片(luffy 的 PlanChecklist,原型「对照」)', () => {
|
||||||
it('组件卡片的行只有名称 / 说明 / 构造参数,组件体的语句照源码顺序是它的子块', async () => {
|
it('组件卡片的行只有名称 / 说明 / 构造参数,组件体的语句照源码顺序是它的子块', async () => {
|
||||||
const view = await card(PLAN, 'PlanChecklist')
|
const view = await card(PLAN, 'PlanChecklist')
|
||||||
@@ -499,10 +564,10 @@ describe('react 库规则:渲染大纲', () => {
|
|||||||
' ⟦|当 platform 全等 "windows" 时⟧ WindowControls',
|
' ⟦|当 platform 全等 "windows" 时⟧ WindowControls',
|
||||||
])
|
])
|
||||||
const [div] = outlineItems(view)
|
const [div] = outlineItems(view)
|
||||||
const [tagSegment, , chip] = div.cells[0]
|
const [tagSegment, , chip] = inlineOf(div.cells[0] ?? [])
|
||||||
expect(tagSegment).toEqual({ text: 'div', role: 'plain', color: { dark: '#808080', light: '#6f6f6f' } })
|
expect(tagSegment).toEqual({ text: 'div', role: 'plain', color: { dark: '#808080', light: '#6f6f6f' } })
|
||||||
expect(chip).toMatchObject({ style: 'classes', detail: 'flex h-10 shrink-0 select-none items-center border-b border-border bg-background' })
|
expect(chip).toMatchObject({ style: 'classes', detail: 'flex h-10 shrink-0 select-none items-center border-b border-border bg-background' })
|
||||||
const controls = div.items?.[2].cells[0].find(piece => 'role' in piece && piece.text === 'WindowControls')
|
const controls = inlineOf(div.items?.[2].cells[0] ?? []).find(piece => 'role' in piece && piece.text === 'WindowControls')
|
||||||
expect(controls).toMatchObject({ role: 'type', color: { dark: '#61dafb', light: '#0a7ea4' } })
|
expect(controls).toMatchObject({ role: 'type', color: { dark: '#61dafb', light: '#0a7ea4' } })
|
||||||
expect(controls && 'role' in controls && TITLEBAR.slice(controls.offset, (controls.offset ?? 0) + 14)).toBe('WindowControls')
|
expect(controls && 'role' in controls && TITLEBAR.slice(controls.offset, (controls.offset ?? 0) + 14)).toBe('WindowControls')
|
||||||
})
|
})
|
||||||
@@ -528,8 +593,8 @@ describe('react 库规则:渲染大纲', () => {
|
|||||||
' ReactMarkdown remarkPlugins=[remarkGfm] components=对象:h2、a {markdown}',
|
' ReactMarkdown remarkPlugins=[remarkGfm] components=对象:h2、a {markdown}',
|
||||||
' ⟦|否⟧ p ⟦样式 2 个类|⟧ {DOC_TITLE[doc]}内容尚未配置, 请在管理后台“站点配置”中填写。',
|
' ⟦|否⟧ p ⟦样式 2 个类|⟧ {DOC_TITLE[doc]}内容尚未配置, 请在管理后台“站点配置”中填写。',
|
||||||
])
|
])
|
||||||
const link = findItem(outlineItems(view), item => segmentText(item.cells[0]).startsWith('Link'))
|
const link = findItem(outlineItems(view), item => previewText(item.cells[0] ?? []).startsWith('Link'))
|
||||||
const chip = link?.cells[0].find(piece => !('role' in piece) && piece.style === 'classes')
|
const chip = inlineOf(link?.cells[0] ?? []).find(piece => !('role' in piece) && piece.style === 'classes')
|
||||||
expect(chip && !('role' in chip) && chip.detail).toBe([
|
expect(chip && !('role' in chip) && chip.detail).toBe([
|
||||||
'flex items-center gap-1.5 px-4 md:px-6 py-1.5 rounded-full text-[13px] md:text-[14px] font-semibold transition-all duration-300',
|
'flex items-center gap-1.5 px-4 md:px-6 py-1.5 rounded-full text-[13px] md:text-[14px] font-semibold transition-all duration-300',
|
||||||
'doc 全等 "terms" 时:bg-surface text-foreground shadow-sm',
|
'doc 全等 "terms" 时:bg-surface text-foreground shadow-sm',
|
||||||
@@ -581,8 +646,8 @@ describe('react 库规则:渲染大纲', () => {
|
|||||||
])
|
])
|
||||||
expect(text).toContain(' SettingsRow label="规格" value=deployment?.instance_type_code ?? "—" note="重新发布时可换档"')
|
expect(text).toContain(' SettingsRow label="规格" value=deployment?.instance_type_code ?? "—" note="重新发布时可换档"')
|
||||||
expect(text.join('\n')).not.toContain('text-primary underline')
|
expect(text.join('\n')).not.toContain('text-primary underline')
|
||||||
const anchor = findItem(outlineItems(view), item => item.cells[0].some(piece => 'role' in piece && piece.text === 'a'))
|
const anchor = findItem(outlineItems(view), item => inlineOf(item.cells[0] ?? []).some(piece => 'role' in piece && piece.text === 'a'))
|
||||||
const chip = anchor?.cells[0].find(piece => !('role' in piece) && piece.style === 'classes')
|
const chip = inlineOf(anchor?.cells[0] ?? []).find(piece => !('role' in piece) && piece.style === 'classes')
|
||||||
expect(chip && !('role' in chip) && chip.detail).toBe('text-primary underline underline-offset-2')
|
expect(chip && !('role' in chip) && chip.detail).toBe('text-primary underline underline-offset-2')
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
import type { CodeBlock, Inline, KindIconName, LibraryRule, Node, PropertyItem, PropertyRow, RuleCard, RuleContext, SegmentGroup, SourceRange, ThemeColor, TokenRole, WorkspaceIndex } from '../contract'
|
import type { CodeBlock, Doc, Inline, KindIconName, LibraryRule, Markdown, Node, PreviewText, PropertyItem, PropertyRow, RuleCard, RuleContext, SegmentGroup, SourceRange, ThemeColor, TokenRole, WorkspaceIndex } from '../contract'
|
||||||
|
import type { ParamDocs } from './docRows'
|
||||||
import type { BindingFacts, BodyFacts, ClassFacts, ComponentFacts, DerivedFacts, EffectFacts, FieldFacts, FieldText, HandlerFacts, OutlineNode, Place, PropsFacts, ReadFacts, RefFacts, StateFacts } from './reactTree'
|
import type { BindingFacts, BodyFacts, ClassFacts, ComponentFacts, DerivedFacts, EffectFacts, FieldFacts, FieldText, HandlerFacts, OutlineNode, Place, PropsFacts, ReadFacts, RefFacts, StateFacts } from './reactTree'
|
||||||
|
import { docBodyRows, paramDocCell, paramDocs, returnNote, throwsRows, unmatchedParamRows } from './docRows'
|
||||||
import { argumentsOf, attributeName, attributeValue, bodyOf, clientDirective, commentText, componentAt, componentOf, componentOfFunction, componentsIn, declaredTypes, enclosingComponent, enclosingFunction, isComponentTag, isFunction, isJsx, named, outlineOf, parametersOf, placesOf, propsOf, sameNode, statementsOf, stringContent, unwrap } from './reactTree'
|
import { argumentsOf, attributeName, attributeValue, bodyOf, clientDirective, commentText, componentAt, componentOf, componentOfFunction, componentsIn, declaredTypes, enclosingComponent, enclosingFunction, isComponentTag, isFunction, isJsx, named, outlineOf, parametersOf, placesOf, propsOf, sameNode, statementsOf, stringContent, unwrap } from './reactTree'
|
||||||
import { hint, joined, name, plain, range } from './segments'
|
import { hint, joined, name, plain, range } from './segments'
|
||||||
|
|
||||||
@@ -63,7 +65,9 @@ const LOOKS: Record<CardKind, { color: ThemeColor, icon: KindIconName }> = {
|
|||||||
interface Words {
|
interface Words {
|
||||||
/** 各种卡片的名字,筛选栏里也叫这个 */
|
/** 各种卡片的名字,筛选栏里也叫这个 */
|
||||||
kinds: Record<CardKind, string>
|
kinds: Record<CardKind, string>
|
||||||
labels: Record<'name' | 'doc' | 'props' | 'initial' | 'updates' | 'uses' | 'variable' | 'hook' | 'meaning' | 'expression' | 'deps' | 'trigger' | 'cleanup' | 'parameters' | 'events' | 'outline', string>
|
labels: Record<'name' | 'doc' | 'props' | 'initial' | 'updates' | 'uses' | 'variable' | 'hook' | 'meaning' | 'expression' | 'deps' | 'trigger' | 'cleanup' | 'parameters' | 'returns' | 'events' | 'outline', string>
|
||||||
|
/** 事件处理函数的说明里有参数对上了 `@param` 时,参数排成表:名称 | 类型 | 默认值 | 说明 */
|
||||||
|
parameterHeads: string[]
|
||||||
/** 触发时机:a、b 变化时,仅挂载时,每次渲染后;useLayoutEffect 在浏览器绘制前跑 */
|
/** 触发时机:a、b 变化时,仅挂载时,每次渲染后;useLayoutEffect 在浏览器绘制前跑 */
|
||||||
changed: string
|
changed: string
|
||||||
mount: string
|
mount: string
|
||||||
@@ -160,9 +164,11 @@ const ZH: Words = {
|
|||||||
trigger: '触发时机',
|
trigger: '触发时机',
|
||||||
cleanup: '清理函数',
|
cleanup: '清理函数',
|
||||||
parameters: '参数',
|
parameters: '参数',
|
||||||
|
returns: '返回',
|
||||||
events: '绑定事件',
|
events: '绑定事件',
|
||||||
outline: 'JSX 结构大纲',
|
outline: 'JSX 结构大纲',
|
||||||
},
|
},
|
||||||
|
parameterHeads: ['名称', '类型', '默认值', '说明'],
|
||||||
changed: '变化时',
|
changed: '变化时',
|
||||||
mount: '仅挂载时',
|
mount: '仅挂载时',
|
||||||
afterRender: '每次渲染后',
|
afterRender: '每次渲染后',
|
||||||
@@ -290,10 +296,21 @@ function componentName(component: ComponentFacts, { words }: Env): PropertyRow {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 说明:组件上面的 JSDoc。 */
|
/**
|
||||||
function docRows(component: ComponentFacts, { context, words }: Env): PropertyRow[] {
|
* 组件说明里的 `@param` 对到构造参数的各项:属性名对上的进说明一格,写成 `props.title`(属性参数名.属性名)的也算;
|
||||||
const doc = component.doc && context.doc(component.doc)
|
* 只写属性参数名的(`@param props`)讲的是整个参数,跟在构造参数那一行后面(whole)。属性参数是解构写的、没有名字时,
|
||||||
return doc ? [{ label: words.labels.doc, value: doc }] : []
|
* 说明里点号前面写的那个名字(`@param props.title` 的 props)当它的名字。
|
||||||
|
*/
|
||||||
|
function propDocs(doc: Doc | undefined, props: PropsFacts, entries: readonly PropEntry[]): { docs: ParamDocs, whole?: Markdown } {
|
||||||
|
const written = doc?.params ?? []
|
||||||
|
const dotted = written.flatMap(param => param.name.includes('.') ? [param.name.slice(0, param.name.indexOf('.'))] : [])
|
||||||
|
const holder = props.parameter?.type === 'identifier' ? props.parameter.text : dotted[0]
|
||||||
|
const prefix = holder && `${holder}.`
|
||||||
|
const whole = written.find(param => param.name === holder)?.text
|
||||||
|
const params = written
|
||||||
|
.filter(param => param.name !== holder)
|
||||||
|
.map(param => prefix && param.name.startsWith(prefix) ? { ...param, name: param.name.slice(prefix.length) } : param)
|
||||||
|
return { docs: paramDocs(doc && { ...doc, params }, entries.map(entry => entry.key)), ...(whole?.markdown && { whole }) }
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── 组件:构造参数 ───────────────────────────────────────────────────────────
|
// ─── 组件:构造参数 ───────────────────────────────────────────────────────────
|
||||||
@@ -393,19 +410,20 @@ function propRequired(entry: PropEntry, words: Words): Inline[] {
|
|||||||
return [hint(words.nothing)]
|
return [hint(words.nothing)]
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 说明一格:字段上面的 JSDoc / 注释。 */
|
/** 说明一格:字段上面的 JSDoc / 注释;字段没写时是组件说明里讲它的 `@param`。 */
|
||||||
function propDoc(entry: PropEntry): Inline[] {
|
function propDoc(entry: PropEntry, docs: ParamDocs): PreviewText {
|
||||||
const text = entry.field ? commentText(entry.field.docs) : entry.text?.doc
|
const text = entry.field ? commentText(entry.field.docs) : entry.text?.doc
|
||||||
return text ? [{ text: text.replace(/\s*\n\s*/g, ' '), role: 'comment' }] : []
|
return text ? [{ text: text.replace(/\s*\n\s*/g, ' '), role: 'comment' }] : docs.of(entry.key) ?? []
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 构造参数:解构的每个属性一行(名称 / 类型 / 默认值 / 必填,有属性带说明时多一列说明);属性类型是一个名字时「构造参数」后面写出它。 */
|
/**
|
||||||
function propsRow(component: ComponentFacts, env: Env): PropertyRow {
|
* 构造参数:解构的每个属性一行(名称 / 类型 / 默认值 / 必填,有属性带说明时多一列说明);属性类型是一个名字时「构造参数」后面写出它,
|
||||||
|
* 组件说明里讲整个属性参数的 `@param` 跟在这一行的值后面。
|
||||||
|
*/
|
||||||
|
function propsRow(props: PropsFacts, entries: readonly PropEntry[], written: { docs: ParamDocs, whole?: Markdown }, env: Env): PropertyRow {
|
||||||
const { context, words } = env
|
const { context, words } = env
|
||||||
const props = propsOf(component)
|
const docs = entries.map(entry => propDoc(entry, written.docs))
|
||||||
const entries = propEntries(props, env)
|
const described = docs.some(doc => 'markdown' in doc || doc.length > 0)
|
||||||
const docs = entries.map(propDoc)
|
|
||||||
const described = docs.some(doc => doc.length > 0)
|
|
||||||
const items = entries.map((entry, index): PropertyItem => {
|
const items = entries.map((entry, index): PropertyItem => {
|
||||||
const defines = entry.binding?.name ?? entry.field?.name
|
const defines = entry.binding?.name ?? entry.field?.name
|
||||||
return {
|
return {
|
||||||
@@ -422,13 +440,28 @@ function propsRow(component: ComponentFacts, env: Env): PropertyRow {
|
|||||||
return {
|
return {
|
||||||
label: words.labels.props,
|
label: words.labels.props,
|
||||||
value: [hint(words.count(items.length)), ...(props.from ? [hint(words.typed), name(props.from, 'type')] : [])],
|
value: [hint(words.count(items.length)), ...(props.from ? [hint(words.typed), name(props.from, 'type')] : [])],
|
||||||
|
...(written.whole && { note: written.whole }),
|
||||||
...(items.length > 0 && { items, columns: described ? [...words.propHeads, words.propDoc] : words.propHeads }),
|
...(items.length > 0 && { items, columns: described ? [...words.propHeads, words.propDoc] : words.propHeads }),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 组件卡片:名称 / 说明 / 构造参数。组件体的语句照常是子块;直接返回 JSX 的箭头函数没有子块,它渲染的大纲写在卡片里。 */
|
/**
|
||||||
|
* 组件卡片:名称 / 说明 / 对不上的参数 / 构造参数 / 异常(说明里的块标签拆开放,见 propDocs)。组件体的语句照常是子块;
|
||||||
|
* 直接返回 JSX 的箭头函数没有子块,它渲染的大纲写在卡片里。
|
||||||
|
*/
|
||||||
function componentCard(component: ComponentFacts, env: Env): RuleCard {
|
function componentCard(component: ComponentFacts, env: Env): RuleCard {
|
||||||
const rows = [componentName(component, env), ...docRows(component, env), propsRow(component, env)]
|
const { context, words } = env
|
||||||
|
const doc = component.doc && context.doc(component.doc)
|
||||||
|
const props = propsOf(component)
|
||||||
|
const entries = propEntries(props, env)
|
||||||
|
const written = propDocs(doc, props, entries)
|
||||||
|
const rows = [
|
||||||
|
componentName(component, env),
|
||||||
|
...docBodyRows(doc, words.labels.doc),
|
||||||
|
...unmatchedParamRows(written.docs.unmatched),
|
||||||
|
propsRow(props, entries, written, env),
|
||||||
|
...throwsRows(doc),
|
||||||
|
]
|
||||||
if (component.body.type !== 'statement_block')
|
if (component.body.type !== 'statement_block')
|
||||||
rows.push(outlineRow(component.body, env))
|
rows.push(outlineRow(component.body, env))
|
||||||
return card('component', rows, env, { defines: range(component.name) })
|
return card('component', rows, env, { defines: range(component.name) })
|
||||||
@@ -725,17 +758,48 @@ function eventsText(handler: HandlerFacts, analysis: Analysis, env: Env): Inline
|
|||||||
return [hint(words.nothing)]
|
return [hint(words.nothing)]
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 事件处理函数:名称 / 参数 / 绑定事件。函数里的语句照常是这张卡片的子块。 */
|
/**
|
||||||
function handlerCard(handler: HandlerFacts, analysis: Analysis, env: Env): RuleCard {
|
* 事件处理函数:名称 / 说明 / 对不上的参数 / 参数 / 返回(说明里写了 `@return` 才有)/ 异常 / 绑定事件。参数逗号隔开写在一行,
|
||||||
|
* 说明里有参数对上了 `@param` 时排成一张表,多一列说明。函数里的语句照常是这张卡片的子块。
|
||||||
|
*/
|
||||||
|
function handlerCard(block: CodeBlock, handler: HandlerFacts, analysis: Analysis, env: Env): RuleCard {
|
||||||
const { context, words } = env
|
const { context, words } = env
|
||||||
const parameters = parametersOf(handler.fn).map(parameter => [
|
const doc = block.kind === 'function' && block.facts.doc ? context.doc(block.facts.doc) : undefined
|
||||||
...binding(parameter.name, 'variable', env),
|
const parameters = parametersOf(handler.fn)
|
||||||
...(parameter.type ? [plain(': '), ...context.translate(parameter.type)] : []),
|
const docs = paramDocs(doc, parameters.map(parameter => parameter.name.text))
|
||||||
...(parameter.default ? [plain(' = '), ...context.translate(parameter.default)] : []),
|
const described = parameters.some(parameter => docs.of(parameter.name.text))
|
||||||
])
|
const parameterRow: PropertyRow = described
|
||||||
|
? {
|
||||||
|
label: words.labels.parameters,
|
||||||
|
value: [hint(words.count(parameters.length))],
|
||||||
|
columns: words.parameterHeads,
|
||||||
|
items: parameters.map(parameter => ({
|
||||||
|
cells: [
|
||||||
|
binding(parameter.name, 'variable', env),
|
||||||
|
parameter.type ? context.translate(parameter.type) : [hint(words.unmarked)],
|
||||||
|
parameter.default ? context.translate(parameter.default) : [hint(words.nothing)],
|
||||||
|
paramDocCell(docs, parameter.name.text),
|
||||||
|
],
|
||||||
|
defines: range(parameter.name),
|
||||||
|
})),
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
label: words.labels.parameters,
|
||||||
|
value: parameters.length > 0
|
||||||
|
? joined(parameters.map(parameter => [
|
||||||
|
...binding(parameter.name, 'variable', env),
|
||||||
|
...(parameter.type ? [plain(': '), ...context.translate(parameter.type)] : []),
|
||||||
|
...(parameter.default ? [plain(' = '), ...context.translate(parameter.default)] : []),
|
||||||
|
]), plain(', '))
|
||||||
|
: [hint(words.none)],
|
||||||
|
}
|
||||||
return card('handler', [
|
return card('handler', [
|
||||||
{ label: words.labels.name, value: [name(handler.name, 'function')] },
|
{ label: words.labels.name, value: [name(handler.name, 'function')] },
|
||||||
{ label: words.labels.parameters, value: parameters.length > 0 ? joined(parameters, plain(', ')) : [hint(words.none)] },
|
...docBodyRows(doc, words.labels.doc),
|
||||||
|
...unmatchedParamRows(docs.unmatched),
|
||||||
|
parameterRow,
|
||||||
|
...(doc?.returns ? [{ label: words.labels.returns, value: [], ...returnNote(doc) }] : []),
|
||||||
|
...throwsRows(doc),
|
||||||
{ label: words.labels.events, value: eventsText(handler, analysis, env) },
|
{ label: words.labels.events, value: eventsText(handler, analysis, env) },
|
||||||
], env, { defines: range(handler.name) })
|
], env, { defines: range(handler.name) })
|
||||||
}
|
}
|
||||||
@@ -976,7 +1040,7 @@ function bodyCard(block: CodeBlock, env: Env): RuleCard | undefined {
|
|||||||
if (effect)
|
if (effect)
|
||||||
return effectCard(block, effect, env)
|
return effectCard(block, effect, env)
|
||||||
const handler = body.handlers.find(at)
|
const handler = body.handlers.find(at)
|
||||||
return handler && handlerCard(handler, analysis, env)
|
return handler && handlerCard(block, handler, analysis, env)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import type { ImplBlock, KindBadge, LibraryRule, RuleCard, ThemeColor } from '../contract'
|
import type { ImplBlock, KindBadge, LibraryRule, RuleCard, RuleContext, ThemeColor } from '../contract'
|
||||||
import { implRows, plainSay } from './rustRows'
|
import { implRows, plainSay } from './rustRows'
|
||||||
|
|
||||||
/** 和函数卡同一个黄,实现块看起来仍是函数那一挂。 */
|
/** 和函数卡同一个黄,实现块看起来仍是函数那一挂。 */
|
||||||
@@ -14,15 +14,15 @@ export const rustImplRule: LibraryRule = {
|
|||||||
label: 'Rust 实现',
|
label: 'Rust 实现',
|
||||||
languageIds: ['rust'],
|
languageIds: ['rust'],
|
||||||
libraries: [],
|
libraries: [],
|
||||||
claim: block => block.kind === 'impl' ? implCard(block) : undefined,
|
claim: (block, context) => block.kind === 'impl' ? implCard(block, context) : undefined,
|
||||||
attributeNames: () => ({
|
attributeNames: () => ({
|
||||||
attributes: {},
|
attributes: {},
|
||||||
values: { kind: { 'rust.impl': '实现' } },
|
values: { kind: { 'rust.impl': '实现' } },
|
||||||
}),
|
}),
|
||||||
}
|
}
|
||||||
|
|
||||||
function implCard(block: ImplBlock): RuleCard {
|
function implCard(block: ImplBlock, context: RuleContext): RuleCard {
|
||||||
const rows = implRows(block, plainSay())
|
const rows = implRows(block, plainSay(context.doc))
|
||||||
const row = rows.findIndex(item => item.label === '方法')
|
const row = rows.findIndex(item => item.label === '方法')
|
||||||
const methods = block.children.filter(child => child.kind === 'function')
|
const methods = block.children.filter(child => child.kind === 'function')
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -108,7 +108,8 @@ describe('serde 与 thiserror', () => {
|
|||||||
const newCard = (await views(after)).find(block => block.kind === 'axum.route')
|
const newCard = (await views(after)).find(block => block.kind === 'axum.route')
|
||||||
const marks = oldCard && newCard ? blockMarks(oldCard, newCard) : undefined
|
const marks = oldCard && newCard ? blockMarks(oldCard, newCard) : undefined
|
||||||
const row = newCard && 'properties' in newCard ? newCard.properties.findIndex(item => item.label === '请求') : -1
|
const row = newCard && 'properties' in newCard ? newCard.properties.findIndex(item => item.label === '请求') : -1
|
||||||
const parts = marks?.rows?.changed[row]?.items?.changed[1]?.cells?.[1] ?? []
|
const cell = marks?.rows?.changed[row]?.items?.changed[1]?.cells?.[1]
|
||||||
|
const parts = cell && 'inline' in cell ? cell.inline : []
|
||||||
const text = (change: 'add' | 'del') => parts
|
const text = (change: 'add' | 'del') => parts
|
||||||
.filter(part => part.change === change)
|
.filter(part => part.change === change)
|
||||||
.flatMap(part => part.segments)
|
.flatMap(part => part.segments)
|
||||||
|
|||||||
@@ -1,11 +1,14 @@
|
|||||||
import type { AttributeBlock, AttributeFacts, AttributeItem, CfgPredicate, ClassBlock, CodeBlock, EnumBlock, FunctionBlock, ImplBlock, ImportBlock, Inline, ModuleBlock, PropertyItem, PropertyRow, SourceSpan, TokenRole, TraitBlock } from '../contract'
|
import type { AttributeBlock, AttributeFacts, AttributeItem, CfgPredicate, ClassBlock, CodeBlock, Doc, EnumBlock, FunctionBlock, ImplBlock, ImportBlock, Inline, ModuleBlock, Node, PropertyItem, PropertyRow, SourceSpan, TokenRole, TraitBlock } from '../contract'
|
||||||
import type { RustVisibility } from './rustTree'
|
import type { RustVisibility } from './rustTree'
|
||||||
import { bodyFunctions, docSentence, isMethod, matchArms, oneLine, returnText, signatureParams, structFields, variantPayload } from './rustTree'
|
import { paramDocCell, paramDocs, returnNote, throwsRows, unmatchedParamRows } from './docRows'
|
||||||
|
import { bodyFunctions, docSummary, isMethod, matchArms, oneLine, returnText, signatureParams, structFields, variantPayload } from './rustTree'
|
||||||
|
|
||||||
/** 把类型、名字画成原文。翻译插件传入时,名字带源码下标。 */
|
/** 把类型、名字画成原文,读说明。翻译插件传入时,名字带源码下标。 */
|
||||||
export interface RustSay {
|
export interface RustSay {
|
||||||
name: (node: import('../contract').Node, role: TokenRole) => Inline[]
|
name: (node: Node, role: TokenRole) => Inline[]
|
||||||
source: (node: import('../contract').Node, role?: TokenRole) => Inline[]
|
source: (node: Node, role?: TokenRole) => Inline[]
|
||||||
|
/** 读一项的文档注释(连着几行的 `///` 是一段),见 Translator.doc;没有时没有。 */
|
||||||
|
doc: (node: Node) => Doc | undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
const VISIBILITY: Record<RustVisibility, string> = {
|
const VISIBILITY: Record<RustVisibility, string> = {
|
||||||
@@ -49,15 +52,20 @@ export function importRows(block: ImportBlock, say: RustSay): PropertyRow[] {
|
|||||||
|
|
||||||
export function structRows(block: ClassBlock, say: RustSay): PropertyRow[] {
|
export function structRows(block: ClassBlock, say: RustSay): PropertyRow[] {
|
||||||
const fields = structFields(block.node)
|
const fields = structFields(block.node)
|
||||||
|
const doc = block.facts.doc && say.doc(block.facts.doc)
|
||||||
|
const docs = paramDocs(doc, fields.map(field => field.name.text))
|
||||||
|
const described = fields.some(field => docs.of(field.name.text))
|
||||||
return [
|
return [
|
||||||
nameRow(block.facts.name, say, 'type'),
|
nameRow(block.facts.name, say, 'type'),
|
||||||
...docRow(block.facts.doc),
|
...docRow(doc),
|
||||||
|
...unmatchedParamRows(docs.unmatched),
|
||||||
visibilityRow(block),
|
visibilityRow(block),
|
||||||
...tableRow('字段', ['字段', '类型', '可见性'], fields.map(field => ({
|
...tableRow('字段', ['字段', '类型', '可见性', ...(described ? ['说明'] : [])], fields.map(field => ({
|
||||||
cells: [
|
cells: [
|
||||||
say.name(field.name, 'variable'),
|
say.name(field.name, 'variable'),
|
||||||
say.source(field.type, 'type'),
|
say.source(field.type, 'type'),
|
||||||
[keyword(VISIBILITY[field.visibility])],
|
[keyword(VISIBILITY[field.visibility])],
|
||||||
|
...(described ? [paramDocCell(docs, field.name.text)] : []),
|
||||||
],
|
],
|
||||||
defines: range(field.name),
|
defines: range(field.name),
|
||||||
}))),
|
}))),
|
||||||
@@ -67,7 +75,7 @@ export function structRows(block: ClassBlock, say: RustSay): PropertyRow[] {
|
|||||||
export function moduleRows(block: ModuleBlock, say: RustSay): PropertyRow[] {
|
export function moduleRows(block: ModuleBlock, say: RustSay): PropertyRow[] {
|
||||||
return [
|
return [
|
||||||
nameRow(block.facts.name, say, 'variable'),
|
nameRow(block.facts.name, say, 'variable'),
|
||||||
...docRow(block.facts.doc),
|
...docRow(block.facts.doc && say.doc(block.facts.doc)),
|
||||||
visibilityRow(block),
|
visibilityRow(block),
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -166,7 +174,7 @@ function cfgText(predicate: CfgPredicate, nested: boolean): Inline[] {
|
|||||||
export function enumRows(block: EnumBlock, say: RustSay): PropertyRow[] {
|
export function enumRows(block: EnumBlock, say: RustSay): PropertyRow[] {
|
||||||
return [
|
return [
|
||||||
nameRow(block.facts.name, say, 'type'),
|
nameRow(block.facts.name, say, 'type'),
|
||||||
...docRow(block.facts.doc),
|
...docRow(block.facts.doc && say.doc(block.facts.doc)),
|
||||||
...tableRow('变体', ['变体', '携带的数据'], block.facts.members.map(member => ({
|
...tableRow('变体', ['变体', '携带的数据'], block.facts.members.map(member => ({
|
||||||
cells: [say.name(member.name, 'type'), [hint(variantPayload(member.value) ?? '—')]],
|
cells: [say.name(member.name, 'type'), [hint(variantPayload(member.value) ?? '—')]],
|
||||||
defines: range(member.name),
|
defines: range(member.name),
|
||||||
@@ -176,16 +184,26 @@ export function enumRows(block: EnumBlock, say: RustSay): PropertyRow[] {
|
|||||||
|
|
||||||
export function functionRows(block: FunctionBlock, say: RustSay): PropertyRow[] {
|
export function functionRows(block: FunctionBlock, say: RustSay): PropertyRow[] {
|
||||||
const arms = matchArms(block.node)
|
const arms = matchArms(block.node)
|
||||||
|
const doc = block.facts.doc && say.doc(block.facts.doc)
|
||||||
|
const { parameters } = block.facts
|
||||||
|
const docs = paramDocs(doc, parameters.map(param => param.name.text))
|
||||||
|
const described = parameters.some(param => docs.of(param.name.text))
|
||||||
return [
|
return [
|
||||||
nameRow(block.facts.name, say, 'function'),
|
nameRow(block.facts.name, say, 'function'),
|
||||||
...docRow(block.facts.doc),
|
...docRow(doc),
|
||||||
|
...unmatchedParamRows(docs.unmatched),
|
||||||
...(block.facts.async ? [{ label: '异步', value: [keyword('是')] }] : []),
|
...(block.facts.async ? [{ label: '异步', value: [keyword('是')] }] : []),
|
||||||
visibilityRow(block),
|
visibilityRow(block),
|
||||||
...tableRow(isMethod(block.node) ? '参数' : '输入参数', ['名称', '类型'], block.facts.parameters.map(param => ({
|
...tableRow(isMethod(block.node) ? '参数' : '输入参数', ['名称', '类型', ...(described ? ['说明'] : [])], parameters.map(param => ({
|
||||||
cells: [say.name(param.name, 'variable'), param.type ? say.source(param.type, 'type') : [hint('—')]],
|
cells: [
|
||||||
|
say.name(param.name, 'variable'),
|
||||||
|
param.type ? say.source(param.type, 'type') : [hint('—')],
|
||||||
|
...(described ? [paramDocCell(docs, param.name.text)] : []),
|
||||||
|
],
|
||||||
defines: range(param.name),
|
defines: range(param.name),
|
||||||
}))),
|
}))),
|
||||||
{ label: '返回', value: [plain(returnText(block.node))] },
|
{ label: '返回', value: [plain(returnText(block.node))], ...returnNote(doc) },
|
||||||
|
...throwsRows(doc),
|
||||||
...tableRow('分情况', ['分支', '做什么'], arms.map(arm => ({
|
...tableRow('分情况', ['分支', '做什么'], arms.map(arm => ({
|
||||||
cells: [[plain(arm.pattern)], [hint(arm.does)]],
|
cells: [[plain(arm.pattern)], [hint(arm.does)]],
|
||||||
})), '支'),
|
})), '支'),
|
||||||
@@ -196,7 +214,7 @@ export function traitRows(block: TraitBlock, say: RustSay): PropertyRow[] {
|
|||||||
const methods = bodyFunctions(block.node)
|
const methods = bodyFunctions(block.node)
|
||||||
return [
|
return [
|
||||||
nameRow(block.facts.name, say, 'type'),
|
nameRow(block.facts.name, say, 'type'),
|
||||||
...docRow(block.facts.doc),
|
...docRow(block.facts.doc && say.doc(block.facts.doc)),
|
||||||
...tableRow('方法', ['名称', '参数', '返回'], methods.map((method) => {
|
...tableRow('方法', ['名称', '参数', '返回'], methods.map((method) => {
|
||||||
const name = method.childForFieldName('name')
|
const name = method.childForFieldName('name')
|
||||||
return {
|
return {
|
||||||
@@ -218,7 +236,7 @@ export function implRows(block: ImplBlock, say: RustSay): PropertyRow[] {
|
|||||||
: [hint('实现')]
|
: [hint('实现')]
|
||||||
return [
|
return [
|
||||||
{ label: '名称', value: title, ...(block.facts.name && { defines: range(block.facts.name) }) },
|
{ label: '名称', value: title, ...(block.facts.name && { defines: range(block.facts.name) }) },
|
||||||
...docRow(block.facts.doc),
|
...docRow(block.facts.doc && say.doc(block.facts.doc)),
|
||||||
...tableRow('方法', ['名称', '参数', '返回'], methods.map((method) => {
|
...tableRow('方法', ['名称', '参数', '返回'], methods.map((method) => {
|
||||||
const name = method.facts.name
|
const name = method.facts.name
|
||||||
return {
|
return {
|
||||||
@@ -233,7 +251,7 @@ export function implRows(block: ImplBlock, say: RustSay): PropertyRow[] {
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
function nameRow(name: import('../contract').Node | undefined, say: RustSay, role: TokenRole): PropertyRow {
|
function nameRow(name: Node | undefined, say: RustSay, role: TokenRole): PropertyRow {
|
||||||
return {
|
return {
|
||||||
label: '名称',
|
label: '名称',
|
||||||
value: name ? say.name(name, role) : [hint('匿名')],
|
value: name ? say.name(name, role) : [hint('匿名')],
|
||||||
@@ -241,8 +259,8 @@ function nameRow(name: import('../contract').Node | undefined, say: RustSay, rol
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function docRow(doc: import('../contract').Node | undefined): PropertyRow[] {
|
function docRow(doc: Doc | undefined): PropertyRow[] {
|
||||||
const sentence = docSentence(doc)
|
const sentence = docSummary(doc)
|
||||||
return sentence ? [{ label: '说明', value: { markdown: sentence } }] : []
|
return sentence ? [{ label: '说明', value: { markdown: sentence } }] : []
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -277,13 +295,14 @@ function note(text: string): Inline {
|
|||||||
return { text, role: 'comment' }
|
return { text, role: 'comment' }
|
||||||
}
|
}
|
||||||
|
|
||||||
function range(node: import('../contract').Node): { startIndex: number, endIndex: number } {
|
function range(node: Node): { startIndex: number, endIndex: number } {
|
||||||
return { startIndex: node.startIndex, endIndex: node.endIndex }
|
return { startIndex: node.startIndex, endIndex: node.endIndex }
|
||||||
}
|
}
|
||||||
/** 名字、类型照抄源码,名字带上下标,点了能跳到定义。 */
|
/** 名字、类型照抄源码,名字带上下标,点了能跳到定义。 */
|
||||||
export function plainSay(): RustSay {
|
export function plainSay(doc: RustSay['doc']): RustSay {
|
||||||
return {
|
return {
|
||||||
name: (node, role) => [{ text: node.text, role, offset: node.startIndex }],
|
name: (node, role) => [{ text: node.text, role, offset: node.startIndex }],
|
||||||
source: (node, role = 'type') => [{ text: oneLine(node.text), role, offset: node.startIndex }],
|
source: (node, role = 'type') => [{ text: oneLine(node.text), role, offset: node.startIndex }],
|
||||||
|
doc,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import type { Access, BlockAttributes, Node } from '../contract'
|
import type { Access, BlockAttributes, Doc, Node } from '../contract'
|
||||||
|
|
||||||
/** 具名子节点,空的跳过。 */
|
/** 具名子节点,空的跳过。 */
|
||||||
export function named(node: Node | null | undefined): Node[] {
|
export function named(node: Node | null | undefined): Node[] {
|
||||||
@@ -19,11 +19,10 @@ export function commentPlain(node: Node): string {
|
|||||||
return raw.replace(/\n/g, ' ').trim()
|
return raw.replace(/\n/g, ' ').trim()
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 说明行用的第一句。没有文字时没有。 */
|
/** 说明行用的第一句:说明(翻译插件读出来的,块标签已经拿掉)正文的第一句,换行当空格。没有正文时没有。 */
|
||||||
export function docSentence(node: Node | undefined): string | undefined {
|
export function docSummary(doc: Doc | undefined): string | undefined {
|
||||||
if (!node)
|
const text = doc?.body?.markdown.replace(/\s*\n\s*/g, ' ').trim()
|
||||||
return undefined
|
const sentence = text && firstSentence(text)
|
||||||
const sentence = firstSentence(commentPlain(node))
|
|
||||||
return sentence || undefined
|
return sentence || undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import type { ClassBlock, Inline, KindBadge, LibraryRule, Node, PropertyRow, RuleCard, SegmentGroup, ThemeColor, TokenRole } from '../contract'
|
import type { ClassBlock, Inline, KindBadge, LibraryRule, Node, PropertyRow, RuleCard, RuleContext, SegmentGroup, ThemeColor, TokenRole } from '../contract'
|
||||||
import type { rustVisibility } from './rustTree'
|
import type { rustVisibility } from './rustTree'
|
||||||
|
import { paramDocCell, paramDocs, unmatchedParamRows } from './docRows'
|
||||||
import { visibilityWord } from './rustRows'
|
import { visibilityWord } from './rustRows'
|
||||||
import { attributeName, deriveNames, docSentence, itemAttributes, leadingAttributes, named, oneLine, stringContent, structFields } from './rustTree'
|
import { attributeName, deriveNames, docSummary, itemAttributes, leadingAttributes, named, oneLine, stringContent, structFields } from './rustTree'
|
||||||
|
|
||||||
/** 和结构体同一个青,序列化只是名称后的标签,不另换边框色。 */
|
/** 和结构体同一个青,序列化只是名称后的标签,不另换边框色。 */
|
||||||
const COLOR: ThemeColor = { dark: '#4ec9b0', light: '#267f99' }
|
const COLOR: ThemeColor = { dark: '#4ec9b0', light: '#267f99' }
|
||||||
@@ -17,7 +18,7 @@ export const serdeRule: LibraryRule = {
|
|||||||
label: 'serde 序列化',
|
label: 'serde 序列化',
|
||||||
languageIds: ['rust'],
|
languageIds: ['rust'],
|
||||||
libraries: ['serde'],
|
libraries: ['serde'],
|
||||||
claim: block => block.kind === 'class' && block.node.type === 'struct_item' && marked(block) ? card(block) : undefined,
|
claim: (block, context) => block.kind === 'class' && block.node.type === 'struct_item' && marked(block) ? card(block, context) : undefined,
|
||||||
attributeNames: () => ({ attributes: {}, values: { kind: { 'serde.struct': '可序列化' } } }),
|
attributeNames: () => ({ attributes: {}, values: { kind: { 'serde.struct': '可序列化' } } }),
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -25,30 +26,35 @@ function marked(block: ClassBlock): boolean {
|
|||||||
return deriveNames(block.node).some(name => MARKS.has(name))
|
return deriveNames(block.node).some(name => MARKS.has(name))
|
||||||
}
|
}
|
||||||
|
|
||||||
function card(block: ClassBlock): RuleCard {
|
function card(block: ClassBlock, context: RuleContext): RuleCard {
|
||||||
const fields = structFields(block.node)
|
const fields = structFields(block.node)
|
||||||
const camel = renameAll(block.node) === 'camelCase'
|
const camel = renameAll(block.node) === 'camelCase'
|
||||||
const doc = docSentence(block.facts.doc)
|
const doc = block.facts.doc && context.doc(block.facts.doc)
|
||||||
|
const summary = docSummary(doc)
|
||||||
|
const docs = paramDocs(doc, fields.map(field => field.name.text))
|
||||||
|
const described = fields.some(field => docs.of(field.name.text))
|
||||||
const rows: PropertyRow[] = [
|
const rows: PropertyRow[] = [
|
||||||
{
|
{
|
||||||
label: '名称',
|
label: '名称',
|
||||||
value: block.facts.name ? [...copied(block.facts.name, 'type'), plain(' '), tag('可序列化')] : [tag('可序列化')],
|
value: block.facts.name ? [...copied(block.facts.name, 'type'), plain(' '), tag('可序列化')] : [tag('可序列化')],
|
||||||
...(block.facts.name && { defines: range(block.facts.name) }),
|
...(block.facts.name && { defines: range(block.facts.name) }),
|
||||||
},
|
},
|
||||||
...(doc ? [{ label: '说明', value: { markdown: doc } }] : []),
|
...(summary ? [{ label: '说明', value: { markdown: summary } }] : []),
|
||||||
|
...unmatchedParamRows(docs.unmatched),
|
||||||
{ label: '可见性', value: [{ text: visibilityWord(block), role: 'keyword' as const }] },
|
{ label: '可见性', value: [{ text: visibilityWord(block), role: 'keyword' as const }] },
|
||||||
...(camel ? [{ label: '选项', value: [keyword('字段名改驼峰')] }] : []),
|
...(camel ? [{ label: '选项', value: [keyword('字段名改驼峰')] }] : []),
|
||||||
...(fields.length > 0
|
...(fields.length > 0
|
||||||
? [{
|
? [{
|
||||||
label: '字段',
|
label: '字段',
|
||||||
value: [hint(`${fields.length} 个`)],
|
value: [hint(`${fields.length} 个`)],
|
||||||
columns: ['字段', '类型', '可见性', '序列化'],
|
columns: ['字段', '类型', '可见性', '序列化', ...(described ? ['说明'] : [])],
|
||||||
items: fields.map(field => ({
|
items: fields.map(field => ({
|
||||||
cells: [
|
cells: [
|
||||||
copied(field.name, 'variable'),
|
copied(field.name, 'variable'),
|
||||||
copied(field.type, 'type'),
|
copied(field.type, 'type'),
|
||||||
[keyword(visibilityWordOf(field.visibility))],
|
[keyword(visibilityWordOf(field.visibility))],
|
||||||
[serdeCell(field.name)],
|
[serdeCell(field.name)],
|
||||||
|
...(described ? [paramDocCell(docs, field.name.text)] : []),
|
||||||
],
|
],
|
||||||
defines: range(field.name),
|
defines: range(field.name),
|
||||||
})),
|
})),
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { describe, expect, it } from 'vitest'
|
|||||||
import { toBlockViews } from '../blockView'
|
import { toBlockViews } from '../blockView'
|
||||||
import { replaceLines } from '../editText'
|
import { replaceLines } from '../editText'
|
||||||
import { createPythonAdapter } from '../languages/python'
|
import { createPythonAdapter } from '../languages/python'
|
||||||
import { flatSegments, propertyLines, readFixture, WASM_DIR, withBlocks } from '../testSupport'
|
import { flatSegments, inlineOf, propertyLines, readFixture, WASM_DIR, withBlocks } from '../testSupport'
|
||||||
import { pythonZhTranslator } from '../translators/pythonZh'
|
import { pythonZhTranslator } from '../translators/pythonZh'
|
||||||
import { sqlalchemyRule } from './sqlalchemy'
|
import { sqlalchemyRule } from './sqlalchemy'
|
||||||
|
|
||||||
@@ -38,7 +38,7 @@ async function model(source: string, name: string): Promise<BlockView> {
|
|||||||
function linkedSegments(rows: PropertyRow[]): Segment[] {
|
function linkedSegments(rows: PropertyRow[]): Segment[] {
|
||||||
const pieces: Inline[] = rows.flatMap(row => [
|
const pieces: Inline[] = rows.flatMap(row => [
|
||||||
...('markdown' in row.value ? [] : row.value),
|
...('markdown' in row.value ? [] : row.value),
|
||||||
...(row.items ?? []).flatMap(item => item.cells.flat()),
|
...(row.items ?? []).flatMap(item => item.cells.flatMap(inlineOf)),
|
||||||
])
|
])
|
||||||
return flatSegments(pieces).filter(segment => segment.offset !== undefined)
|
return flatSegments(pieces).filter(segment => segment.offset !== undefined)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -352,7 +352,7 @@ function columnItem({ member, call }: Column, env: Env): PropertyItem {
|
|||||||
for (const foreign of calls.filter((found): found is Call => found?.name === 'ForeignKey'))
|
for (const foreign of calls.filter((found): found is Call => found?.name === 'ForeignKey'))
|
||||||
constraints.push(foreignKey(foreign, env))
|
constraints.push(foreignKey(foreign, env))
|
||||||
const comment = keywords.get('comment')
|
const comment = keywords.get('comment')
|
||||||
const doc = member.doc && context.doc(member.doc)
|
const doc = member.doc && context.doc(member.doc)?.body
|
||||||
return {
|
return {
|
||||||
cells: [
|
cells: [
|
||||||
[name(member.name, 'variable')],
|
[name(member.name, 'variable')],
|
||||||
@@ -409,7 +409,7 @@ function tableBlock(block: ClassBlock, model: Model, env: Env): RuleCard {
|
|||||||
const { labels } = words
|
const { labels } = words
|
||||||
const { options, items } = tableArgs(model.args?.value)
|
const { options, items } = tableArgs(model.args?.value)
|
||||||
const comment = dictValue(options, 'comment')
|
const comment = dictValue(options, 'comment')
|
||||||
const doc = (comment && context.doc(comment)) ?? (facts.doc && context.doc(facts.doc))
|
const doc = (comment && context.doc(comment)?.body) ?? (facts.doc && context.doc(facts.doc)?.body)
|
||||||
const indexes: PropertyItem[] = []
|
const indexes: PropertyItem[] = []
|
||||||
const uniques: PropertyItem[] = []
|
const uniques: PropertyItem[] = []
|
||||||
for (const item of items) {
|
for (const item of items) {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import type { CodeBlock, Inline, KindBadge, LibraryRule, Node, PropertyRow, RuleCard, ThemeColor } from '../contract'
|
import type { CodeBlock, Inline, KindBadge, LibraryRule, Node, PropertyRow, RuleCard, RuleContext, ThemeColor } from '../contract'
|
||||||
import { commentPlain, firstSentence, macroInvocation, named, oneLine, stringContent } from './rustTree'
|
import { docSummary, macroInvocation, named, oneLine, stringContent } from './rustTree'
|
||||||
|
|
||||||
/** 查询卡用字符串那种橙。 */
|
/** 查询卡用字符串那种橙。 */
|
||||||
const COLOR: ThemeColor = { dark: '#ce9178', light: '#a31515' }
|
const COLOR: ThemeColor = { dark: '#ce9178', light: '#a31515' }
|
||||||
@@ -15,16 +15,16 @@ export const sqlxRule: LibraryRule = {
|
|||||||
label: 'sqlx 查询',
|
label: 'sqlx 查询',
|
||||||
languageIds: ['rust'],
|
languageIds: ['rust'],
|
||||||
libraries: ['sqlx'],
|
libraries: ['sqlx'],
|
||||||
claim: (block) => {
|
claim: (block, context) => {
|
||||||
if (block.kind === 'comment')
|
if (block.kind === 'comment')
|
||||||
return nextQuery(block.node) ? { omit: true } : undefined
|
return nextQuery(block.node) ? { omit: true } : undefined
|
||||||
const macro = block.kind === 'statement' ? findQuery(block.node) : undefined
|
const macro = block.kind === 'statement' ? findQuery(block.node) : undefined
|
||||||
return macro ? card(block, macro) : undefined
|
return macro ? card(block, macro, context) : undefined
|
||||||
},
|
},
|
||||||
attributeNames: () => ({ attributes: {}, values: { kind: { 'sqlx.query': '查询' } } }),
|
attributeNames: () => ({ attributes: {}, values: { kind: { 'sqlx.query': '查询' } } }),
|
||||||
}
|
}
|
||||||
|
|
||||||
function card(block: CodeBlock, macro: Node): RuleCard {
|
function card(block: CodeBlock, macro: Node, context: RuleContext): RuleCard {
|
||||||
const call = macroInvocation(macro)
|
const call = macroInvocation(macro)
|
||||||
const args = call?.args ?? []
|
const args = call?.args ?? []
|
||||||
const typed = call?.name.split('::').pop() !== 'query'
|
const typed = call?.name.split('::').pop() !== 'query'
|
||||||
@@ -32,7 +32,7 @@ function card(block: CodeBlock, macro: Node): RuleCard {
|
|||||||
const sql = stringContent(sqlNode) ?? ''
|
const sql = stringContent(sqlNode) ?? ''
|
||||||
const params = args.slice(typed ? 2 : 1)
|
const params = args.slice(typed ? 2 : 1)
|
||||||
const returned = typed ? args[0] : undefined
|
const returned = typed ? args[0] : undefined
|
||||||
const note = precedingNote(block.node)
|
const note = precedingNote(block.node, context)
|
||||||
const rows: PropertyRow[] = [
|
const rows: PropertyRow[] = [
|
||||||
...(note ? [{ label: '说明', value: { markdown: note } }] : []),
|
...(note ? [{ label: '说明', value: { markdown: note } }] : []),
|
||||||
{ label: 'SQL', value: [{ text: sql, role: 'string', ...(sqlNode && { offset: sqlNode.startIndex }) }] },
|
{ label: 'SQL', value: [{ text: sql, role: 'string', ...(sqlNode && { offset: sqlNode.startIndex }) }] },
|
||||||
@@ -65,14 +65,13 @@ function nextQuery(comment: Node): Node | undefined {
|
|||||||
return next ? findQuery(next) : undefined
|
return next ? findQuery(next) : undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
function precedingNote(node: Node): string | undefined {
|
function precedingNote(node: Node, context: RuleContext): string | undefined {
|
||||||
let prev = node.previousNamedSibling
|
let prev = node.previousNamedSibling
|
||||||
while (prev?.type === 'attribute_item')
|
while (prev?.type === 'attribute_item')
|
||||||
prev = prev.previousNamedSibling
|
prev = prev.previousNamedSibling
|
||||||
if (!prev || (prev.type !== 'line_comment' && prev.type !== 'block_comment'))
|
if (!prev || (prev.type !== 'line_comment' && prev.type !== 'block_comment'))
|
||||||
return undefined
|
return undefined
|
||||||
const sentence = firstSentence(commentPlain(prev))
|
return docSummary(context.doc(prev))
|
||||||
return sentence || undefined
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function hint(text: string): Inline {
|
function hint(text: string): Inline {
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import type { FunctionBlock, Inline, KindBadge, LibraryRule, Node, PropertyRow, RuleCard, SegmentGroup, ThemeColor } from '../contract'
|
import type { FunctionBlock, Inline, KindBadge, LibraryRule, Node, PropertyRow, RuleCard, RuleContext, SegmentGroup, ThemeColor } from '../contract'
|
||||||
import { attributeName, docSentence, oneLine, returnText } from './rustTree'
|
import { paramDocCell, paramDocs, returnNote, throwsRows, unmatchedParamRows } from './docRows'
|
||||||
|
import { attributeName, docSummary, oneLine, returnText } from './rustTree'
|
||||||
|
|
||||||
/** 桌面命令的边框用这个橙。 */
|
/** 桌面命令的边框用这个橙。 */
|
||||||
const COLOR: ThemeColor = { dark: '#e5c07b', light: '#9a6a00' }
|
const COLOR: ThemeColor = { dark: '#e5c07b', light: '#9a6a00' }
|
||||||
@@ -15,7 +16,7 @@ export const tauriRule: LibraryRule = {
|
|||||||
label: 'tauri 桌面命令',
|
label: 'tauri 桌面命令',
|
||||||
languageIds: ['rust'],
|
languageIds: ['rust'],
|
||||||
libraries: ['tauri'],
|
libraries: ['tauri'],
|
||||||
claim: block => block.kind === 'function' && commanded(block) ? card(block) : undefined,
|
claim: (block, context) => block.kind === 'function' && commanded(block) ? card(block, context) : undefined,
|
||||||
attributeNames: () => ({ attributes: {}, values: { kind: { 'tauri.command': '桌面命令' } } }),
|
attributeNames: () => ({ attributes: {}, values: { kind: { 'tauri.command': '桌面命令' } } }),
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -23,8 +24,12 @@ function commanded(block: FunctionBlock): boolean {
|
|||||||
return block.facts.decorators.some(attr => attributeName(attr) === 'tauri::command')
|
return block.facts.decorators.some(attr => attributeName(attr) === 'tauri::command')
|
||||||
}
|
}
|
||||||
|
|
||||||
function card(block: FunctionBlock): RuleCard {
|
function card(block: FunctionBlock, context: RuleContext): RuleCard {
|
||||||
const doc = docSentence(block.facts.doc)
|
const doc = block.facts.doc && context.doc(block.facts.doc)
|
||||||
|
const summary = docSummary(doc)
|
||||||
|
const { parameters } = block.facts
|
||||||
|
const docs = paramDocs(doc, parameters.map(param => param.name.text))
|
||||||
|
const described = parameters.some(param => docs.of(param.name.text))
|
||||||
const rows: PropertyRow[] = [
|
const rows: PropertyRow[] = [
|
||||||
{
|
{
|
||||||
label: '名称',
|
label: '名称',
|
||||||
@@ -33,24 +38,27 @@ function card(block: FunctionBlock): RuleCard {
|
|||||||
: [tag('桌面命令')],
|
: [tag('桌面命令')],
|
||||||
...(block.facts.name && { defines: range(block.facts.name) }),
|
...(block.facts.name && { defines: range(block.facts.name) }),
|
||||||
},
|
},
|
||||||
...(doc ? [{ label: '说明', value: { markdown: doc } }] : []),
|
...(summary ? [{ label: '说明', value: { markdown: summary } }] : []),
|
||||||
|
...unmatchedParamRows(docs.unmatched),
|
||||||
...(block.facts.async ? [{ label: '异步', value: [keyword('是')] }] : []),
|
...(block.facts.async ? [{ label: '异步', value: [keyword('是')] }] : []),
|
||||||
...(block.facts.parameters.length > 0
|
...(parameters.length > 0
|
||||||
? [{
|
? [{
|
||||||
label: '参数',
|
label: '参数',
|
||||||
value: [hint(`${block.facts.parameters.length} 个`)],
|
value: [hint(`${parameters.length} 个`)],
|
||||||
columns: ['名称', '类型', '来源'],
|
columns: ['名称', '类型', '来源', ...(described ? ['说明'] : [])],
|
||||||
items: block.facts.parameters.map(param => ({
|
items: parameters.map(param => ({
|
||||||
cells: [
|
cells: [
|
||||||
[variable(param.name)],
|
[variable(param.name)],
|
||||||
[param.type ? typed(param.type) : hint('—')],
|
[param.type ? typed(param.type) : hint('—')],
|
||||||
[hint(isWindowState(param.type) ? '窗口状态' : '调用方传入')],
|
[hint(isWindowState(param.type) ? '窗口状态' : '调用方传入')],
|
||||||
|
...(described ? [paramDocCell(docs, param.name.text)] : []),
|
||||||
],
|
],
|
||||||
defines: range(param.name),
|
defines: range(param.name),
|
||||||
})),
|
})),
|
||||||
}]
|
}]
|
||||||
: []),
|
: []),
|
||||||
{ label: '返回', value: [plain(returnText(block.node))] },
|
{ label: '返回', value: [plain(returnText(block.node))], ...returnNote(doc) },
|
||||||
|
...throwsRows(doc),
|
||||||
]
|
]
|
||||||
return {
|
return {
|
||||||
kind: 'tauri.command',
|
kind: 'tauri.command',
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import type { EnumBlock, Inline, KindBadge, LibraryRule, Node, PropertyRow, RuleCard, SegmentGroup, ThemeColor } from '../contract'
|
import type { EnumBlock, Inline, KindBadge, LibraryRule, Node, PreviewText, PropertyRow, RuleCard, RuleContext, SegmentGroup, ThemeColor } from '../contract'
|
||||||
import { attributeName, commentPlain, deriveNames, docSentence, firstSentence, named, stringContent, variantPayload } from './rustTree'
|
import { attributeName, commentPlain, deriveNames, docSummary, firstSentence, named, stringContent, variantPayload } from './rustTree'
|
||||||
|
|
||||||
/** 错误卡用异常那种红。 */
|
/** 错误卡用异常那种红。 */
|
||||||
const COLOR: ThemeColor = { dark: '#f14c4c', light: '#cd3131' }
|
const COLOR: ThemeColor = { dark: '#f14c4c', light: '#cd3131' }
|
||||||
@@ -14,12 +14,12 @@ export const thiserrorRule: LibraryRule = {
|
|||||||
label: 'thiserror 错误',
|
label: 'thiserror 错误',
|
||||||
languageIds: ['rust'],
|
languageIds: ['rust'],
|
||||||
libraries: ['thiserror'],
|
libraries: ['thiserror'],
|
||||||
claim: block => block.kind === 'enum' && deriveNames(block.node).includes('Error') ? card(block) : undefined,
|
claim: (block, context) => block.kind === 'enum' && deriveNames(block.node).includes('Error') ? card(block, context) : undefined,
|
||||||
attributeNames: () => ({ attributes: {}, values: { kind: { 'thiserror.error': '错误' } } }),
|
attributeNames: () => ({ attributes: {}, values: { kind: { 'thiserror.error': '错误' } } }),
|
||||||
}
|
}
|
||||||
|
|
||||||
function card(block: EnumBlock): RuleCard {
|
function card(block: EnumBlock, context: RuleContext): RuleCard {
|
||||||
const doc = docSentence(block.facts.doc)
|
const doc = docSummary(block.facts.doc && context.doc(block.facts.doc))
|
||||||
const rows: PropertyRow[] = [
|
const rows: PropertyRow[] = [
|
||||||
{
|
{
|
||||||
label: '名称',
|
label: '名称',
|
||||||
@@ -34,7 +34,7 @@ function card(block: EnumBlock): RuleCard {
|
|||||||
items: block.facts.members.map((member) => {
|
items: block.facts.members.map((member) => {
|
||||||
const variant = member.name.parent?.type === 'enum_variant' ? member.name.parent : member.name
|
const variant = member.name.parent?.type === 'enum_variant' ? member.name.parent : member.name
|
||||||
const display = errorText(variant)
|
const display = errorText(variant)
|
||||||
const cells: Inline[][] = [
|
const cells: PreviewText[] = [
|
||||||
[{ text: member.name.text, role: 'type', offset: member.name.startIndex }],
|
[{ text: member.name.text, role: 'type', offset: member.name.startIndex }],
|
||||||
[hint(whenText(variant) ?? '—')],
|
[hint(whenText(variant) ?? '—')],
|
||||||
display
|
display
|
||||||
|
|||||||
@@ -66,18 +66,23 @@ export function previewText(text: PreviewText): string {
|
|||||||
return 'markdown' in text ? text.markdown : segmentText(text)
|
return 'markdown' in text ? text.markdown : segmentText(text)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 预览文字里着色的词;Markdown 没有。 */
|
||||||
|
export function inlineOf(text: PreviewText): Inline[] {
|
||||||
|
return 'markdown' in text ? [] : text
|
||||||
|
}
|
||||||
|
|
||||||
/** 子行写成文字:各列用 ` | ` 隔开,下一层子行、排在最后一格里的表(PropertyItem.table)的各行比这一层多缩进两格。 */
|
/** 子行写成文字:各列用 ` | ` 隔开,下一层子行、排在最后一格里的表(PropertyItem.table)的各行比这一层多缩进两格。 */
|
||||||
function itemLines(items: PropertyItem[] | undefined, indent: string): string[] {
|
function itemLines(items: PropertyItem[] | undefined, indent: string): string[] {
|
||||||
return (items ?? []).flatMap(item => [
|
return (items ?? []).flatMap(item => [
|
||||||
`${indent}${item.cells.map(segmentText).join(' | ')}`,
|
`${indent}${item.cells.map(previewText).join(' | ')}`,
|
||||||
...itemLines(item.items, `${indent} `),
|
...itemLines(item.items, `${indent} `),
|
||||||
...itemLines(item.table?.items, `${indent} `),
|
...itemLines(item.table?.items, `${indent} `),
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 属性表写成文字:每行 `名称: 值`,展开的子行比上一层多缩进两格(见 itemLines)。 */
|
/** 属性表写成文字:每行 `名称: 值`(跟着说明的写成 `值 — 说明`),展开的子行比上一层多缩进两格(见 itemLines)。 */
|
||||||
export function propertyLines(rows: PropertyRow[]): string[] {
|
export function propertyLines(rows: PropertyRow[]): string[] {
|
||||||
return rows.flatMap(row => [`${row.label}: ${previewText(row.value)}`, ...itemLines(row.items, ' ')])
|
return rows.flatMap(row => [`${row.label}: ${previewText(row.value)}${row.note ? ` — ${row.note.markdown}` : ''}`, ...itemLines(row.items, ' ')])
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 一行译文下面那张表写成文字,同 propertyLines 的子行,第一层不缩进。 */
|
/** 一行译文下面那张表写成文字,同 propertyLines 的子行,第一层不缩进。 */
|
||||||
@@ -93,7 +98,7 @@ export function tableLines(table: PreviewTable | undefined): string[] {
|
|||||||
export function viewLines(blocks: readonly BlockView[], indent = ''): string[] {
|
export function viewLines(blocks: readonly BlockView[], indent = ''): string[] {
|
||||||
const value = (text: PreviewText): string => 'markdown' in text ? text.markdown.split('\n')[0] : segmentText(text)
|
const value = (text: PreviewText): string => 'markdown' in text ? text.markdown.split('\n')[0] : segmentText(text)
|
||||||
const items = (list: PropertyItem[] | undefined, pad: string): string[] => (list ?? []).flatMap(item => [
|
const items = (list: PropertyItem[] | undefined, pad: string): string[] => (list ?? []).flatMap(item => [
|
||||||
pad + item.cells.map(segmentText).join(' | ').replace(/\n/g, ' ⏎ '),
|
pad + item.cells.map(previewText).join(' | ').replace(/\n/g, ' ⏎ '),
|
||||||
...items(item.items, `${pad} `),
|
...items(item.items, `${pad} `),
|
||||||
...items(item.table?.items, `${pad} `),
|
...items(item.table?.items, `${pad} `),
|
||||||
])
|
])
|
||||||
|
|||||||
@@ -3,8 +3,6 @@ import { attributeRows, enumRows, functionRows, implRows, importRows, moduleRows
|
|||||||
import { isCrateRoot } from '../rules/rustTree'
|
import { isCrateRoot } from '../rules/rustTree'
|
||||||
import { createZhTemplateTranslator, KIND_ZH, ZH_ATTRIBUTE_NAMES } from './zhTemplate'
|
import { createZhTemplateTranslator, KIND_ZH, ZH_ATTRIBUTE_NAMES } from './zhTemplate'
|
||||||
|
|
||||||
const say = plainSay()
|
|
||||||
|
|
||||||
const template = createZhTemplateTranslator({
|
const template = createZhTemplateTranslator({
|
||||||
id: 'rust.zh-template',
|
id: 'rust.zh-template',
|
||||||
label: '中文直译',
|
label: '中文直译',
|
||||||
@@ -55,6 +53,7 @@ const template = createZhTemplateTranslator({
|
|||||||
mapLiterals: {},
|
mapLiterals: {},
|
||||||
stringNodes: ['string_literal'],
|
stringNodes: ['string_literal'],
|
||||||
commentText,
|
commentText,
|
||||||
|
docNodes,
|
||||||
nodeTemplates: {},
|
nodeTemplates: {},
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -82,6 +81,7 @@ export const rustZhTranslator: Translator = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function describe(block: FactBlock, document: SourceDocument): PropertyRow[] {
|
function describe(block: FactBlock, document: SourceDocument): PropertyRow[] {
|
||||||
|
const say = plainSay(node => template.doc(node, document))
|
||||||
switch (block.kind) {
|
switch (block.kind) {
|
||||||
case 'import':
|
case 'import':
|
||||||
return importRows(block, say)
|
return importRows(block, say)
|
||||||
@@ -130,6 +130,17 @@ function original(node: Node, span: SourceSpan, { source, language }: SourceDocu
|
|||||||
return out
|
return out
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 一项的文档注释:`///` 一行一个节点,从第一条起往下连着写的(下一行紧跟着另一条 `///`)都算这一段;块注释本来就是一个节点。
|
||||||
|
*/
|
||||||
|
function docNodes(first: Node): Node[] {
|
||||||
|
const outer = (node: Node): boolean => node.type === 'line_comment' && node.namedChildren.some(child => child?.type === 'outer_doc_comment_marker')
|
||||||
|
const nodes = [first]
|
||||||
|
for (let next = first.nextSibling; outer(first) && next && outer(next) && next.startPosition.row === nodes.at(-1)!.startPosition.row + 1; next = next.nextSibling)
|
||||||
|
nodes.push(next)
|
||||||
|
return nodes
|
||||||
|
}
|
||||||
|
|
||||||
/** 行注释去掉 `//`(文档注释连 `/`、`!`)和后面一个空格,缩进留给 Markdown;块注释去掉开头结尾的注释符号。 */
|
/** 行注释去掉 `//`(文档注释连 `/`、`!`)和后面一个空格,缩进留给 Markdown;块注释去掉开头结尾的注释符号。 */
|
||||||
function commentText(node: Node, text: string): string {
|
function commentText(node: Node, text: string): string {
|
||||||
if (node.type === 'line_comment')
|
if (node.type === 'line_comment')
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import type { BlockKind, CallFacts, Inline, LanguageAdapter, PreviewTable, PropertyItem, PropertyRow, Segment, SourceDocument, TokenRole, Translator } from '../contract'
|
import type { BlockKind, CallFacts, Inline, LanguageAdapter, PreviewTable, PreviewText, PropertyItem, PropertyRow, Segment, SourceDocument, TokenRole, Translator } from '../contract'
|
||||||
import type { ZhTable } from './zhTemplate'
|
import type { ZhTable } from './zhTemplate'
|
||||||
import { describe, expect, it } from 'vitest'
|
import { describe, expect, it } from 'vitest'
|
||||||
import { createPythonAdapter } from '../languages/python'
|
import { createPythonAdapter } from '../languages/python'
|
||||||
import { createTypeScriptAdapter } from '../languages/typescript'
|
import { createTypeScriptAdapter } from '../languages/typescript'
|
||||||
import { flatSegments, propertyLines, readFixture, segmentText, tableLines, walk, WASM_DIR, withBlocks } from '../testSupport'
|
import { flatSegments, inlineOf, previewText, propertyLines, readFixture, tableLines, walk, WASM_DIR, withBlocks } from '../testSupport'
|
||||||
import { pythonZhTranslator } from './pythonZh'
|
import { pythonZhTranslator } from './pythonZh'
|
||||||
import { typescriptZhTranslator } from './typescriptZh'
|
import { typescriptZhTranslator } from './typescriptZh'
|
||||||
import { createZhTemplateTranslator } from './zhTemplate'
|
import { createZhTemplateTranslator } from './zhTemplate'
|
||||||
@@ -58,14 +58,14 @@ function valueOf(rows: PropertyRow[] | undefined, label: string): Inline[] {
|
|||||||
return value
|
return value
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 译文写成文字,框写成 `⟦框头|框里的文字⟧`。 */
|
/** 译文写成文字,框写成 `⟦框头|框里的文字⟧`;Markdown 取原文。 */
|
||||||
function text(segments: Inline[] | undefined): string {
|
function text(segments: PreviewText | undefined): string {
|
||||||
return segmentText(segments ?? [])
|
return previewText(segments ?? [])
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 带下标的段(框里的也算),按出现顺序写成 [文字, 下标]。 */
|
/** 带下标的段(框里的也算),按出现顺序写成 [文字, 下标]。 */
|
||||||
function linked(segments: Inline[] | undefined): [string, number][] {
|
function linked(segments: PreviewText | undefined): [string, number][] {
|
||||||
return flatSegments(segments ?? []).flatMap(segment => segment.offset === undefined ? [] : [[segment.text, segment.offset] as [string, number]])
|
return flatSegments(inlineOf(segments ?? [])).flatMap(segment => segment.offset === undefined ? [] : [[segment.text, segment.offset] as [string, number]])
|
||||||
}
|
}
|
||||||
|
|
||||||
function texts(map: Map<number, Translated>): Record<number, string> {
|
function texts(map: Map<number, Translated>): Record<number, string> {
|
||||||
@@ -578,7 +578,7 @@ describe('属性表', () => {
|
|||||||
])
|
])
|
||||||
})
|
})
|
||||||
|
|
||||||
it('说明:紧跟名称,去掉注释符号、引号、共同缩进和首尾空行,保留换行和 @ 标签行;没有文字不显示', async () => {
|
it('说明:紧跟名称,去掉注释符号、引号、共同缩进和首尾空行,保留换行;块标签拆到参数表、输出参数后面;没有文字不显示', async () => {
|
||||||
const tsSource = [
|
const tsSource = [
|
||||||
'class K {',
|
'class K {',
|
||||||
' /**',
|
' /**',
|
||||||
@@ -595,7 +595,8 @@ describe('属性表', () => {
|
|||||||
'}',
|
'}',
|
||||||
].join('\n')
|
].join('\n')
|
||||||
const tsRows = rowTexts(await describeAll(ts, 'typescript', typescriptZhTranslator, tsSource))
|
const tsRows = rowTexts(await describeAll(ts, 'typescript', typescriptZhTranslator, tsSource))
|
||||||
expect(tsRows[2].slice(0, 3)).toEqual(['名称: load', '说明: 读配置。\n\n例子:\n load(\'a\')\n@param path 文件路径\n@returns 配置', '访问级别: 公共'])
|
expect(tsRows[2].slice(0, 3)).toEqual(['名称: load', '说明: 读配置。\n\n例子:\n load(\'a\')', '访问级别: 公共'])
|
||||||
|
expect(tsRows[2].slice(-3)).toEqual(['输入参数: 1 个', ' path | 字符串 | | 文件路径', '输出参数: 未标注 — 配置'])
|
||||||
expect(tsRows[11].slice(0, 2)).toEqual(['名称: empty', '访问级别: 公共'])
|
expect(tsRows[11].slice(0, 2)).toEqual(['名称: empty', '访问级别: 公共'])
|
||||||
|
|
||||||
const pySource = [
|
const pySource = [
|
||||||
@@ -615,7 +616,7 @@ describe('属性表', () => {
|
|||||||
expect(pyRows[8].slice(0, 2)).toEqual(['名称: g', '访问级别: 公共'])
|
expect(pyRows[8].slice(0, 2)).toEqual(['名称: g', '访问级别: 公共'])
|
||||||
})
|
})
|
||||||
|
|
||||||
it('注释块和函数的说明是 Markdown 原文:只去注释符号、引号和共同缩进,reST 角色、双反引号、JSDoc 标签、HTML 照抄', async () => {
|
it('注释块和函数的说明是 Markdown 原文:只去注释符号、引号和共同缩进,reST 角色、双反引号、{@link}、HTML 照抄,块标签不进正文', async () => {
|
||||||
const pySource = [
|
const pySource = [
|
||||||
'"""手机号规范化 / 脱敏工具.',
|
'"""手机号规范化 / 脱敏工具.',
|
||||||
'',
|
'',
|
||||||
@@ -662,7 +663,7 @@ describe('属性表', () => {
|
|||||||
expect(tsText[3]).toBe('单行块注释')
|
expect(tsText[3]).toBe('单行块注释')
|
||||||
const tsRows = await describeAll(ts, 'typescript', typescriptZhTranslator, tsSource)
|
const tsRows = await describeAll(ts, 'typescript', typescriptZhTranslator, tsSource)
|
||||||
expect(tsRows.get(4)?.find(row => row.label === '说明')?.value).toEqual({
|
expect(tsRows.get(4)?.find(row => row.label === '说明')?.value).toEqual({
|
||||||
markdown: '读配置,见 {@link load}。\n\n```ts\nload(\'a\')\n```\n@param path 文件路径',
|
markdown: '读配置,见 {@link load}。\n\n```ts\nload(\'a\')\n```',
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -854,12 +855,12 @@ describe('属性表', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
describe('源码下标(跳到定义用)', () => {
|
describe('源码下标(跳到定义用)', () => {
|
||||||
function itemCells(rows: PropertyRow[] | undefined, label: string): Inline[][] {
|
function itemCells(rows: PropertyRow[] | undefined, label: string): PreviewText[] {
|
||||||
return rows?.find(row => row.label === label)?.items?.flatMap(item => item.cells) ?? []
|
return rows?.find(row => row.label === label)?.items?.flatMap(item => item.cells) ?? []
|
||||||
}
|
}
|
||||||
|
|
||||||
function propertySegments(rows: PropertyRow[]): Segment[] {
|
function propertySegments(rows: PropertyRow[]): Segment[] {
|
||||||
const itemSegments = (items: PropertyItem[] | undefined): Segment[] => (items ?? []).flatMap(item => [...flatSegments(item.cells.flat()), ...itemSegments(item.items)])
|
const itemSegments = (items: PropertyItem[] | undefined): Segment[] => (items ?? []).flatMap(item => [...flatSegments(item.cells.flatMap(inlineOf)), ...itemSegments(item.items)])
|
||||||
return rows.flatMap(row => [...('markdown' in row.value ? [] : flatSegments(row.value)), ...itemSegments(row.items)])
|
return rows.flatMap(row => [...('markdown' in row.value ? [] : flatSegments(row.value)), ...itemSegments(row.items)])
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1033,6 +1034,82 @@ describe('源码下标(跳到定义用)', () => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe('说明里的块标签', () => {
|
||||||
|
it('render_conversation_brief:@param 进输入参数表的说明列,@return 跟在输出参数后面,说明里只剩正文', async () => {
|
||||||
|
const rows = await describeAll(py, 'python', pythonZhTranslator, readFixture('conversationBrief.py'))
|
||||||
|
const lines = rowTexts(rows)[25]
|
||||||
|
expect(lines.find(line => line.startsWith('说明'))).toBe('说明: 把对话转写成「前情摘要 + 近期消息」一段文字; 什么都没有时返回空串.')
|
||||||
|
expect(lines.join('\n')).not.toMatch(/@param|@return/)
|
||||||
|
const params = rows.get(25)?.find(row => row.label === '输入参数')
|
||||||
|
expect(params?.columns).toEqual(['名称', '类型', '默认值', '说明'])
|
||||||
|
expect(params?.items?.map(item => [text(item.cells[0]), text(item.cells[3])])).toEqual([
|
||||||
|
['messages', '对话历史 (`messages` 状态).'],
|
||||||
|
['summary', '更早对话的压缩摘要, 没有就传 `None`.'],
|
||||||
|
['speakers', '转写谁说的话: [USER] / [ASSISTANT]; 只要用户原话时只传 [USER].'],
|
||||||
|
['max_lines', '近期消息最多留几行, 多了从头丢弃并注明.'],
|
||||||
|
])
|
||||||
|
expect(params?.items?.[3].cells[3]).toEqual({ markdown: '近期消息最多留几行, 多了从头丢弃并注明.' })
|
||||||
|
const output = rows.get(25)?.find(row => row.label === '输出参数')
|
||||||
|
expect(output?.value).toEqual([{ text: '字符串', role: 'type' }])
|
||||||
|
expect(output?.note).toEqual({ markdown: '以 `[会话进展]` 开头的转写.' })
|
||||||
|
expect(lines.some(line => line.startsWith('对不上的参数'))).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('gate.py:@throws 成「异常」表,第一行带完整链接,第二行是不带路径的引用', async () => {
|
||||||
|
const rows = await describeAll(py, 'python', pythonZhTranslator, readFixture('localRuntimeGate.py'))
|
||||||
|
const lines = rowTexts(rows)[18]
|
||||||
|
expect(lines.join('\n')).not.toMatch(/@throws/)
|
||||||
|
expect(lines.slice(-3)).toEqual([
|
||||||
|
'异常: 2 个',
|
||||||
|
' [DesktopOnly](backend/src/luffy_agent/sandbox/local_runtime/visibility.py#DesktopOnly) | 请求不是桌面端发来的',
|
||||||
|
' [LocalRuntimeDisabled] | 总开关关着',
|
||||||
|
])
|
||||||
|
const throws = rows.get(18)?.find(row => row.label === '异常')
|
||||||
|
expect(throws?.columns).toEqual(['异常', '触发条件'])
|
||||||
|
expect(throws?.items?.[1].cells).toEqual([{ markdown: '[LocalRuntimeDisabled]' }, { markdown: '总开关关着' }])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('名字对不上的 @param 排在说明下面,提示色,悬停「没有这个参数」;没有对上的参数时参数表不加说明列', async () => {
|
||||||
|
const source = [
|
||||||
|
'/**',
|
||||||
|
' * 读文件。',
|
||||||
|
' * @param pth 路径',
|
||||||
|
' * @throws Error 读不到',
|
||||||
|
' */',
|
||||||
|
'export function read(path: string): string { return \'\' }',
|
||||||
|
].join('\n')
|
||||||
|
const rows = await describeAll(ts, 'typescript', typescriptZhTranslator, source)
|
||||||
|
const lines = rowTexts(rows)[1]
|
||||||
|
expect(lines.slice(0, 4)).toEqual(['名称: read', '说明: 读文件。', '对不上的参数: 1 个', ' pth | 路径'])
|
||||||
|
const unmatched = rows.get(1)?.find(row => row.label === '对不上的参数')
|
||||||
|
expect(unmatched?.items?.[0].cells[0]).toEqual([{ text: 'pth', role: 'hint', tooltip: '没有这个参数' }])
|
||||||
|
expect(rows.get(1)?.find(row => row.label === '输入参数')?.columns).toEqual(['名称', '类型', '默认值'])
|
||||||
|
expect(lines.slice(-2)).toEqual(['异常: 1 个', ' [Error] | 读不到'])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('类:没有构造参数表可对,@param 照写成「构造参数」表;异常类的 @param 对到构造参数表,*args 去掉星号对名字', async () => {
|
||||||
|
const source = [
|
||||||
|
'class K:',
|
||||||
|
' """类。',
|
||||||
|
'',
|
||||||
|
' @param a 甲',
|
||||||
|
' """',
|
||||||
|
'',
|
||||||
|
'class E(Exception):',
|
||||||
|
' """出错了。',
|
||||||
|
'',
|
||||||
|
' @param code 错误码',
|
||||||
|
' @param args 其余参数',
|
||||||
|
' """',
|
||||||
|
' def __init__(self, code: int, *args):',
|
||||||
|
' pass',
|
||||||
|
].join('\n')
|
||||||
|
const lines = rowTexts(await describeAll(py, 'python', pythonZhTranslator, source))
|
||||||
|
expect(lines[1]).toEqual(['名称: K', '说明: 类。', '访问级别: 公共', '构造参数: 1 个', ' a | 甲'])
|
||||||
|
expect(lines[7].slice(-3)).toEqual(['构造参数: 2 个', ' code | 整数 | | 错误码', ' *args | 未标注 | | 其余参数'])
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
describe('跨几行写的调用', () => {
|
describe('跨几行写的调用', () => {
|
||||||
const routes = readFixture('routes.py')
|
const routes = readFixture('routes.py')
|
||||||
const calls = readFixture('calls.ts.txt')
|
const calls = readFixture('calls.ts.txt')
|
||||||
@@ -1189,10 +1266,10 @@ describe('跨几行写的调用', () => {
|
|||||||
expect(asked).toEqual(expect.arrayContaining(['PaymentCapabilityDiagnostic', 'CredentialPresence']))
|
expect(asked).toEqual(expect.arrayContaining(['PaymentCapabilityDiagnostic', 'CredentialPresence']))
|
||||||
const nested = line?.table?.items[6]
|
const nested = line?.table?.items[6]
|
||||||
expect(nested?.table?.columns).toEqual(['参数', '值'])
|
expect(nested?.table?.columns).toEqual(['参数', '值'])
|
||||||
expect(nested?.cells[1].find(piece => 'role' in piece && piece.text === 'CredentialPresence')).toEqual({ text: 'CredentialPresence', role: 'type', offset: payment.indexOf('CredentialPresence(', payment.indexOf('credentials_present=')) })
|
expect(inlineOf(nested?.cells[1] ?? []).find(piece => 'role' in piece && piece.text === 'CredentialPresence')).toEqual({ text: 'CredentialPresence', role: 'type', offset: payment.indexOf('CredentialPresence(', payment.indexOf('credentials_present=')) })
|
||||||
expect(nested?.table?.items[0].cells[0]).toEqual([{ text: 'mp_app_secret', role: 'variable', offset: payment.indexOf('mp_app_secret=') }])
|
expect(nested?.table?.items[0].cells[0]).toEqual([{ text: 'mp_app_secret', role: 'variable', offset: payment.indexOf('mp_app_secret=') }])
|
||||||
const unresolved = (await translateAll(py, 'python', pythonZhTranslator, payment)).get(64)
|
const unresolved = (await translateAll(py, 'python', pythonZhTranslator, payment)).get(64)
|
||||||
expect(segmentText(unresolved?.table?.items[6].cells[1] ?? [])).toBe('调用 CredentialPresence · 4 个参数')
|
expect(text(unresolved?.table?.items[6].cells[1])).toBe('调用 CredentialPresence · 4 个参数')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('嵌套一层层往下套;await 的调用写「等待」', async () => {
|
it('嵌套一层层往下套;await 的调用写「等待」', async () => {
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
import type { Access, AttributeNames, BaseFacts, BlockKind, CallArgument, CallFacts, ClassFacts, CommandContext, CommandFacts, CommandForm, CommandReading, CommandRule, CommandWord, CommonAttribute, DeclaratorFacts, Defines, DirectiveFacts, EmbeddedReading, EntryFacts, EnumFacts, ExceptionFacts, FactBlock, FunctionFacts, HealthFacts, ImportFacts, ImportItem, Inline, InstructionFlag, InterpreterFacts, IterationClause, IterationFacts, IterationResult, LanguageAdapter, Markdown, Node, OptionsFacts, ParameterFacts, PositionalParameter, PreviewTable, PropertyItem, PropertyRow, Redirect, Segment, SegmentGroup, SourceDocument, SourceRange, SourceSpan, StageFacts, StartFacts, TextBlock, TextTranslation, TokenRole, Translator, VariableBlock } from '../contract'
|
import type { Access, AttributeNames, BaseFacts, BlockKind, CallArgument, CallFacts, ClassFacts, CommandContext, CommandFacts, CommandForm, CommandReading, CommandRule, CommandWord, CommonAttribute, DeclaratorFacts, Defines, DirectiveFacts, Doc, EmbeddedReading, EntryFacts, EnumFacts, ExceptionFacts, FactBlock, FunctionFacts, HealthFacts, ImportFacts, ImportItem, Inline, InstructionFlag, InterpreterFacts, IterationClause, IterationFacts, IterationResult, LanguageAdapter, Markdown, Node, OptionsFacts, ParameterFacts, PositionalParameter, PreviewTable, PropertyItem, PropertyRow, Redirect, Segment, SegmentGroup, SourceDocument, SourceRange, SourceSpan, StageFacts, StartFacts, TextBlock, TextTranslation, TokenRole, Translator, VariableBlock } from '../contract'
|
||||||
|
import type { ParamDocs } from '../rules/docRows'
|
||||||
|
import { readDoc } from '../docTags'
|
||||||
import { commonIndent, splitLines } from '../editText'
|
import { commonIndent, splitLines } from '../editText'
|
||||||
|
import { docBodyRows, paramDocCell, paramDocs, returnNote, throwsRows, unmatchedParamRows, writtenParamRows } from '../rules/docRows'
|
||||||
|
|
||||||
/** 按所在语法节点区分译法的关键字,例如 Python 的 else:if 后「否则」,for / while 后「未中断时」。 */
|
/** 按所在语法节点区分译法的关键字,例如 Python 的 else:if 后「否则」,for / while 后「未中断时」。 */
|
||||||
export interface ContextualWord {
|
export interface ContextualWord {
|
||||||
@@ -46,6 +49,11 @@ export interface ZhTable {
|
|||||||
stringNodes: string[]
|
stringNodes: string[]
|
||||||
/** 注释(或 docstring)节点去掉注释符号后的原文,可以多行;text 是节点的源码去掉代码阅读的记号(SourceDocument.markers)以后剩下的。 */
|
/** 注释(或 docstring)节点去掉注释符号后的原文,可以多行;text 是节点的源码去掉代码阅读的记号(SourceDocument.markers)以后剩下的。 */
|
||||||
commentText: (node: Node, text: string) => string
|
commentText: (node: Node, text: string) => string
|
||||||
|
/**
|
||||||
|
* 说明是连着几条行注释写的(Rust 的 `///`)时,从第一条(facts.doc)起这一段的各条注释节点,照源码顺序;
|
||||||
|
* 不填就是那一个节点(JSDoc、docstring 本来就是一个节点)。
|
||||||
|
*/
|
||||||
|
docNodes?: (first: Node) => Node[]
|
||||||
/**
|
/**
|
||||||
* 节点模板:节点类型 → 按中文语序重排的译文。命中的节点只输出模板里的译词和槽位,节点自己的关键字、标点不再输出;
|
* 节点模板:节点类型 → 按中文语序重排的译文。命中的节点只输出模板里的译词和槽位,节点自己的关键字、标点不再输出;
|
||||||
* 槽位里的子节点照常递归翻译。模板引用的子节点缺了,或节点没整个落在要翻的这一段里,就不重排、照常翻。
|
* 槽位里的子节点照常递归翻译。模板引用的子节点缺了,或节点没整个落在要翻的这一段里,就不重排、照常翻。
|
||||||
@@ -661,9 +669,9 @@ export function createZhTemplateTranslator(table: ZhTable): Translator {
|
|||||||
return joinTokens(tokensIn(node, nodeSpan(node), document))
|
return joinTokens(tokensIn(node, nodeSpan(node), document))
|
||||||
}
|
}
|
||||||
|
|
||||||
function doc(node: Node, document: SourceDocument): Markdown | undefined {
|
function doc(node: Node, document: SourceDocument): Doc | undefined {
|
||||||
const markdown = docText([commentOf(node, document)])
|
const markdown = docText((table.docNodes?.(node) ?? [node]).map(part => commentOf(part, document)))
|
||||||
return markdown ? { markdown } : undefined
|
return markdown ? readDoc(markdown) : undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
function describe(block: FactBlock, document: SourceDocument): PropertyRow[] {
|
function describe(block: FactBlock, document: SourceDocument): PropertyRow[] {
|
||||||
@@ -817,8 +825,8 @@ interface Phrases {
|
|||||||
module: (node: Node) => Segment[]
|
module: (node: Node) => Segment[]
|
||||||
/** 源码里的一个名字原样照抄、按 role 着色(函数、类的名称)。 */
|
/** 源码里的一个名字原样照抄、按 role 着色(函数、类的名称)。 */
|
||||||
name: (node: Node, role: TokenRole) => Segment[]
|
name: (node: Node, role: TokenRole) => Segment[]
|
||||||
/** 函数、类的说明(JSDoc、docstring):去掉注释符号、引号和共同缩进的 Markdown 原文;没有文字时没有。 */
|
/** 函数、类的说明(JSDoc、docstring):去掉注释符号、引号和共同缩进的 Markdown 原文拆成正文和块标签;没有文字时没有。 */
|
||||||
doc: (node: Node) => Markdown | undefined
|
doc: (node: Node) => Doc | undefined
|
||||||
/** 一条要执行的命令:数组写法交给命令规则读,shell 写法是宿主读好的嵌着的代码(SourceDocument.embedded),可以带一张选项表。 */
|
/** 一条要执行的命令:数组写法交给命令规则读,shell 写法是宿主读好的嵌着的代码(SourceDocument.embedded),可以带一张选项表。 */
|
||||||
command: (form: CommandForm) => { text: Inline[], table?: PreviewTable }
|
command: (form: CommandForm) => { text: Inline[], table?: PreviewTable }
|
||||||
}
|
}
|
||||||
@@ -907,38 +915,50 @@ function aliased(item: Inline[], alias: Node | undefined, say: Phrases): Inline[
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 函数:名称 / 说明 / 访问级别 / 异步(只有分这个的语言才有)/ 装饰器 / 输入参数 / 输出参数。
|
* 函数:名称 / 说明 / 对不上的参数 / 访问级别 / 异步(只有分这个的语言才有)/ 装饰器 / 输入参数 / 输出参数 / 异常。
|
||||||
* 输入参数是一张表,列名是名称 / 类型 / 默认值,每个参数一行;没有默认值的格子留空。
|
* 输入参数是一张表,列名是名称 / 类型 / 默认值,每个参数一行;没有默认值的格子留空。说明里的块标签拆开放:
|
||||||
|
* `@param` 进输入参数表多出的「说明」列(有参数对上才有这一列),名字对不上的排在说明下面;`@return` 跟在输出参数的类型后面;
|
||||||
|
* `@throws` 是「异常」一行。
|
||||||
*/
|
*/
|
||||||
function functionRows(facts: FunctionFacts, say: Phrases): PropertyRow[] {
|
function functionRows(facts: FunctionFacts, say: Phrases): PropertyRow[] {
|
||||||
|
const doc = facts.doc && say.doc(facts.doc)
|
||||||
|
const names = facts.positional ? facts.positional.flatMap(param => param.name ? [param.name.text] : []) : facts.parameters.map(param => param.name.text)
|
||||||
|
const docs = paramDocs(doc, names)
|
||||||
return [
|
return [
|
||||||
{ label: '名称', value: facts.name ? say.name(facts.name, 'function') : [hint('匿名')] },
|
{ label: '名称', value: facts.name ? say.name(facts.name, 'function') : [hint('匿名')] },
|
||||||
...docRows(facts.doc, say),
|
...docBodyRows(doc),
|
||||||
|
...unmatchedParamRows(docs.unmatched),
|
||||||
...accessRows(facts.access),
|
...accessRows(facts.access),
|
||||||
...flagRows('async', facts.async),
|
...flagRows('async', facts.async),
|
||||||
...decoratorRows(facts.decorators, say),
|
...decoratorRows(facts.decorators, say),
|
||||||
...(facts.positional
|
...(facts.positional
|
||||||
? positionalRows(facts, facts.positional, say)
|
? positionalRows(facts, facts.positional, say, docs, doc)
|
||||||
: [
|
: [
|
||||||
parameterRow('输入参数', facts.parameters, say),
|
parameterRow('输入参数', facts.parameters, say, docs),
|
||||||
{ label: '输出参数', value: facts.returnType ? say.translated(facts.returnType) : [hint('未标注')] },
|
{ label: '输出参数', value: facts.returnType ? say.translated(facts.returnType) : [hint('未标注')], ...returnNote(doc) },
|
||||||
]),
|
]),
|
||||||
|
...throwsRows(doc),
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 按位置取参数的函数(shell):输入参数写「N 个,按位置」,下面一张表「位置 | 函数里叫」(没起名的写 $N);
|
* 按位置取参数的函数(shell):输入参数写「N 个,按位置」,下面一张表「位置 | 函数里叫」(没起名的写 $N;`@param` 按起的名字对上时多一列说明);
|
||||||
* 输出是它打印的那几句(调用处用 $(函数 …) 拿去用时注明);返回是写了的 return 值,没写 return、也不打印时是最后一句成不成功。
|
* 输出是它打印的那几句(调用处用 $(函数 …) 拿去用时注明);返回是写了的 return 值,没写 return、也不打印时是最后一句成不成功。
|
||||||
*/
|
*/
|
||||||
function positionalRows(facts: FunctionFacts, positional: PositionalParameter[], say: Phrases): PropertyRow[] {
|
function positionalRows(facts: FunctionFacts, positional: PositionalParameter[], say: Phrases, docs: ParamDocs, doc: Doc | undefined): PropertyRow[] {
|
||||||
|
const described = positional.some(param => param.name && docs.of(param.name.text))
|
||||||
const inputs: PropertyRow = positional.length === 0
|
const inputs: PropertyRow = positional.length === 0
|
||||||
? { label: '输入参数', value: [hint('无')] }
|
? { label: '输入参数', value: [hint('无')] }
|
||||||
: {
|
: {
|
||||||
label: '输入参数',
|
label: '输入参数',
|
||||||
value: [hint(`${positional.length} 个,按位置`)],
|
value: [hint(`${positional.length} 个,按位置`)],
|
||||||
columns: ['位置', '函数里叫'],
|
columns: described ? ['位置', '函数里叫', '说明'] : ['位置', '函数里叫'],
|
||||||
items: positional.map(param => ({
|
items: positional.map(param => ({
|
||||||
cells: [[hint(`第 ${param.position} 个`)], param.name ? say.name(param.name, 'variable') : [segment(`$${param.position}`, 'variable', undefined)]],
|
cells: [
|
||||||
|
[hint(`第 ${param.position} 个`)],
|
||||||
|
param.name ? say.name(param.name, 'variable') : [segment(`$${param.position}`, 'variable', undefined)],
|
||||||
|
...(described ? [param.name ? paramDocCell(docs, param.name.text) : []] : []),
|
||||||
|
],
|
||||||
...defining(param.name),
|
...defining(param.name),
|
||||||
})),
|
})),
|
||||||
}
|
}
|
||||||
@@ -950,7 +970,9 @@ function positionalRows(facts: FunctionFacts, positional: PositionalParameter[],
|
|||||||
const result: PropertyRow[] = values.length > 0
|
const result: PropertyRow[] = values.length > 0
|
||||||
? [{ label: '返回', value: joined(values, '、') }]
|
? [{ label: '返回', value: joined(values, '、') }]
|
||||||
: prints.length === 0 ? [{ label: '返回', value: [hint('最后一句成不成功')] }] : []
|
: prints.length === 0 ? [{ label: '返回', value: [hint('最后一句成不成功')] }] : []
|
||||||
return [inputs, ...output, ...result]
|
// `@return` 跟在返回那一行后面;只有打印、没写 return 时跟在输出那一行后面
|
||||||
|
const [noted] = [...result, ...output]
|
||||||
|
return [inputs, ...[...output, ...result].map(row => row === noted ? { ...row, ...returnNote(doc) } : row)]
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 解释器:叫什么,怎么找到它(按 PATH 找,或写死的路径)。 */
|
/** 解释器:叫什么,怎么找到它(按 PATH 找,或写死的路径)。 */
|
||||||
@@ -991,7 +1013,7 @@ function flagValue(flag: InstructionFlag): Inline[] {
|
|||||||
* 接着前面某个阶段建的写「阶段 X」)/ 平台(写了 --platform 才有)。
|
* 接着前面某个阶段建的写「阶段 X」)/ 平台(写了 --platform 才有)。
|
||||||
*/
|
*/
|
||||||
function stageRows(facts: StageFacts, say: Phrases): PropertyRow[] {
|
function stageRows(facts: StageFacts, say: Phrases): PropertyRow[] {
|
||||||
const doc = (facts.doc ?? []).map(node => say.doc(node)?.markdown).filter(Boolean).join('\n')
|
const doc = (facts.doc ?? []).map(node => say.doc(node)?.body?.markdown).filter(Boolean).join('\n')
|
||||||
const platform = facts.flags.find(flag => flag.name === 'platform')
|
const platform = facts.flags.find(flag => flag.name === 'platform')
|
||||||
const image: Inline[] = facts.stage
|
const image: Inline[] = facts.stage
|
||||||
? [segment('阶段', 'keyword', undefined), segment(' ', 'plain', undefined), ...say.name(facts.image, 'variable')]
|
? [segment('阶段', 'keyword', undefined), segment(' ', 'plain', undefined), ...say.name(facts.image, 'variable')]
|
||||||
@@ -1078,17 +1100,22 @@ function scopedVariableRows(declarators: DeclaratorFacts[], scope: 'build' | 'en
|
|||||||
}]
|
}]
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 参数表:列名是名称 / 类型 / 默认值,每个参数一行,讲的定义是参数名;没有默认值的格子留空。 */
|
/**
|
||||||
function parameterRow(label: string, parameters: ParameterFacts[], say: Phrases): PropertyRow {
|
* 参数表:列名是名称 / 类型 / 默认值,每个参数一行,讲的定义是参数名;没有默认值的格子留空。
|
||||||
|
* 说明里有参数对上了(docs)时多一列「说明」,没对上的格子留空。
|
||||||
|
*/
|
||||||
|
function parameterRow(label: string, parameters: ParameterFacts[], say: Phrases, docs: ParamDocs): PropertyRow {
|
||||||
|
const described = parameters.some(param => docs.of(param.name.text))
|
||||||
return {
|
return {
|
||||||
label,
|
label,
|
||||||
value: count(parameters.length),
|
value: count(parameters.length),
|
||||||
columns: ['名称', '类型', '默认值'],
|
columns: described ? ['名称', '类型', '默认值', '说明'] : ['名称', '类型', '默认值'],
|
||||||
items: parameters.map(param => ({
|
items: parameters.map(param => ({
|
||||||
cells: [
|
cells: [
|
||||||
param.optional ? [...say.translated(param.name), { text: '?', role: 'plain' }] : say.translated(param.name),
|
param.optional ? [...say.translated(param.name), { text: '?', role: 'plain' }] : say.translated(param.name),
|
||||||
param.type ? say.translated(param.type) : [hint('未标注')],
|
param.type ? say.translated(param.type) : [hint('未标注')],
|
||||||
param.default ? say.translated(param.default) : [],
|
param.default ? say.translated(param.default) : [],
|
||||||
|
...(described ? [paramDocCell(docs, param.name.text)] : []),
|
||||||
],
|
],
|
||||||
...defining(param.name),
|
...defining(param.name),
|
||||||
})),
|
})),
|
||||||
@@ -1098,19 +1125,23 @@ function parameterRow(label: string, parameters: ParameterFacts[], say: Phrases)
|
|||||||
/**
|
/**
|
||||||
* 类:名称 / 说明 / 访问级别 / 抽象(只有分这个的语言才有)/ 装饰器 / 类型参数 / 父类 / 实现的接口,
|
* 类:名称 / 说明 / 访问级别 / 抽象(只有分这个的语言才有)/ 装饰器 / 类型参数 / 父类 / 实现的接口,
|
||||||
* 说明及以后各行有才显示;类型参数、父类、实现的接口各是一行,多项用逗号隔开。类型参数那一行讲的是这几个泛型参数的定义。
|
* 说明及以后各行有才显示;类型参数、父类、实现的接口各是一行,多项用逗号隔开。类型参数那一行讲的是这几个泛型参数的定义。
|
||||||
|
* 类没有构造参数表可对,说明里的 `@param` 照写成「构造参数」一张「名称 | 说明」表;`@throws` 是「异常」一行。
|
||||||
*/
|
*/
|
||||||
function classRows(facts: ClassFacts, say: Phrases): PropertyRow[] {
|
function classRows(facts: ClassFacts, say: Phrases): PropertyRow[] {
|
||||||
const listRow = (label: string, parts: Inline[][], defines: Defines = {}): PropertyRow[] =>
|
const listRow = (label: string, parts: Inline[][], defines: Defines = {}): PropertyRow[] =>
|
||||||
parts.length > 0 ? [{ label, value: joined(parts, ', '), ...defines }] : []
|
parts.length > 0 ? [{ label, value: joined(parts, ', '), ...defines }] : []
|
||||||
|
const doc = facts.doc && say.doc(facts.doc)
|
||||||
return [
|
return [
|
||||||
{ label: '名称', value: facts.name ? say.name(facts.name, 'type') : [hint('匿名')] },
|
{ label: '名称', value: facts.name ? say.name(facts.name, 'type') : [hint('匿名')] },
|
||||||
...docRows(facts.doc, say),
|
...docBodyRows(doc),
|
||||||
...accessRows(facts.access),
|
...accessRows(facts.access),
|
||||||
...flagRows('abstract', facts.abstract),
|
...flagRows('abstract', facts.abstract),
|
||||||
...decoratorRows(facts.decorators, say),
|
...decoratorRows(facts.decorators, say),
|
||||||
...listRow('类型参数', facts.typeParameters.map(say.translated), defining(...facts.typeParameters)),
|
...listRow('类型参数', facts.typeParameters.map(say.translated), defining(...facts.typeParameters)),
|
||||||
...listRow('父类', facts.bases.map(base => baseText(base, say))),
|
...listRow('父类', facts.bases.map(base => baseText(base, say))),
|
||||||
...listRow('实现的接口', facts.implements.map(say.translated)),
|
...listRow('实现的接口', facts.implements.map(say.translated)),
|
||||||
|
...writtenParamRows(doc?.params ?? [], '构造参数'),
|
||||||
|
...throwsRows(doc),
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1129,7 +1160,7 @@ function enumRows(facts: EnumFacts, say: Phrases): PropertyRow[] {
|
|||||||
const described = docs.some(Boolean)
|
const described = docs.some(Boolean)
|
||||||
return [
|
return [
|
||||||
{ label: '名称', value: say.name(facts.name, 'type') },
|
{ label: '名称', value: say.name(facts.name, 'type') },
|
||||||
...docRows(facts.doc, say),
|
...docBodyRows(facts.doc && say.doc(facts.doc)),
|
||||||
...accessRows(facts.access),
|
...accessRows(facts.access),
|
||||||
...decoratorRows(facts.decorators, say),
|
...decoratorRows(facts.decorators, say),
|
||||||
...baseRows(facts.bases, say),
|
...baseRows(facts.bases, say),
|
||||||
@@ -1153,6 +1184,7 @@ function enumRows(facts: EnumFacts, say: Phrases): PropertyRow[] {
|
|||||||
* 异常(Python、TypeScript 同一张表):名称 / 说明 / 访问级别 / 装饰器 / 继承(照写的父类),说明及以后有才显示;
|
* 异常(Python、TypeScript 同一张表):名称 / 说明 / 访问级别 / 装饰器 / 继承(照写的父类),说明及以后有才显示;
|
||||||
* 类属性收成一张表:属性 | 类型 | 值 | 说明,一个属性都没写类型时没有「类型」列、都没有说明时没有「说明」列,没写的格子留空;
|
* 类属性收成一张表:属性 | 类型 | 值 | 说明,一个属性都没写类型时没有「类型」列、都没有说明时没有「说明」列,没写的格子留空;
|
||||||
* 构造参数同函数的输入参数表。没有类属性、没写构造函数时没有这两行。
|
* 构造参数同函数的输入参数表。没有类属性、没写构造函数时没有这两行。
|
||||||
|
* 说明里的 `@param` 对到构造参数表(同函数的输入参数,对不上的排在说明下面;没写构造函数时同类,照写成一张表),`@throws` 是「异常」一行。
|
||||||
*/
|
*/
|
||||||
function exceptionRows(facts: ExceptionFacts, say: Phrases): PropertyRow[] {
|
function exceptionRows(facts: ExceptionFacts, say: Phrases): PropertyRow[] {
|
||||||
const docs = facts.fields.map(field => docsText(field.docs, say))
|
const docs = facts.fields.map(field => docsText(field.docs, say))
|
||||||
@@ -1172,14 +1204,18 @@ function exceptionRows(facts: ExceptionFacts, say: Phrases): PropertyRow[] {
|
|||||||
...defining(field.name),
|
...defining(field.name),
|
||||||
})),
|
})),
|
||||||
}
|
}
|
||||||
|
const doc = facts.doc && say.doc(facts.doc)
|
||||||
|
const params = facts.parameters && paramDocs(doc, facts.parameters.map(param => param.name.text))
|
||||||
return [
|
return [
|
||||||
{ label: '名称', value: facts.name ? say.name(facts.name, 'type') : [hint('匿名')] },
|
{ label: '名称', value: facts.name ? say.name(facts.name, 'type') : [hint('匿名')] },
|
||||||
...docRows(facts.doc, say),
|
...docBodyRows(doc),
|
||||||
|
...unmatchedParamRows(params?.unmatched ?? []),
|
||||||
...accessRows(facts.access),
|
...accessRows(facts.access),
|
||||||
...decoratorRows(facts.decorators, say),
|
...decoratorRows(facts.decorators, say),
|
||||||
...baseRows(facts.bases, say),
|
...baseRows(facts.bases, say),
|
||||||
...(facts.fields.length > 0 ? [fields] : []),
|
...(facts.fields.length > 0 ? [fields] : []),
|
||||||
...(facts.parameters ? [parameterRow('构造参数', facts.parameters, say)] : []),
|
...(facts.parameters && params ? [parameterRow('构造参数', facts.parameters, say, params)] : writtenParamRows(doc?.params ?? [], '构造参数')),
|
||||||
|
...throwsRows(doc),
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1188,21 +1224,15 @@ function baseRows(bases: BaseFacts[], say: Phrases): PropertyRow[] {
|
|||||||
return bases.length > 0 ? [{ label: '继承', value: joined(bases.map(base => baseText(base, say)), ', ') }] : []
|
return bases.length > 0 ? [{ label: '继承', value: joined(bases.map(base => baseText(base, say)), ', ') }] : []
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 表格里一格说明:几段说明文字的原文用空格连起来。 */
|
/** 表格里一格说明:几段说明文字的正文用空格连起来。 */
|
||||||
function docsText(nodes: Node[], say: Phrases): string {
|
function docsText(nodes: Node[], say: Phrases): string {
|
||||||
return nodes.map(node => say.doc(node)?.markdown).filter(Boolean).join(' ')
|
return nodes.map(node => say.doc(node)?.body?.markdown).filter(Boolean).join(' ')
|
||||||
}
|
}
|
||||||
|
|
||||||
function docCell(text: string): Inline[] {
|
function docCell(text: string): Inline[] {
|
||||||
return text ? [segment(text, 'comment', undefined)] : []
|
return text ? [segment(text, 'comment', undefined)] : []
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 函数、类、常量、变量的说明:有文字才有这一行。 */
|
|
||||||
function docRows(doc: Node | undefined, say: Phrases): PropertyRow[] {
|
|
||||||
const markdown = doc && say.doc(doc)
|
|
||||||
return markdown ? [{ label: '说明', value: markdown }] : []
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 装饰器:源码原样照抄,多个换行。 */
|
/** 装饰器:源码原样照抄,多个换行。 */
|
||||||
function decoratorRows(decorators: Node[], say: Phrases): PropertyRow[] {
|
function decoratorRows(decorators: Node[], say: Phrases): PropertyRow[] {
|
||||||
return decorators.length > 0 ? [{ label: '装饰器', value: joined(decorators.map(say.verbatim), '\n') }] : []
|
return decorators.length > 0 ? [{ label: '装饰器', value: joined(decorators.map(say.verbatim), '\n') }] : []
|
||||||
@@ -1238,7 +1268,7 @@ function variableRows(block: VariableBlock, say: Phrases): PropertyRow[] {
|
|||||||
const valueLabel = block.kind === 'const' ? '值' : '初始值'
|
const valueLabel = block.kind === 'const' ? '值' : '初始值'
|
||||||
return block.facts.declarators.flatMap((declarator, i) => [
|
return block.facts.declarators.flatMap((declarator, i) => [
|
||||||
{ label: '名称', value: say.translated(declarator.name), ...defining(declarator.name) },
|
{ label: '名称', value: say.translated(declarator.name), ...defining(declarator.name) },
|
||||||
...(i === 0 ? docRows(block.facts.doc, say) : []),
|
...(i === 0 ? docBodyRows(block.facts.doc && say.doc(block.facts.doc)) : []),
|
||||||
...accessRows(block.facts.access),
|
...accessRows(block.facts.access),
|
||||||
...(declarator.type ? [{ label: '类型', value: say.translated(declarator.type) }] : []),
|
...(declarator.type ? [{ label: '类型', value: say.translated(declarator.type) }] : []),
|
||||||
{ label: valueLabel, ...(declarator.value ? declaredValue(declarator.value, declarator.entries, say) : { value: [hint('无')] }) },
|
{ label: valueLabel, ...(declarator.value ? declaredValue(declarator.value, declarator.entries, say) : { value: [hint('无')] }) },
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import type { CSSProperties, ReactElement, ReactNode } from 'react'
|
import type { CSSProperties, ReactElement, ReactNode } from 'react'
|
||||||
import type { BlockView, Inline, InlinePart, ItemDiff, ItemsDiff, KindBadge, PropertyItem, PropertyRow, Removed, RowsDiff } from './types'
|
import type { BlockView, ItemDiff, ItemsDiff, KindBadge, PreviewText, PropertyItem, PropertyRow, Removed, RowDiff, RowsDiff, TextDiff } from './types'
|
||||||
import { ChevronDown, ChevronRight } from 'lucide-react'
|
import { ChevronDown, ChevronRight } from 'lucide-react'
|
||||||
import { Fragment } from 'react'
|
import { Fragment } from 'react'
|
||||||
|
import { cn } from '@/lib/utils'
|
||||||
import { DiffInline, DiffText } from './DiffText'
|
import { DiffInline, DiffText } from './DiffText'
|
||||||
import { inlineText } from './segments'
|
import { inlineText } from './segments'
|
||||||
import { SegmentText } from './SegmentText'
|
import { SegmentText } from './SegmentText'
|
||||||
@@ -51,23 +52,51 @@ function removedAt<T>(removed: readonly Removed<T>[] | undefined, after: number)
|
|||||||
return (removed ?? []).flatMap((entry, index) => entry.after === after ? [{ value: entry.value, index }] : [])
|
return (removed ?? []).flatMap((entry, index) => entry.after === after ? [{ value: entry.value, index }] : [])
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 一格的字:这一格的值改了就逐截标(DiffInline),否则照常。 */
|
/** 一格是不是成块排的 Markdown 说明(参数的说明、异常的触发条件):它不能放进 .cr-code 的段落里。 */
|
||||||
function CellText({ cell, parts }: { cell: Inline[], parts: InlinePart[] | undefined }): ReactElement {
|
function isBlock(cell: PreviewText, parts: TextDiff | undefined): boolean {
|
||||||
return parts ? <DiffInline parts={parts} /> : <SegmentText segments={cell} />
|
return 'markdown' in cell || (parts !== undefined && 'markdown' in parts)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 一格的字:着色的词照常,改了的逐截标(DiffInline);Markdown 说明照常排版,改了的逐词标(DiffText)。 */
|
||||||
|
function CellText({ cell, parts }: { cell: PreviewText, parts: TextDiff | undefined }): ReactElement {
|
||||||
|
if (parts)
|
||||||
|
return 'inline' in parts ? <DiffInline parts={parts.inline} /> : <DiffText diff={parts} className="min-w-0" />
|
||||||
|
return 'markdown' in cell ? <TextPreview text={cell} className="min-w-0" /> : <SegmentText segments={cell} />
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 一格连外面那层:着色的词排在 .cr-code 的段落里,Markdown 说明是块,lead(差异记号、展开按钮)排在它左边。 */
|
||||||
|
function CellBox({ cell, parts, className, lead, ...attrs }: { cell: PreviewText, parts: TextDiff | undefined, className?: string, lead?: ReactNode, role?: string }): ReactElement {
|
||||||
|
if (isBlock(cell, parts)) {
|
||||||
|
return (
|
||||||
|
<div className={cn('flex min-w-0 items-start gap-1', className)} {...attrs}>
|
||||||
|
{lead}
|
||||||
|
<CellText cell={cell} parts={parts} />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<p className={cn('cr-code min-w-0', className)} {...attrs}>
|
||||||
|
{lead}
|
||||||
|
<CellText cell={cell} parts={parts} />
|
||||||
|
</p>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 格子写成一串字(读屏念的展开 / 折叠按钮名):着色的词拼起来,Markdown 取原文。 */
|
||||||
|
function cellLabel(cell: PreviewText): string {
|
||||||
|
return 'markdown' in cell ? cell.markdown : inlineText(cell)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 子行的第一列(字典的键):带下一层的子行前面是折叠箭头(图标、大小、颜色同属性行的),点箭头或这一列切换;
|
* 子行的第一列(字典的键):带下一层的子行前面是折叠箭头(图标、大小、颜色同属性行的),点箭头或这一列切换;
|
||||||
* 同一层里不带下一层的子行在同样的位置留空,第一列才对得齐。
|
* 同一层里不带下一层的子行在同样的位置留空,第一列才对得齐。
|
||||||
*/
|
*/
|
||||||
function ItemHead({ id, cell, foldable, folding }: { id: string, cell: Inline[], foldable: boolean, folding: Folding }): ReactElement {
|
function ItemHead({ id, cell, foldable, folding }: { id: string, cell: PreviewText, foldable: boolean, folding: Folding }): ReactElement {
|
||||||
if (!foldable) {
|
if (!foldable) {
|
||||||
return (
|
return (
|
||||||
<div className="flex min-w-0 gap-1">
|
<div className="flex min-w-0 gap-1">
|
||||||
<span className="w-3.5 shrink-0" />
|
<span className="w-3.5 shrink-0" />
|
||||||
<p className="cr-code min-w-0">
|
<CellBox cell={cell} parts={undefined} />
|
||||||
<SegmentText segments={cell} />
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -80,15 +109,15 @@ function ItemHead({ id, cell, foldable, folding }: { id: string, cell: Inline[],
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="flex h-5 w-3.5 shrink-0 items-center rounded-sm"
|
className="flex h-5 w-3.5 shrink-0 items-center rounded-sm"
|
||||||
aria-label={`${folded ? '展开' : '折叠'}${inlineText(cell)}`}
|
aria-label={`${folded ? '展开' : '折叠'}${cellLabel(cell)}`}
|
||||||
aria-expanded={!folded}
|
aria-expanded={!folded}
|
||||||
onClick={toggle}
|
onClick={toggle}
|
||||||
>
|
>
|
||||||
<Chevron size={14} strokeWidth={1.8} className="text-[var(--vscode-descriptionForeground)] group-hover:text-[var(--vscode-foreground)]" />
|
<Chevron size={14} strokeWidth={1.8} className="text-[var(--vscode-descriptionForeground)] group-hover:text-[var(--vscode-foreground)]" />
|
||||||
</button>
|
</button>
|
||||||
<p className="cr-code min-w-0 cursor-pointer" onClick={toggle}>
|
<div className="min-w-0 cursor-pointer" onClick={toggle}>
|
||||||
<SegmentText segments={cell} />
|
<CellBox cell={cell} parts={undefined} />
|
||||||
</p>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -114,11 +143,7 @@ function PropertyItems({ parentId, items, folding, marks }: { parentId: string,
|
|||||||
>
|
>
|
||||||
{item.cells.map((cell, i) => i === 0 && nested
|
{item.cells.map((cell, i) => i === 0 && nested
|
||||||
? <ItemHead key={i} id={id} cell={cell} foldable={children.length > 0 && change !== 'del'} folding={folds} />
|
? <ItemHead key={i} id={id} cell={cell} foldable={children.length > 0 && change !== 'del'} folding={folds} />
|
||||||
: (
|
: <CellBox key={i} cell={cell} parts={mark?.cells?.[i]} />)}
|
||||||
<p key={i} className="cr-code min-w-0">
|
|
||||||
<CellText cell={cell} parts={mark?.cells?.[i]} />
|
|
||||||
</p>
|
|
||||||
))}
|
|
||||||
</div>
|
</div>
|
||||||
{children.length > 0 && !folds.collapsed.has(id) && (change === 'del'
|
{children.length > 0 && !folds.collapsed.has(id) && (change === 'del'
|
||||||
? (
|
? (
|
||||||
@@ -171,18 +196,18 @@ export function PropertyTable({ parentId, columns, items, marks, toggles }: {
|
|||||||
/** 一行:删掉的(change 是 del)不带 id,套着的表不再标;toggle 是这一行讲的子块,第一格成了展开它的按钮 */
|
/** 一行:删掉的(change 是 del)不带 id,套着的表不再标;toggle 是这一行讲的子块,第一格成了展开它的按钮 */
|
||||||
const tableRow = (item: PropertyItem, id: string, change: 'add' | 'del' | undefined, mark: ItemDiff | undefined, toggle?: ItemToggle): ReactElement => {
|
const tableRow = (item: PropertyItem, id: string, change: 'add' | 'del' | undefined, mark: ItemDiff | undefined, toggle?: ItemToggle): ReactElement => {
|
||||||
const cells = tableCells(item, columns)
|
const cells = tableCells(item, columns)
|
||||||
const text = (cell: Inline[], i: number): ReactElement => (
|
const sign = (i: number): ReactNode => signed && i === 0 && <span className="cr-diff-sign" data-change={change} aria-hidden>{SIGNS[change ?? 'none']}</span>
|
||||||
<>
|
/** 一格:讲子块的那一格是展开它的按钮(只放着色的词),别的照常(见 CellBox) */
|
||||||
{signed && i === 0 && <span className="cr-diff-sign" data-change={change} aria-hidden>{SIGNS[change ?? 'none']}</span>}
|
const cellBox = (cell: PreviewText, i: number, className?: string): ReactElement => toggle && i === 0
|
||||||
{toggle && i === 0
|
? (
|
||||||
? (
|
<p className={cn('cr-code min-w-0', className)} role="cell">
|
||||||
<button type="button" className="cr-item-toggle" aria-expanded={toggle.open} aria-label={`${toggle.open ? '收起' : '展开'}${inlineText(cell)}`} onClick={toggle.onToggle}>
|
{sign(i)}
|
||||||
<CellText cell={cell} parts={mark?.cells?.[i]} />
|
<button type="button" className="cr-item-toggle" aria-expanded={toggle.open} aria-label={`${toggle.open ? '收起' : '展开'}${cellLabel(cell)}`} onClick={toggle.onToggle}>
|
||||||
</button>
|
<CellText cell={cell} parts={mark?.cells?.[i]} />
|
||||||
)
|
</button>
|
||||||
: <CellText cell={cell} parts={mark?.cells?.[i]} />}
|
</p>
|
||||||
</>
|
)
|
||||||
)
|
: <CellBox cell={cell} parts={mark?.cells?.[i]} className={className} lead={sign(i)} role="cell" />
|
||||||
const tint = toggle?.open && toggle.badge
|
const tint = toggle?.open && toggle.badge
|
||||||
? { 'data-badge': '', 'style': { '--kind-dark': toggle.badge.color.dark, '--kind-light': toggle.badge.color.light } as CSSProperties }
|
? { 'data-badge': '', 'style': { '--kind-dark': toggle.badge.color.dark, '--kind-light': toggle.badge.color.light } as CSSProperties }
|
||||||
: {}
|
: {}
|
||||||
@@ -191,11 +216,11 @@ export function PropertyTable({ parentId, columns, items, marks, toggles }: {
|
|||||||
{cells.map((cell, i) => item.table && i === cells.length - 1
|
{cells.map((cell, i) => item.table && i === cells.length - 1
|
||||||
? (
|
? (
|
||||||
<div key={i} className="cr-table-cell flex min-w-0 flex-col gap-[3px]" role="cell">
|
<div key={i} className="cr-table-cell flex min-w-0 flex-col gap-[3px]" role="cell">
|
||||||
<p className="cr-code">{text(cell, i)}</p>
|
<CellBox cell={cell} parts={mark?.cells?.[i]} lead={sign(i)} />
|
||||||
<PropertyTable parentId={tableId(id)} columns={item.table.columns} items={item.table.items} marks={mark?.table} />
|
<PropertyTable parentId={tableId(id)} columns={item.table.columns} items={item.table.items} marks={mark?.table} />
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
: <p key={i} className="cr-table-cell cr-code min-w-0" role="cell">{text(cell, i)}</p>)}
|
: <Fragment key={i}>{cellBox(cell, i, 'cr-table-cell')}</Fragment>)}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -271,7 +296,7 @@ export function PropertyGrid({ blockId, rows, collapsed, onToggleFold, marks, sl
|
|||||||
</button>
|
</button>
|
||||||
)
|
)
|
||||||
: <span className="cr-prop-label flex h-5 items-center">{row.label}</span>}
|
: <span className="cr-prop-label flex h-5 items-center">{row.label}</span>}
|
||||||
{mark?.value ? <DiffText diff={mark.value} className="min-w-0" /> : <TextPreview text={row.value} className="min-w-0" />}
|
<RowValue row={row} mark={mark} />
|
||||||
</div>
|
</div>
|
||||||
{!folded && layout === 'table' && <PropertyTable parentId={id} columns={row.columns ?? []} items={row.items ?? []} marks={mark?.items} toggles={toggles} />}
|
{!folded && layout === 'table' && <PropertyTable parentId={id} columns={row.columns ?? []} items={row.items ?? []} marks={mark?.items} toggles={toggles} />}
|
||||||
{!folded && layout === 'list' && <PropertyItems parentId={id} items={row.items ?? []} folding={{ collapsed, onToggleFold }} marks={mark?.items} />}
|
{!folded && layout === 'list' && <PropertyItems parentId={id} items={row.items ?? []} folding={{ collapsed, onToggleFold }} marks={mark?.items} />}
|
||||||
@@ -284,6 +309,22 @@ export function PropertyGrid({ blockId, rows, collapsed, onToggleFold, marks, sl
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 一行的值:着色的词或 Markdown(改了的逐截 / 逐词标);跟着说明(PropertyRow.note,输出参数的 `@return`)时说明接在值后面,
|
||||||
|
* 同一行放得下就并排,剩的地方不到 12 个字宽时换到下一行。
|
||||||
|
*/
|
||||||
|
function RowValue({ row, mark }: { row: PropertyRow, mark: RowDiff | undefined }): ReactElement {
|
||||||
|
const value = mark?.value ? <DiffText diff={mark.value} className="min-w-0" /> : <TextPreview text={row.value} className="min-w-0" />
|
||||||
|
if (!row.note)
|
||||||
|
return value
|
||||||
|
return (
|
||||||
|
<div className="flex min-w-0 flex-wrap items-baseline gap-x-2">
|
||||||
|
{value}
|
||||||
|
{mark?.note ? <DiffText diff={mark.note} className="min-w-0 flex-[1_1_12em]" /> : <TextPreview text={row.note} className="min-w-0 flex-[1_1_12em]" />}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
/** 属性表里删掉的一行:整行淡红底、划掉,左边 −;它下面的子行照样摊开、划掉。 */
|
/** 属性表里删掉的一行:整行淡红底、划掉,左边 −;它下面的子行照样摊开、划掉。 */
|
||||||
function RemovedRow({ id, row }: { id: string, row: PropertyRow }): ReactElement {
|
function RemovedRow({ id, row }: { id: string, row: PropertyRow }): ReactElement {
|
||||||
const layout = subRowsLayout(row)
|
const layout = subRowsLayout(row)
|
||||||
@@ -291,7 +332,7 @@ function RemovedRow({ id, row }: { id: string, row: PropertyRow }): ReactElement
|
|||||||
<>
|
<>
|
||||||
<div className="cr-prop-row grid grid-cols-subgrid" data-diff="del">
|
<div className="cr-prop-row grid grid-cols-subgrid" data-diff="del">
|
||||||
<span className="cr-prop-label flex h-5 items-center">{row.label}</span>
|
<span className="cr-prop-label flex h-5 items-center">{row.label}</span>
|
||||||
<TextPreview text={row.value} className="min-w-0" />
|
<RowValue row={row} mark={undefined} />
|
||||||
</div>
|
</div>
|
||||||
{layout && (
|
{layout && (
|
||||||
<div className="cr-diff-gone">
|
<div className="cr-diff-gone">
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import { HighlightContext } from './useOccurrences'
|
|||||||
* 按词性给每一段上色,颜色在 codeReader.css 的 .cr-role-* 里按主题切换,带自己颜色(color)的段用它(.cr-colored);带源码下标的名字记在 data-offset 上
|
* 按词性给每一段上色,颜色在 codeReader.css 的 .cr-role-* 里按主题切换,带自己颜色(color)的段用它(.cr-colored);带源码下标的名字记在 data-offset 上
|
||||||
* (讲别处名字的段记那个名字的下标,见 sourceOf),跳到定义、高亮各处出现用;是眼下高亮着的名字的一处出现时 data-occurrence 标出底色,是当前那一处(点中的、用键盘跳到的)时
|
* (讲别处名字的段记那个名字的下标,见 sourceOf),跳到定义、高亮各处出现用;是眼下高亮着的名字的一处出现时 data-occurrence 标出底色,是当前那一处(点中的、用键盘跳到的)时
|
||||||
* 再标 data-current。起了中文名的名字显示中文名(data-name),悬停看原名,定义处本身在后面淡淡写原名;下标照旧,跳到定义、高亮照常。
|
* 再标 data-current。起了中文名的名字显示中文名(data-name),悬停看原名,定义处本身在后面淡淡写原名;下标照旧,跳到定义、高亮照常。
|
||||||
|
* 带悬停提示(tooltip)的段悬停看它。
|
||||||
* 框起来的一组词画成小框,框里的词同样处理。
|
* 框起来的一组词画成小框,框里的词同样处理。
|
||||||
*/
|
*/
|
||||||
export function SegmentText({ segments }: { segments: Inline[] }): ReactElement {
|
export function SegmentText({ segments }: { segments: Inline[] }): ReactElement {
|
||||||
@@ -31,7 +32,7 @@ export function SegmentText({ segments }: { segments: Inline[] }): ReactElement
|
|||||||
data-occurrence={highlight && segmentOccurrence(piece, highlight.occurrences)}
|
data-occurrence={highlight && segmentOccurrence(piece, highlight.occurrences)}
|
||||||
data-current={highlight && isCurrent(piece, highlight) ? '' : undefined}
|
data-current={highlight && isCurrent(piece, highlight) ? '' : undefined}
|
||||||
data-name={shown?.name}
|
data-name={shown?.name}
|
||||||
data-tooltip={shown && `原名 ${piece.text}`}
|
data-tooltip={shown ? `原名 ${piece.text}` : piece.tooltip}
|
||||||
>
|
>
|
||||||
{shown ? shown.name : piece.text}
|
{shown ? shown.name : piece.text}
|
||||||
{shown?.definition && <span className="cr-original">{piece.text}</span>}
|
{shown?.definition && <span className="cr-original">{piece.text}</span>}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import type { Inline, PropertyItem, PropertyRow } from './types'
|
import type { PreviewText, PropertyItem, PropertyRow } from './types'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 一行下面展开的子行怎么排:翻译插件给了列名的(输入参数)排成带表头和边框的表格,没给的(导入项、字典 / 对象的各项)排成列表;
|
* 一行下面展开的子行怎么排:翻译插件给了列名的(输入参数)排成带表头和边框的表格,没给的(导入项、字典 / 对象的各项)排成列表;
|
||||||
@@ -21,6 +21,6 @@ export function tableTemplate(count: number): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** 表格里一个子行的格子:按列名的列数取,子行少给的格子留空、多给的不显示。 */
|
/** 表格里一个子行的格子:按列名的列数取,子行少给的格子留空、多给的不显示。 */
|
||||||
export function tableCells(item: PropertyItem, columns: string[]): Inline[][] {
|
export function tableCells(item: PropertyItem, columns: string[]): PreviewText[] {
|
||||||
return columns.map((_, i) => item.cells[i] ?? [])
|
return columns.map((_, i) => item.cells[i] ?? [])
|
||||||
}
|
}
|
||||||
@@ -23,8 +23,9 @@ export type TokenRole
|
|||||||
* reference:库规则自己查定义的名字(Celery 的任务名)才有;webview 不看它,照样只报 offset,扩展端按 offset 认出来交给规则查。
|
* reference:库规则自己查定义的名字(Celery 的任务名)才有;webview 不看它,照样只报 offset,扩展端按 offset 认出来交给规则查。
|
||||||
* color:这段自己的颜色(库规则按库自己的配色给的,例如 HTTP 方法),盖过词性的颜色,按主题挑暗色、亮色。
|
* color:这段自己的颜色(库规则按库自己的配色给的,例如 HTTP 方法),盖过词性的颜色,按主题挑暗色、亮色。
|
||||||
* refers:这段讲的是源码里别处的一个名字(写在字符串里的名字讲的参数):跳到定义、高亮各处出现都当它是那个名字,见 sourceOf
|
* refers:这段讲的是源码里别处的一个名字(写在字符串里的名字讲的参数):跳到定义、高亮各处出现都当它是那个名字,见 sourceOf
|
||||||
|
* tooltip:悬停这段时看的字(说明里写了、参数表里没有的参数名:「没有这个参数」)
|
||||||
*/
|
*/
|
||||||
export interface Segment { text: string, role: TokenRole, offset?: number, reference?: RuleReference, color?: ThemeColor, refers?: SourceRange }
|
export interface Segment { text: string, role: TokenRole, offset?: number, reference?: RuleReference, color?: ThemeColor, refers?: SourceRange, tooltip?: string }
|
||||||
|
|
||||||
/** 规则自己查得到定义的一个名字:哪个规则、规则自己认的键 */
|
/** 规则自己查得到定义的一个名字:哪个规则、规则自己认的键 */
|
||||||
export interface RuleReference { rule: string, key: string }
|
export interface RuleReference { rule: string, key: string }
|
||||||
@@ -51,7 +52,10 @@ export interface SegmentGroup {
|
|||||||
/** 译文里的一段:着色的词,或框起来的一组词 */
|
/** 译文里的一段:着色的词,或框起来的一组词 */
|
||||||
export type Inline = Segment | SegmentGroup
|
export type Inline = Segment | SegmentGroup
|
||||||
|
|
||||||
/** 一段 Markdown 说明文字:注释、JSDoc、docstring 去掉注释符号、引号和共同缩进后的原文,按 Markdown 渲染成注释色的段落、列表、代码块 */
|
/**
|
||||||
|
* 一段 Markdown 说明文字:注释、JSDoc、docstring 去掉注释符号、引号和共同缩进后的原文,按 Markdown 渲染成注释色的段落、列表、代码块;
|
||||||
|
* 不带路径的引用(`[名字]`、`[类.成员]`)问扩展端它指的定义,问到了能点
|
||||||
|
*/
|
||||||
export interface Markdown { markdown: string }
|
export interface Markdown { markdown: string }
|
||||||
|
|
||||||
/** 预览里显示的一段文字:着色的词(一行译文、属性值),或整段 Markdown 说明(注释块、函数和类的说明) */
|
/** 预览里显示的一段文字:着色的词(一行译文、属性值),或整段 Markdown 说明(注释块、函数和类的说明) */
|
||||||
@@ -66,9 +70,9 @@ export interface SourceRange { startIndex: number, endIndex: number }
|
|||||||
*/
|
*/
|
||||||
export interface Defines { defines?: SourceRange }
|
export interface Defines { defines?: SourceRange }
|
||||||
|
|
||||||
/** 属性表里展开的一个子行:几列对齐的格子,例如参数的名称 / 类型 / 默认值、字典一项的键 / 值 */
|
/** 属性表里展开的一个子行:几列对齐的格子,例如参数的名称 / 类型 / 默认值、字典一项的键 / 值;格子是着色的词或 Markdown(参数的说明、异常的触发条件) */
|
||||||
export interface PropertyItem extends Defines {
|
export interface PropertyItem extends Defines {
|
||||||
cells: Inline[][]
|
cells: PreviewText[]
|
||||||
/** 展开在这个子行下面的下一层子行(字典 / 对象的值又是字典 / 对象时,它的各项),一层层可以一直往下套 */
|
/** 展开在这个子行下面的下一层子行(字典 / 对象的值又是字典 / 对象时,它的各项),一层层可以一直往下套 */
|
||||||
items?: PropertyItem[]
|
items?: PropertyItem[]
|
||||||
/** 排在这一行最后一格里、那格文字下面的表(实参的值里跨几行写的调用的实参表),表里的行又可以带 table;总是展开 */
|
/** 排在这一行最后一格里、那格文字下面的表(实参的值里跨几行写的调用的实参表),表里的行又可以带 table;总是展开 */
|
||||||
@@ -82,6 +86,8 @@ export interface PropertyItem extends Defines {
|
|||||||
export interface PropertyRow extends Defines {
|
export interface PropertyRow extends Defines {
|
||||||
label: string
|
label: string
|
||||||
value: PreviewText
|
value: PreviewText
|
||||||
|
/** 跟在值后面的一段说明(Markdown,注释色):函数输出参数那一行,返回类型后面是说明里的 `@return` */
|
||||||
|
note?: Markdown
|
||||||
items?: PropertyItem[]
|
items?: PropertyItem[]
|
||||||
columns?: string[]
|
columns?: string[]
|
||||||
}
|
}
|
||||||
@@ -324,7 +330,7 @@ export interface ItemsDiff {
|
|||||||
/** 一个子行的改动:整行新加的(它下面各层也都是),或按列标的格子、下一层、套着的表 */
|
/** 一个子行的改动:整行新加的(它下面各层也都是),或按列标的格子、下一层、套着的表 */
|
||||||
export interface ItemDiff {
|
export interface ItemDiff {
|
||||||
added?: true
|
added?: true
|
||||||
cells?: Record<number, InlinePart[]>
|
cells?: Record<number, TextDiff>
|
||||||
items?: ItemsDiff
|
items?: ItemsDiff
|
||||||
table?: ItemsDiff
|
table?: ItemsDiff
|
||||||
}
|
}
|
||||||
@@ -335,10 +341,11 @@ export interface RowsDiff {
|
|||||||
removed: Removed<PropertyRow>[]
|
removed: Removed<PropertyRow>[]
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 属性表一行的改动:整行新加的(它下面的子行也都是),或值、子行改了 */
|
/** 属性表一行的改动:整行新加的(它下面的子行也都是),或值、跟在值后面的说明、子行改了 */
|
||||||
export interface RowDiff {
|
export interface RowDiff {
|
||||||
added?: true
|
added?: true
|
||||||
value?: TextDiff
|
value?: TextDiff
|
||||||
|
note?: TextDiff
|
||||||
items?: ItemsDiff
|
items?: ItemsDiff
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user