✨ feat(vscode): 代码阅读预览里跨几行写的列表按项拆开,列表里整项是调用的带着自己的实参表,一层层能往下套
This commit is contained in:
1 parent
7d6e7592c9
commit
2c490cac2d
22 files changed
+848
-314
No files matched your search
@@ -37,7 +37,7 @@
|
||||
| 问 AI | 块右上角「问 AI」,在块下面问 Claude Code(`claude -p`,只读,用输入框旁边选的 profile,记住上次选的,没选过用插件默认的)这块代码,回答逐字出来按 Markdown 显示、能追问(接着这块最近一次的会话,收起再展开、重启 VS Code 也接得上)、能停;输入框里能粘贴 / 拖进图片随问题发(存在 `.spx/code-qa/`,跟着问答删);问答存进各工作区文件夹的 `.spx/code-qa.json`,灰字挂在块下面免得重复问,理解了就删。问答按块的位置记号挂,记号只由和语言无关的块数据(种类、facts 里的名字、源码哈希)算出,文件别处改了照样认得;认不回来的不显示,留在文件里,代码改回来又挂回去。见「问 AI」 |
|
||||
| 译文用词 | 结构照抄原语言,只翻关键字、内置类型、运算符;用专业术语(`export` → 公共,`catch` → 捕获,`===` → 全等) |
|
||||
| 函数、类、导入、常量、变量块 | 预览不显示一行译文,显示树形属性表;属性值里的类型、表达式照常直译 |
|
||||
| 字典 / 对象字面量 | 常量、变量的值是 Python 字典 / TypeScript 对象字面量时不挤成一长行:值写「字典 N 项」/「对象 N 项」,各项展开成「键 \| 值」两列的子行,值又是字典 / 对象时一层层往下展开;参数默认值照旧一行 |
|
||||
| 字典 / 对象、列表字面量 | 常量、变量的值是 Python 字典 / TypeScript 对象字面量时不挤成一长行:值写「字典 N 项」/「对象 N 项」,各项展开成「键 \| 值」两列的子行;跨几行写的列表(元组、集合、数组)同样写「列表 N 项」,各项展开成「第 N 项 \| 值」,整项是调用的排成「新建 / 调用 …」带自己的实参表;项的值又是字典、跨几行的列表时一层层往下展开,实参的值是跨几行的列表时也拆;参数默认值照旧一行 |
|
||||
| 输入参数 | 排成带表头和边框的表格:名称 \| 类型 \| 默认值,表头写了「默认值」,格子里不再写「默认 」 |
|
||||
| 库规则 | 第四种插件:按库认出一种写法,把那块整个换成专门的卡片(内置 SQLAlchemy 规则:模型类 → 「数据表」卡片;内置 Celery 规则:`Celery(...)` → 「Celery 应用」卡片、`beat_schedule` → 「定时任务」卡片;内置 dataclasses 规则:数据类 → 「数据类」卡片;内置 FastAPI 规则:挂在路由对象上的函数 → 「接口」卡片,请求、响应各一张表),或把那块认成契约里现成的一种属性表块(内置 enum 规则:Python 自带 enum 库的类 → 枚举块)。和语言插件、翻译插件一样写代码注册、只经契约拿信息;文件导入了规则关心的库才启用,由宿主按语言插件给的导入判断;卡片的用词归规则自己,认成的现成块照常由翻译插件排,webview 只画结构。见「库规则」 |
|
||||
| 枚举 | 枚举是一种块(种类「枚举」,紫色 #b180d7 / 亮色 #652d90),属性表:名称 / 说明 / 访问级别 / 装饰器 / 继承(有才显示)/ 成员 N 个,成员排成表格「成员 \| 值 \| 说明」。TypeScript 的 `enum` 是语言自己的写法,由语言插件报;Python 没有枚举语法,自带 enum 库的写法由库规则认成同一种块。两边同一种块、同一张表、同一个筛选种类,webview 只有一种画法 |
|
||||
@@ -252,7 +252,7 @@
|
||||
|
||||
### 属性表(函数、类、枚举、异常、导入、常量、变量块)
|
||||
|
||||
卡片外观(折叠箭头、种类图标、底色、悬停高亮、「编辑 / 预览」切换)和其他块一样,只是内容换成一张属性表:左列属性名(界面字体、`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` 色。没有默认值的格子留空。格子可以是 Markdown(参数的「说明」、「异常」表的两列、对不上的参数的说明):按「注释的 Markdown 排版」显示(界面字体、注释色),着色的词照旧排在等宽字体的段落里,差异逐词标。属性行跟着的说明(`PropertyRow.note`,输出参数的 `@return`)接在值后面,同一行放得下就并排,剩下的地方不到 12 个字宽时换到下一行。折叠「输入参数」那一行时整张表收起;子行 id、跳到定义闪「它那个参数」、高亮名字的各处出现都和列表一样,闪的是表格里那一行(行边框和相邻行共用,改在行里面描一圈聚焦色)。哪些子行排成表格由翻译插件定:给了列名(`PropertyRow.columns`)的排成表格,没给的(导入项、字典 / 对象的各项)照旧是列表,webview 只看有没有列名。
|
||||
|
||||
@@ -273,15 +273,35 @@
|
||||
|
||||
值里的表达式只翻落在块头部里的那一段:`const v = useMemo(() => { … })` 这种值带语句块主体回调的块,回调主体是子块,值只翻到 `useMemo(() =>`,和块头部一致。
|
||||
|
||||
字典 / 对象字面量(常量、变量的值,含 Python 类主体里不带注解的类属性;参数默认值照旧是一行译文):
|
||||
字典 / 对象字面量、跨几行写的列表(常量、变量的值,含 Python 类主体里不带注解的类属性;参数默认值照旧是一行译文):
|
||||
|
||||
- 值写项数:Python `字典 N 项`、TypeScript `对象 N 项`(按 hint 着色),各项展开成子行,照源码顺序,注释不算一项。
|
||||
- 每项两列:键 | 值。键照源码写法:字符串键带引号、按字符串着色,名字键按名字着色,计算属性名 `[k]` 照常翻;值照常直译(`等待 …`、`空数组`……)。
|
||||
- 每项两列:键 | 值。键照源码写法:字符串键带引号、按字符串着色,名字键按名字着色,计算属性名 `[k]` 照常翻;值同常量、变量的值:字典 / 对象、跨几行的列表接着拆,跨几行写的调用排成「新建 / 调用 …」、实参表排在这一项下面,别的照常直译(`等待 …`、`空数组`……)。
|
||||
- 值又是字典 / 对象字面量时,它这一项的值也写项数,它的各项展开成下一层子行,一层层往下套。
|
||||
- 展开:Python `**other`、TypeScript `...other` 是一项,键写「展开」(hint),值是展开的表达式。
|
||||
- TypeScript 简写 `{ a }`:键 `a`,值 `a`。对象里的方法、getter、setter:键是方法名(函数色),值是方法头部(到主体开始之前)的译文,如 `异步 load(id: 字符串): Promise<无>`、`get size()`。
|
||||
- 空字面量不展开:值是一行 `空字典` / `空对象`(一行译文里也这样翻,同 `空数组`)。
|
||||
- `as const`、`satisfies` 包着的对象(`{ … } as const`)不算字面量本身,照旧一行译文。
|
||||
- 列表(Python 的列表 / 元组 / 集合、TypeScript 的数组、Rust 的数组 / 元组)跨几行写时同样拆:值写 `列表 N 项`(`元组`、`集合`、`数组` 各用自己的叫法,展开的也算一项),每项两列:`第 N 项`(hint,只数不是展开的项)| 值;`*other`、`...other` 左边写「展开」,值是展开的表达式。列表的项不讲哪个定义(不标 defines)。写在一行里的列表照旧一行译文(`[a, f(b)]`),空的照旧 `空列表`。`[值; 个数]` 不是一项项写出来的,不拆。
|
||||
- 列表的一项整个是一次调用时,不管写成一行还是几行都排成「新建 / 调用 被调用的名字 · N 个参数」(见「调用与构造」),它的实参表排在这一项下面、往右缩一层,同一个列表里样子一致;别处写在一行里的调用照旧。项的值是字典 / 对象照字典拆,是跨几行的列表接着拆,一层层往下套。
|
||||
- 实参的值是跨几行写的列表时也这样拆,各项排在实参表值那一格里、文字下面(见「调用与构造」);实参里的字典 / 对象照旧一行。
|
||||
- 用户那段代码读出来是:
|
||||
|
||||
```
|
||||
初始值 列表 2 项
|
||||
第 1 项 新建 Filter · 3 个参数
|
||||
参数 值
|
||||
field "user_id"
|
||||
operator FilterOperator.EQ
|
||||
value current_user.id
|
||||
第 2 项 新建 Filter · 3 个参数
|
||||
参数 值
|
||||
field "archived_at"
|
||||
operator 如果 archived 则 FilterOperator.NE 否则 FilterOperator.EQ
|
||||
value 空
|
||||
```
|
||||
|
||||
- 语言插件给各项(`LanguageAdapter.collection`:节点 → `CollectionFacts`),翻译插件按字面量的节点类型查叫法(`ZhTable.collections`)、定怎么拆;webview 只画子行和表,不认语言。
|
||||
|
||||
### 异常
|
||||
|
||||
@@ -349,16 +369,16 @@
|
||||
|
||||
原型画板「调用 · 构造」(`code.js` 的「调用与构造」一节,嵌套见 `nestedArgRow`、`NESTED_CALL_SAMPLE`)。实参多的调用一般跨几行写,挤成一行译文读不下去;把实参摊成一张表,实参的值又是这样的调用时在格子里接着摊。
|
||||
|
||||
- 哪些调用:实参列表(括号连同里面,`CallFacts.argumentList`)跨了几行的调用,而且整个落在要翻的这一段里(块头部;TypeScript `describe('x', () => { … })` 这种主体是子块的调用不算)。一行写完的调用照旧一行译文,不问定义。链式调用跨了几行、每段的括号都写在一行里的(`items\n .filter(…)\n .map(…)`)也不算;式子跨了几行、里面的调用各自写在一行里的(`bool(x)\n and Path(x).is_file()`)是式子,也不算。
|
||||
- 哪些调用:实参列表(括号连同里面,`CallFacts.argumentList`)跨了几行的调用,而且整个落在要翻的这一段里(块头部;TypeScript `describe('x', () => { … })` 这种主体是子块的调用不算)。一行写完的调用照旧一行译文,不问定义。链式调用跨了几行、每段的括号都写在一行里的(`items\n .filter(…)\n .map(…)`)也不算;式子跨了几行、里面的调用各自写在一行里的(`bool(x)\n and Path(x).is_file()`)是式子,也不算。拆开的列表里整项是调用的例外,写成一行也排(见「字典 / 对象字面量、跨几行写的列表」)。
|
||||
- 一段排一个:一行译文的块、常量 / 变量的值、实参表里每个值各是一段,各自排自己里面遇到的第一个(先外后里)这样的调用;同一段里后面再有的照常一行译文(挤成一行时圆括号贴着里面的内容:`f(\n x\n)` 写成 `f(x)`,方括号、花括号照旧空一格)。被调用的表达式里的调用(`get_driver(\n m\n).query(\n …\n)` 排的是外面的 query)照常一行。
|
||||
- 一行:`[等待 ]新建 / 调用 被调用的名字 · N 个参数`。「等待」就是 `await` 照常的译法(在调用前面),「新建」「调用」按声明关键字着色;被调用的名字照常翻、带源码下标(⌘ / Ctrl + 点击、单击高亮、中文名都照旧),前面几节(对象、模块)按变量着色,最后那个名字新建时按类型着色、调用时按函数着色;「· N 个参数」按说明词着色,N 数的是表里的行。
|
||||
- 埋在式子里:调用后面还紧跟着同一个式子的别的部分(`.model_dump()`、`[0]`、`or x`;后面是右括号、逗号、分号、冒号或什么都没有的不算),这一行整个括起来:「(新建 X · 3 个参数).model_dump()」「(调用 make · 1 个参数) 或者 fallback」,表照样排在这一段下面。前面有东西的(`fallback or make(\n…\n)`)不括。
|
||||
- 新建还是调用:写法说明了的看写法(`CallFacts.construct`:TypeScript 的 `new X(…)` 是新建,不带 new 的是调用,都不问定义);Python 新建对象和调用函数写法一样,翻译插件问 `SourceDocument.calleeKind`:名字定义成类(含异常、枚举)是「新建」,函数、别的、查不到、还没查到都是「调用」。两种语言都用「新建」这个词。
|
||||
- 查定义(宿主,`calleeKinds.ts` + `editor/definitions.ts` 的 `definitionKind`):排视图数据是同步的,先按已经查到的排(第一次都按调用画),记下问了哪些调用;发出预览后问语言扩展被调用的名字(`CallFacts.name`)定义在哪(和 ⌘ + 点击同一套),打开定义所在的文件、用它那种语言的语言插件分块(`readDefinitionKind`:自己的名字盖住定义处的最里层那块),几处定义取第一处认得出的。查到的和已知的不一样就重排、重发。按被调用的表达式原文记(`user_service.update_user`),同一份文档里写法一样的按一处查;文档改了先用上一版查到的,每个版本查一次,语言扩展还没起来(没答)的照 RETRY_DELAYS 隔一会儿再查(和库规则查名字的定义同一套 `DeferredAnswers`)。被调用的不是名字(`handlers[k](…)`)的不查,按调用算。
|
||||
- 表:两列「参数 | 值」,每个实参一行、照源码顺序。按名字传的(Python `name=value`)写名字,按变量着色、带源码下标;按位置传的写「第 N 个」(说明词,只数按位置传的);`*args`、`...args` 写「展开」,`**kwargs` 写「展开关键字」(说明词)。值照常一行译文(名字带下标);唯一的实参是生成器时(`sum(\n x for x in xs\n)`),共用的那对括号属于调用,格子里只有框。表的样子同输入参数表(`PropertyTable`:表头、圆角外框、格子之间分隔线,第一列按内容定宽)。
|
||||
- 嵌套:值(一段)里有跨几行写的调用时,值那一格写它的一行,它的实参表排在同一格里、文字下面(`PropertyItem.table`),一层层往下套,多深都一样;套着的表不能折叠,总是展开。每一层都和最外层一样:先按「调用」画、查到定义再刷新成「新建」,名字带源码下标,跳到定义、高亮各处出现、中文名照旧。排版:每套一层往右缩 5px(`PropertyTable` 外面那层的左边距),第一列按内容定宽、窄了能收,长的值在格子里换行,不出横向滚动条。
|
||||
- 嵌套:值(一段)里有跨几行写的调用时,值那一格写它的一行,它的实参表排在同一格里、文字下面(`PropertyItem.table`),一层层往下套,多深都一样;套着的表不能折叠,总是展开。值是跨几行写的列表时值那一格写「列表 N 项」,各项排在同一格里、文字下面(`PropertyItem.items`),项里整项是调用的接着带自己的实参表;实参里的字典 / 对象照旧一行。每一层都和最外层一样:先按「调用」画、查到定义再刷新成「新建」,名字带源码下标,跳到定义、高亮各处出现、中文名照旧。排版:每套一层往右缩 5px(`PropertyTable` 外面那层的左边距),第一列按内容定宽、窄了能收,长的值在格子里换行,不出横向滚动条。
|
||||
- 放在哪:
|
||||
- 变量、常量块:初始值 / 值那一行写上面那一行字,表是这一行展开的子行(`PropertyRow.columns = ['参数', '值']`),带折叠箭头、默认展开。值是字典 / 对象字面量时照旧按「字典 N 项」展开,不看里面的调用。
|
||||
- 变量、常量块:初始值 / 值那一行写上面那一行字,表是这一行展开的子行(`PropertyRow.columns = ['参数', '值']`),带折叠箭头、默认展开。值是字典 / 对象字面量、跨几行写的列表时按项展开(见「字典 / 对象字面量、跨几行写的列表」),里面的调用跟着各自那一项。
|
||||
- 一行译文的块(返回、语句、字段……):译文里调用的位置换成那一行字(「返回 新建 UserDetailResponse · 11 个参数」),表排在译文下面(`TextTranslation.table` → 视图数据的 `table`),总是展开;块带说明时说明排在表下面。
|
||||
- 分工:语言插件报结构(`LanguageAdapter.call`:被调用的表达式、最后那个名字、写法是不是新建、实参列表、各个实参),翻译插件定用词、挑哪个调用、怎么排;webview 只画表(`PreviewTable`),不认语言;跳到定义、高亮各处出现、中文名在表里照旧(`renderedNames`、`reveal.ts` 都算上这张表和一层层套着的表)。
|
||||
- 样例(`fixtures/routes.py`、`paymentConsoleRoutes.py`、`chainedCalls.py` 是 luffy 的代码,`calls.ts.txt`、`nestedCalls.ts.txt` 是本仓库的代码):
|
||||
@@ -509,7 +529,7 @@
|
||||
- 打开别的文件只有一条路(预览的 `openElsewhere`,用哪种编辑器由 `editor/openTarget.ts` 的 `openingFor` 按目标文件的语言定,侧栏点文件也按它):跳到定义、注释里的仓库链接、不带路径的引用都走它。
|
||||
- 闪哪一处:页面上**就是这处定义**的最具体的那一处,只按源码位置判断(定义提供者给的定义处下标,对上块和属性行带的源码范围),不看属性名文字:
|
||||
1. 定义是块自己的名字(函数名、类名;只声明一个变量时的变量名,解构模式整个算)→ 整块。块的这段范围由扩展端从 facts 取(`BlockView.defines`),和翻译插件无关。
|
||||
2. 定义是属性表里某一行 / 子行讲的东西(参数、导入项、一条语句声明几个变量时的某一个、类的泛型参数、对象字面量一项的键——TypeScript 对象的属性名就是定义,嵌套几层都算)→ 只闪这一行 / 子行。范围是翻译插件从 facts 取的节点(`PropertyRow.defines`、`PropertyItem.defines`;字典 / 对象的项标的是键,展开项不标),定义处落在范围里就算(`*args` 的定义在 `args` 上、解构参数里的名字都对得上)。
|
||||
2. 定义是属性表里某一行 / 子行讲的东西(参数、导入项、一条语句声明几个变量时的某一个、类的泛型参数、对象字面量一项的键——TypeScript 对象的属性名就是定义,嵌套几层都算)→ 只闪这一行 / 子行。范围是翻译插件从 facts 取的节点(`PropertyRow.defines`、`PropertyItem.defines`;字典 / 对象的项标的是键,展开项、列表的项不标),定义处落在范围里就算(`*args` 的定义在 `args` 上、解构参数里的名字都对得上)。
|
||||
3. 其余(定义在一行译文里,例如循环变量;翻译插件没填 `defines`)→ 显示着定义处那个名字的块,名字在属性行的子行里时展开那一行(在嵌套的子行里时连它上面各层一起展开);页面上找不到这个名字就取包含定义那一行的块。
|
||||
- 同一文件原地跳、跳进另一个文件的预览,规则一样。
|
||||
- 滚过去:目标所在块的祖先块折叠着就展开,目标在折叠的属性行子行里(参数、导入项、字典 / 对象的项)就展开那一行,在嵌套的子行里就连它上面折叠着的各层子行一起展开。目标已在视口里(筛选栏和钉住的卡片下面)不滚,否则滚到视口正中(比视口高就顶部对齐)。然后目标的边框和底色往聚焦色(`--vscode-focusBorder`)闪三下,共约 1.5 秒后复原;同一处再跳一次从头闪;系统开了「减少动态效果」时改成静止高亮 1.5 秒。目标是属性行 / 子行但块正在编辑(页面上没有属性表)时闪整块。
|
||||
@@ -795,10 +815,20 @@ export interface ImportFacts {
|
||||
|
||||
/** 字典 / 对象字面量里的一项,照源码顺序;注释不算。 */
|
||||
export type EntryFacts =
|
||||
| { kind: 'pair', key: Node, value: Node, entries?: EntryFacts[] } // 'a': 1、a: 1、[k]: v;TS 简写 { a } 键和值是同一个节点;entries 同 DeclaratorFacts.entries
|
||||
| { kind: 'pair', key: Node, value: Node } // 'a': 1、a: 1、[k]: v;TS 简写 { a } 键和值是同一个节点
|
||||
| { kind: 'spread', value: Node } // **other、...other:value 是展开的表达式
|
||||
| { kind: 'method', name: Node, node: Node, header: SourceSpan } // 对象里的方法、getter、setter:header 同 BlockShape.header,只到主体开始之前
|
||||
|
||||
/** 列表 / 元组 / 集合 / 数组字面量里的一项,照源码顺序;注释不算。 */
|
||||
export type ElementFacts =
|
||||
| { kind: 'item', value: Node } // 写出来的一项
|
||||
| { kind: 'spread', value: Node } // *other、...other:value 是展开的表达式
|
||||
|
||||
/** 写出各项的字面量(LanguageAdapter.collection):map 是字典 / 对象,sequence 是列表 / 元组 / 集合 / 数组;空字面量的各项是空数组,项的值里还能再套。 */
|
||||
export type CollectionFacts =
|
||||
| { kind: 'map', entries: EntryFacts[] }
|
||||
| { kind: 'sequence', elements: ElementFacts[] }
|
||||
|
||||
/** 写在表达式里的遍历(Python 推导式、生成器表达式)得到什么。 */
|
||||
export type IterationResult = 'generator' | 'list' | 'set' | 'dict'
|
||||
|
||||
@@ -823,7 +853,7 @@ export type CallArgument =
|
||||
| { kind: 'spread', value: Node } // *args、...args
|
||||
| { kind: 'keywordSpread', value: Node } // Python 的 **kwargs
|
||||
|
||||
/** 一次调用(含新建对象)的结构:语言插件读出,翻译插件把跨几行写的排成「新建 / 调用 名字 · N 个参数」一行、实参摊成表(见「调用与构造」)。 */
|
||||
/** 一次调用(含新建对象)的结构:语言插件读出,翻译插件把跨几行写的(拆开的列表里整项是调用的,写成一行也算)排成「新建 / 调用 名字 · N 个参数」一行、实参摊成表(见「调用与构造」)。 */
|
||||
export interface CallFacts {
|
||||
callee: Node // 被调用的表达式(TypeScript 的 new 不算在里面)
|
||||
name?: Node // callee 里最后那个名字(点分名字的最后一节):着色、查定义用它;callee 不是名字时没有
|
||||
@@ -836,7 +866,6 @@ export interface DeclaratorFacts {
|
||||
name: Node
|
||||
type?: Node
|
||||
value?: Node
|
||||
entries?: EntryFacts[] // 值是字典 / 对象字面量(Python dictionary、TS object)时是它的各项,空字面量是空数组;别的值没有
|
||||
}
|
||||
|
||||
export interface VariableFacts {
|
||||
@@ -967,17 +996,18 @@ export interface Defines { defines?: SourceRange }
|
||||
|
||||
/**
|
||||
* 属性表里展开的一个子行:几列对齐的格子,格子是着色的词或一段 Markdown(参数的说明、异常的触发条件);
|
||||
* items 是展开在它下面的下一层子行(嵌套的字典 / 对象),一层层可以一直往下套。
|
||||
* items 是最后一格的值拆开的下一层子行(字典 / 对象、跨几行写的列表的各项):排成列表的子行展开在它下面、能折叠,
|
||||
* 表格的行排在最后一格里、文字下面、总是展开。一层层可以一直往下套。
|
||||
*/
|
||||
export interface PropertyItem extends Defines {
|
||||
cells: PreviewText[]
|
||||
items?: PropertyItem[]
|
||||
table?: PreviewTable // 排在最后一格里、文字下面的表:实参的值里跨几行写的调用的实参表,一层层往下套,总是展开
|
||||
table?: PreviewTable // 最后一格的值里那次调用的实参表:表格的行排在最后一格里、文字下面,排成列表的子行排在它下面;总是展开,表里的行又能带 items、table
|
||||
}
|
||||
|
||||
/**
|
||||
* 属性表的一行:属性名、着色的值或 Markdown 说明(函数、类的「说明」)、展开在下面的子行(输入参数、导入项、字典 / 对象的各项)。
|
||||
* columns 是子行各列的列名(输入参数的名称 / 类型 / 默认值):给了的子行排成带表头的表格(只有一层),没给的排成列表。
|
||||
* 属性表的一行:属性名、着色的值或 Markdown 说明(函数、类的「说明」)、展开在下面的子行(输入参数、导入项、字典 / 对象、列表的各项)。
|
||||
* columns 是子行各列的列名(输入参数的名称 / 类型 / 默认值):给了的子行排成带表头的表格,没给的排成列表;两种子行都能再带下一层。
|
||||
* note 是跟在值后面的一段说明(Markdown,注释色):输出参数那一行,返回类型后面是说明里的 `@return`。
|
||||
*/
|
||||
export interface PropertyRow extends Defines { label: string, value: PreviewText, note?: Markdown, items?: PropertyItem[], columns?: string[] }
|
||||
@@ -1007,6 +1037,8 @@ export interface LanguageAdapter {
|
||||
iteration?: (node: Node) => IterationFacts | undefined
|
||||
/** node 是一次调用(含新建对象)时给出它的结构,别的节点没有;没给的语言不展开调用,照常一行译文。 */
|
||||
call?: (node: Node) => CallFacts | undefined
|
||||
/** node 是写出各项的字面量(字典、列表……)时给出它的各项,别的节点没有;没给的语言不拆字面量,照常一行译文。 */
|
||||
collection?: (node: Node) => CollectionFacts | undefined
|
||||
/** node 是执行一条命令(shell)时给出它的结构,交给命令规则读;没有命令的语言不填。 */
|
||||
command?: (node: Node) => CommandFacts | undefined
|
||||
/** 一份源码里自己认得出的名字(LocalSymbol);VS Code 有这种语言的定义、高亮提供者时用不上,不填。 */
|
||||
@@ -1211,9 +1243,9 @@ export interface CommandRule {
|
||||
- `activate()` 返回 `{ codeReader: CodeReaderApi }`;其他扩展声明 `extensionDependencies` 后用 `extensions.getExtension('clurdra.superpowers-vscode-clurdra')!.exports.codeReader` 注册。
|
||||
- 内置的 TypeScript、Python、Shell、Dockerfile 语言插件、四个中文直译翻译插件、shell / 装系统 / 装软件包的命令规则和 SQLAlchemy、Celery、enum、dataclasses、FastAPI 库规则在 `activate()` 里通过同一个 API 注册(规则按这个顺序),没有旁路。
|
||||
- 注册表变化时更新 `superpowers.codeReader.languageIds` 上下文,并让已打开的预览刷新。
|
||||
- 分工:语言插件只从语法树读出和语言无关的事实(facts,引用源码节点,不做正则匹配文字);翻译插件决定属性名、枚举词(公共 / 是 / 默认导出)和值怎么译,也给筛选属性和值起名(`attributeNames`)。筛选属性的通用部分由宿主从 facts 推出(`attributes.ts` 的 `blockAttributes`),语言插件只在 `BlockShape.attributes` 里补自己语言特有的。第三方语言插件给函数、类、枚举、异常、导入、常量、变量块填 facts 就能用上任何翻译插件的属性表;自己语言的字典 / 映射字面量填进 `DeclaratorFacts.entries` 就能展开成子行(中文模板直译按字面量的节点类型查叫法,见 `ZhTable.mapLiterals`)。写在表达式里的遍历(推导式这类)由语言插件的 `iteration` 给结构(节点 → `IterationFacts`),翻译插件排成一个 `SegmentGroup`(中文直译是样式 `iteration`、框头是结果类型的译名);webview 只认框的样式 id,不认语言。调用由语言插件的 `call` 给结构(节点 → `CallFacts`),跨几行写的由翻译插件排成一行字加一张表(`PreviewTable`,见「调用与构造」);webview 只画表。
|
||||
- 分工:语言插件只从语法树读出和语言无关的事实(facts,引用源码节点,不做正则匹配文字);翻译插件决定属性名、枚举词(公共 / 是 / 默认导出)和值怎么译,也给筛选属性和值起名(`attributeNames`)。筛选属性的通用部分由宿主从 facts 推出(`attributes.ts` 的 `blockAttributes`),语言插件只在 `BlockShape.attributes` 里补自己语言特有的。第三方语言插件给函数、类、枚举、异常、导入、常量、变量块填 facts 就能用上任何翻译插件的属性表;自己语言的字典 / 映射、列表 / 数组这类字面量由 `collection` 给各项(节点 → `CollectionFacts`)就能展开成子行(中文模板直译按字面量的节点类型查叫法,见 `ZhTable.collections`)。写在表达式里的遍历(推导式这类)由语言插件的 `iteration` 给结构(节点 → `IterationFacts`),翻译插件排成一个 `SegmentGroup`(中文直译是样式 `iteration`、框头是结果类型的译名);webview 只认框的样式 id,不认语言。调用由语言插件的 `call` 给结构(节点 → `CallFacts`),跨几行写的由翻译插件排成一行字加一张表(`PreviewTable`,见「调用与构造」);webview 只画表。
|
||||
- 语言插件的信息只经契约到翻译插件:宿主翻每一块时把 `{ source, language }`(`SourceDocument`,language 是解析这份源码的语言插件)交给 `translate` / `describe`,翻译插件要词性、遍历结构都从 `language` 取,不 import 任何语言插件的模块(lint 规则挡住 `translators/` 下的非测试文件 import `languages/`)。所以第三方翻译插件(英文、AI 翻译)拿得到和内置翻译插件一样的信息,第三方语言插件也能配上任何翻译插件。
|
||||
- 第三方语言插件要给的:`id`、`languageIds`、`grammarPath`、`toBlocks`(块的种类、头部、子块;函数、类、枚举、异常、导入、常量、变量块带 facts)、`tokenRole`。可以给的:facts 里语言有才填的项(async、abstract、typeOnly、access、doc、`DeclaratorFacts.entries`)、`BlockShape.attributes`(通用词表以外的筛选属性)、`iteration`(有写在表达式里的遍历的语言)、`call`(调用的结构)、`displayNames`(这门语言里给定义起中文名的写法)。没给的就少那一样:没有 entries 的值不展开,没有 `iteration` 的遍历照源码结构逐词翻,没有 `call` 的调用不摊成表,没有 `displayNames` 就没有中文名。
|
||||
- 第三方语言插件要给的:`id`、`languageIds`、`grammarPath`、`toBlocks`(块的种类、头部、子块;函数、类、枚举、异常、导入、常量、变量块带 facts)、`tokenRole`。可以给的:facts 里语言有才填的项(async、abstract、typeOnly、access、doc)、`BlockShape.attributes`(通用词表以外的筛选属性)、`iteration`(有写在表达式里的遍历的语言)、`call`(调用的结构)、`collection`(字面量的各项)、`displayNames`(这门语言里给定义起中文名的写法)。没给的就少那一样:没有 `collection` 的字面量不展开,没有 `iteration` 的遍历照源码结构逐词翻,没有 `call` 的调用不摊成表,没有 `displayNames` 就没有中文名。
|
||||
- 嵌着的代码的分工:外层的语言插件只报是哪几段(`LanguageAdapter.embedded`、`TextBlock.embedded`、`CommandForm`),宿主按语言找那种语言的语言插件、翻译插件、命令规则解析和分步(`LanguageAdapter.steps`),外层的翻译插件只问读出来的样子(`SourceDocument.embedded`),连词、条数这些叫法归它;两种语言的插件互不 import。
|
||||
- 命令的分工:语言插件认出一条命令的结构(`LanguageAdapter.command`),命令规则按命令名读成译文(`SourceDocument.commandRule`,宿主按语言、注册顺序给),翻译插件读命令时问它、接上重定向;规则只从契约拿东西,不认识语言插件、翻译插件。
|
||||
- 中文名的分工:语言插件只认写法(`DisplayNameReader.find`:定义处的名字、中文名、注释里的记号);宿主解析一次源码时顺带问它,记号放进 `SourceDocument.markers` 交给翻译插件(翻注释、说明时去掉,中文模板直译在 `ZhTable.commentText` 之前去掉),定义交给预览找用到它的各处(`nameResolution.ts`);翻译插件不认识 `@Comment`,webview 不认识任何语言。
|
||||
|
||||
@@ -322,10 +322,23 @@ export interface ImportFacts {
|
||||
|
||||
/** 字典 / 对象字面量里的一项,照源码顺序;注释不算。 */
|
||||
export type EntryFacts
|
||||
= | { kind: 'pair', key: Node, value: Node, entries?: EntryFacts[] } // 'a': 1、a: 1、[k]: v;TypeScript 的简写 { a } 键和值是同一个节点;entries 同 DeclaratorFacts.entries
|
||||
= | { kind: 'pair', key: Node, value: Node } // 'a': 1、a: 1、[k]: v;TypeScript 的简写 { a } 键和值是同一个节点
|
||||
| { kind: 'spread', value: Node } // **other、...other:value 是展开的那个表达式
|
||||
| { kind: 'method', name: Node, node: Node, header: SourceSpan } // 对象字面量里的方法、getter、setter:node 是整个方法,header 同 BlockShape.header,只到主体开始之前
|
||||
|
||||
/** 列表 / 元组 / 集合 / 数组字面量里的一项,照源码顺序;注释不算。 */
|
||||
export type ElementFacts
|
||||
= | { kind: 'item', value: Node } // 写出来的一项
|
||||
| { kind: 'spread', value: Node } // *other、...other:value 是展开的那个表达式
|
||||
|
||||
/**
|
||||
* 写出各项的字面量:语言插件从语法树读出结构(LanguageAdapter.collection),翻译插件把各项拆成子行。
|
||||
* map 是字典 / 对象,sequence 是列表 / 元组 / 集合 / 数组;空字面量的各项是空数组。节点都指向源码,项的值里还能再套字面量。
|
||||
*/
|
||||
export type CollectionFacts
|
||||
= | { kind: 'map', entries: EntryFacts[] }
|
||||
| { kind: 'sequence', elements: ElementFacts[] }
|
||||
|
||||
/** 写在表达式里的遍历得到什么:逐个产出的生成器,或一次建好的列表、集合、字典。 */
|
||||
export type IterationResult = 'generator' | 'list' | 'set' | 'dict'
|
||||
|
||||
@@ -361,8 +374,8 @@ export type CallArgument
|
||||
| { kind: 'keywordSpread', value: Node } // 展开成按名字传的几个:Python 的 **kwargs
|
||||
|
||||
/**
|
||||
* 一次调用(含新建对象):语言插件从语法树读出结构(LanguageAdapter.call),翻译插件把跨几行写的调用排成
|
||||
* 「新建 / 调用 被调用的名字 · N 个参数」一行,实参摊成一张表。节点都指向源码。
|
||||
* 一次调用(含新建对象):语言插件从语法树读出结构(LanguageAdapter.call),翻译插件把跨几行写的调用(拆开的列表里整项是调用的,
|
||||
* 写成一行也算)排成「新建 / 调用 被调用的名字 · N 个参数」一行,实参摊成一张表。节点都指向源码。
|
||||
*/
|
||||
export interface CallFacts {
|
||||
/** 被调用的表达式:`user_service.update_user`、`UserDetailResponse`;TypeScript 的 new 不算在里面。 */
|
||||
@@ -450,8 +463,6 @@ export interface DeclaratorFacts {
|
||||
name: Node
|
||||
type?: Node
|
||||
value?: Node
|
||||
/** 值是字典 / 对象字面量(Python 的 dictionary、TypeScript 的 object)时是它的各项,空字面量是空数组;别的值没有。 */
|
||||
entries?: EntryFacts[]
|
||||
}
|
||||
|
||||
export interface VariableFacts {
|
||||
@@ -667,23 +678,27 @@ export type PreviewText = Inline[] | Markdown
|
||||
export interface Defines { defines?: SourceRange }
|
||||
|
||||
/**
|
||||
* 属性表里展开的一个子行:几列对齐的格子,例如参数的名称 / 类型 / 默认值、字典一项的键 / 值。
|
||||
* 属性表里展开的一个子行:几列对齐的格子,例如参数的名称 / 类型 / 默认值、字典一项的键 / 值、列表的第 N 项 / 值。
|
||||
* 格子是着色的词,或一段 Markdown(参数的说明、异常的触发条件)。
|
||||
*/
|
||||
export interface PropertyItem extends Defines {
|
||||
cells: PreviewText[]
|
||||
/** 展开在这个子行下面的下一层子行(字典 / 对象的值又是字典 / 对象时,它的各项);一层层可以一直往下套。 */
|
||||
/**
|
||||
* 最后一格的值拆开的下一层子行(值是字典 / 对象、跨几行写的列表时,它的各项):排成列表的子行把它展开在自己下面,能折叠;
|
||||
* 表格的行把它排在最后一格里、那格文字下面,总是展开。一层层可以一直往下套。
|
||||
*/
|
||||
items?: PropertyItem[]
|
||||
/**
|
||||
* 排在这一行最后一格里、那格文字下面的表:实参表里一个实参的值里有跨几行写的调用时,是那个调用的实参表,
|
||||
* 表里的行又可以带 table,一层层往下套。总是展开的。
|
||||
* 最后一格的值里那次调用的实参表(跨几行写的调用;列表里整项是调用的,写成一行也算):表格的行把它排在最后一格里、
|
||||
* 那格文字下面,排成列表的子行把它排在自己下面。总是展开的;表里的行又可以带 items、table,一层层往下套。
|
||||
*/
|
||||
table?: PreviewTable
|
||||
}
|
||||
|
||||
/**
|
||||
* 属性表的一行:左列属性名,右列着色的值或 Markdown 说明;items 是展开在这一行下面的子行(输入参数、导入项、字典 / 对象的各项)。
|
||||
* columns 是子行各列的列名(输入参数的名称 / 类型 / 默认值):给了列名的子行排成带表头和边框的表格,没给的排成列表;表格只有一层,子行不再往下套。
|
||||
* 属性表的一行:左列属性名,右列着色的值或 Markdown 说明;items 是展开在这一行下面的子行(输入参数、导入项、字典 / 对象、列表的各项)。
|
||||
* columns 是子行各列的列名(输入参数的名称 / 类型 / 默认值):给了列名的子行排成带表头和边框的表格,没给的排成列表;
|
||||
* 两种子行都能再带下一层(PropertyItem.items、table)。
|
||||
*/
|
||||
export interface PropertyRow extends Defines {
|
||||
label: string
|
||||
@@ -735,6 +750,8 @@ export interface LanguageAdapter {
|
||||
iteration?: (node: Node) => IterationFacts | undefined
|
||||
/** node 是一次调用(含新建对象)时给出它的结构,别的节点没有;没给的语言不展开调用,照常一行译文。 */
|
||||
call?: (node: Node) => CallFacts | undefined
|
||||
/** node 是写出各项的字面量(字典、列表……)时给出它的各项,别的节点没有;没给的语言不拆字面量,照常一行译文。 */
|
||||
collection?: (node: Node) => CollectionFacts | undefined
|
||||
/** node 是执行一条命令(shell)时给出它的结构,交给命令规则读;别的节点没有,没有命令的语言不填。 */
|
||||
command?: (node: Node) => CommandFacts | undefined
|
||||
/** 一份源码里自己认得出的名字(见 LocalSymbol);VS Code 有这种语言的定义、高亮提供者时用不上,不填。 */
|
||||
|
||||
@@ -304,33 +304,66 @@ describe('python 属性表信息(facts)', () => {
|
||||
expect(found[10]).not.toHaveProperty('access')
|
||||
})
|
||||
|
||||
it('字典字面量的各项:字符串键、名字键、嵌套字典连它的各项、**展开;空字典是空数组,别的值没有;类属性也有', async () => {
|
||||
it('声明的值照源码给,字面量的各项不在 facts 里(见 LanguageAdapter.collection)', async () => {
|
||||
const found = await facts('EMPTY = {}\nITEMS = [1]\nclass K:\n labels = {"x": 1}')
|
||||
expect(found[1]).toEqual({ access: 'public', declarators: [{ name: 'EMPTY', value: '{}' }] })
|
||||
expect(found[2]).toEqual({ access: 'public', declarators: [{ name: 'ITEMS', value: '[1]' }] })
|
||||
expect(found[4]).toEqual({ access: 'public', declarators: [{ name: 'labels', value: '{"x": 1}' }] })
|
||||
})
|
||||
})
|
||||
|
||||
describe('python 字面量的各项', () => {
|
||||
const collections = (source: string): Promise<unknown[]> => nodeFacts(adapter, 'python', source, node => adapter.collection?.(node))
|
||||
|
||||
it('字典:字符串键、名字键、**展开,注释不算一项;值里的字典另有自己的各项,空字典没有项', async () => {
|
||||
const source = [
|
||||
'_KEYS = {',
|
||||
' "published": "contests:publish",',
|
||||
' key: 1,',
|
||||
' # 注释不算一项',
|
||||
' "nested": {"a": {}, "b": [1]},',
|
||||
' "nested": {"a": {}},',
|
||||
' **other,',
|
||||
'}',
|
||||
'EMPTY = {}',
|
||||
'ITEMS = [1]',
|
||||
'class K:',
|
||||
' labels = {"x": 1}',
|
||||
].join('\n')
|
||||
const found = await facts(source)
|
||||
expect(found[1]).toMatchObject({ access: 'private', declarators: [{ name: '_KEYS', entries: [
|
||||
{ kind: 'pair', key: '"published"', value: '"contests:publish"' },
|
||||
{ kind: 'pair', key: 'key', value: '1' },
|
||||
{ kind: 'pair', key: '"nested"', value: '{"a": {}, "b": [1]}', entries: [
|
||||
{ kind: 'pair', key: '"a"', value: '{}', entries: [] },
|
||||
{ kind: 'pair', key: '"b"', value: '[1]' },
|
||||
expect(await collections(source)).toEqual([
|
||||
{ kind: 'map', entries: [
|
||||
{ kind: 'pair', key: '"published"', value: '"contests:publish"' },
|
||||
{ kind: 'pair', key: 'key', value: '1' },
|
||||
{ kind: 'pair', key: '"nested"', value: '{"a": {}}' },
|
||||
{ kind: 'spread', value: 'other' },
|
||||
] },
|
||||
{ kind: 'spread', value: 'other' },
|
||||
] }] })
|
||||
expect(found[8]).toEqual({ access: 'public', declarators: [{ name: 'EMPTY', value: '{}', entries: [] }] })
|
||||
expect(found[9]).toEqual({ access: 'public', declarators: [{ name: 'ITEMS', value: '[1]' }] })
|
||||
expect(found[11]).toEqual({ access: 'public', declarators: [{ name: 'labels', value: '{"x": 1}', entries: [{ kind: 'pair', key: '"x"', value: '1' }] }] })
|
||||
{ kind: 'map', entries: [{ kind: 'pair', key: '"a"', value: '{}' }] },
|
||||
{ kind: 'map', entries: [] },
|
||||
])
|
||||
})
|
||||
|
||||
it('列表、元组、集合:各项照源码顺序,*x 是展开,注释不算一项;值里的字面量另有自己的各项,空列表没有项', async () => {
|
||||
const source = [
|
||||
'filters = [',
|
||||
' Filter(field="user_id"),',
|
||||
' # 注释不算一项',
|
||||
' *rest,',
|
||||
' (a, b),',
|
||||
' {1},',
|
||||
' [],',
|
||||
']',
|
||||
].join('\n')
|
||||
expect(await collections(source)).toEqual([
|
||||
{ kind: 'sequence', elements: [
|
||||
{ kind: 'item', value: 'Filter(field="user_id")' },
|
||||
{ kind: 'spread', value: 'rest' },
|
||||
{ kind: 'item', value: '(a, b)' },
|
||||
{ kind: 'item', value: '{1}' },
|
||||
{ kind: 'item', value: '[]' },
|
||||
] },
|
||||
{ kind: 'sequence', elements: [{ kind: 'item', value: 'a' }, { kind: 'item', value: 'b' }] },
|
||||
{ kind: 'sequence', elements: [{ kind: 'item', value: '1' }] },
|
||||
{ kind: 'sequence', elements: [] },
|
||||
])
|
||||
})
|
||||
|
||||
it('推导式、括号里的一个式子、调用的实参列表、别的值不是', async () => {
|
||||
expect(await collections('x = [a for a in b]\ny = (a)\nf(a, b)\nz = a, b')).toEqual([])
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { Access, BlockShape, CallArgument, CallFacts, ClassFacts, CodeBlock, CommentBlock, EntryFacts, FunctionFacts, ImportFacts, ImportItem, IterationClause, IterationFacts, IterationResult, LanguageAdapter, Node, ParameterFacts, TextKind, TokenRole } from '../contract'
|
||||
import type { Access, BlockShape, CallArgument, CallFacts, ClassFacts, CodeBlock, CollectionFacts, CommentBlock, ElementFacts, EntryFacts, FunctionFacts, ImportFacts, ImportItem, IterationClause, IterationFacts, IterationResult, LanguageAdapter, Node, ParameterFacts, TextKind, TokenRole } from '../contract'
|
||||
import type { CommentRules, ExceptionRules } from './shared'
|
||||
import * as path from 'node:path'
|
||||
import { pythonNameReader } from './pythonNames'
|
||||
@@ -303,7 +303,7 @@ function expressionBlock(node: Node, scope: Scope, doc: Node | undefined): CodeB
|
||||
facts: {
|
||||
access: scopeAccess(left, scope),
|
||||
doc,
|
||||
declarators: [{ name: left, type: type ?? undefined, value: value ?? undefined, entries: dictionaryEntries(value) }],
|
||||
declarators: [{ name: left, type: type ?? undefined, value: value ?? undefined }],
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -314,21 +314,34 @@ function assignmentIn(statement: Node): Node | undefined {
|
||||
return expression?.type === 'assignment' ? expression : undefined
|
||||
}
|
||||
|
||||
/** 字典字面量的各项:`键: 值`(值又是字典时连它的各项)、`**other`;值不是字典字面量时没有。 */
|
||||
function dictionaryEntries(value: Node | null): EntryFacts[] | undefined {
|
||||
if (value?.type !== 'dictionary')
|
||||
/** 列表、元组、集合字面量(各项是 ElementFacts)的节点类型。 */
|
||||
const SEQUENCES = new Set(['list', 'tuple', 'set'])
|
||||
|
||||
/** 字面量的各项:字典的 `键: 值`、`**other`,列表、元组、集合的各项、`*other`;别的节点没有。 */
|
||||
function pythonCollection(node: Node): CollectionFacts | undefined {
|
||||
if (node.type === 'dictionary')
|
||||
return { kind: 'map', entries: syntaxChildren(node).flatMap(dictionaryEntry) }
|
||||
if (!SEQUENCES.has(node.type))
|
||||
return undefined
|
||||
return syntaxChildren(value).flatMap((item): EntryFacts[] => {
|
||||
if (item.type === 'dictionary_splat') {
|
||||
const [spread] = syntaxChildren(item)
|
||||
return {
|
||||
kind: 'sequence',
|
||||
elements: syntaxChildren(node).flatMap((element): ElementFacts[] => {
|
||||
if (element.type !== 'list_splat')
|
||||
return [{ kind: 'item', value: element }]
|
||||
const [spread] = syntaxChildren(element)
|
||||
return spread ? [{ kind: 'spread', value: spread }] : []
|
||||
}
|
||||
const key = item.childForFieldName('key')
|
||||
const itemValue = item.childForFieldName('value')
|
||||
if (item.type !== 'pair' || !key || !itemValue)
|
||||
return []
|
||||
return [{ kind: 'pair', key, value: itemValue, entries: dictionaryEntries(itemValue) }]
|
||||
})
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
function dictionaryEntry(item: Node): EntryFacts[] {
|
||||
if (item.type === 'dictionary_splat') {
|
||||
const [spread] = syntaxChildren(item)
|
||||
return spread ? [{ kind: 'spread', value: spread }] : []
|
||||
}
|
||||
const key = item.childForFieldName('key')
|
||||
const value = item.childForFieldName('value')
|
||||
return item.type === 'pair' && key && value ? [{ kind: 'pair', key, value }] : []
|
||||
}
|
||||
|
||||
const ITERATION_RESULTS: Record<string, IterationResult> = {
|
||||
@@ -671,6 +684,7 @@ export function createPythonAdapter(wasmDir: string): LanguageAdapter {
|
||||
freshDoc,
|
||||
iteration: pythonIteration,
|
||||
call: pythonCall,
|
||||
collection: pythonCollection,
|
||||
displayNames: pythonNameReader,
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,7 @@ import { toBlockViews } from '../blockView'
|
||||
import { blockMarks } from '../diff/viewDiff'
|
||||
import { rustImplRule } from '../rules/rustImpl'
|
||||
import { isCrateRoot } from '../rules/rustTree'
|
||||
import { commentText, outline, propertyLines, readFixture, viewLines, WASM_DIR, withBlocks } from '../testSupport'
|
||||
import { commentText, nodeFacts, outline, propertyLines, readFixture, viewLines, WASM_DIR, withBlocks } from '../testSupport'
|
||||
import { rustZhTranslator } from '../translators/rustZh'
|
||||
import { createRustAdapter } from './rust'
|
||||
|
||||
@@ -188,6 +188,29 @@ describe('rust 语言卡片', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('rust 字面量的各项', () => {
|
||||
it('数组、元组的各项照源码顺序,注释不算一项;[值; 个数]、别的值不是', async () => {
|
||||
const source = 'const A: [&str; 2] = [\n "a", // 第一个\n "b",\n];\nconst Z: [u8; 4] = [0; 4];\nconst T: (u8, u8) = (1, 2);\nfn f() { g(a, b); }'
|
||||
expect(await nodeFacts(adapter, 'rust', source, node => adapter.collection?.(node))).toEqual([
|
||||
{ kind: 'sequence', elements: [{ kind: 'item', value: '"a"' }, { kind: 'item', value: '"b"' }] },
|
||||
{ kind: 'sequence', elements: [{ kind: 'item', value: '1' }, { kind: 'item', value: '2' }] },
|
||||
])
|
||||
})
|
||||
|
||||
it('常量的值是跨几行写的数组、元组时按项拆开:「数组 N 项」,每项一行「第 N 项 | 值」;写在一行里的照旧', async () => {
|
||||
const source = 'pub const NAMES: [&str; 2] = [\n "a",\n "b",\n];\npub const PAIR: (u8, u8) = (\n 1,\n 2,\n);\npub const ONE: [u8; 2] = [1, 2];\n'
|
||||
expect(viewLines(await views(source)).filter(line => /[值第|]/.test(line))).toEqual([
|
||||
' 值: 数组 2 项',
|
||||
' 第 1 项 | "a"',
|
||||
' 第 2 项 | "b"',
|
||||
' 值: 元组 2 项',
|
||||
' 第 1 项 | 1',
|
||||
' 第 2 项 | 2',
|
||||
' 值: [1, 2]',
|
||||
])
|
||||
})
|
||||
})
|
||||
|
||||
describe('rust 注释', () => {
|
||||
it('连着写的 // 合成一块注释,空的 // 是分段,预览是去掉 // 的 Markdown;后面的 use 照常是导入', async () => {
|
||||
const list = await views(readFixture('rustComments.rs'))
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import type { Access, AttributeFacts, AttributeItem, CfgPredicate, ClassFacts, CodeBlock, CommentBlock, EnumBlock, FunctionBlock, ImplBlock, ImportBlock, ImportItem, LanguageAdapter, Node, ParameterFacts, SourceSpan, TokenRole, TraitBlock } from '../contract'
|
||||
import type { Access, AttributeFacts, AttributeItem, CfgPredicate, ClassFacts, CodeBlock, CollectionFacts, CommentBlock, EnumBlock, FunctionBlock, ImplBlock, ImportBlock, ImportItem, LanguageAdapter, Node, ParameterFacts, SourceSpan, TokenRole, TraitBlock } from '../contract'
|
||||
import type { CommentRules } from './shared'
|
||||
import * as path from 'node:path'
|
||||
import { leadingAttributes, visibilityFacts } from '../rules/rustTree'
|
||||
import { attachComments, children, compoundShape, extendHeader, headerSpan, isCommentNode, leafBlock, leafShape, listBlocks, named } from './shared'
|
||||
import { attachComments, children, compoundShape, extendHeader, headerSpan, isCommentNode, leafBlock, leafShape, listBlocks, named, syntaxChildren } from './shared'
|
||||
|
||||
const STORAGE = new Set(['pub', 'async', 'const', 'static', 'mut', 'let'])
|
||||
|
||||
@@ -15,9 +15,18 @@ export function createRustAdapter(wasmDir: string): LanguageAdapter {
|
||||
tokenRole: rustTokenRole,
|
||||
lineComment: '//',
|
||||
freshDoc,
|
||||
collection: rustCollection,
|
||||
}
|
||||
}
|
||||
|
||||
/** 数组 `[a, b]`、元组 `(a, b)` 的各项;`[值; 个数]` 不是一项项写出来的,别的节点也没有。项前面的 `#[…]` 不算一项。 */
|
||||
function rustCollection(node: Node): CollectionFacts | undefined {
|
||||
const listed = node.type === 'tuple_expression' || (node.type === 'array_expression' && !node.childForFieldName('length'))
|
||||
if (!listed)
|
||||
return undefined
|
||||
return { kind: 'sequence', elements: syntaxChildren(node).filter(child => child.type !== 'attribute_item').map(value => ({ kind: 'item', value })) }
|
||||
}
|
||||
|
||||
/** extern 块本身不成一块源码,里面的签名跟所在这一层的别的项走同一条路。 */
|
||||
function unwrapExtern(item: Node): Node[] {
|
||||
if (item.type !== 'foreign_mod_item')
|
||||
|
||||
@@ -338,54 +338,13 @@ describe('typeScript 属性表信息(facts)', () => {
|
||||
name: 'result',
|
||||
type: 'EnvLockResult',
|
||||
value: '{ total: files.length, ok: [], failed: [] }',
|
||||
entries: [
|
||||
{ kind: 'pair', key: 'total', value: 'files.length' },
|
||||
{ kind: 'pair', key: 'ok', value: '[]' },
|
||||
{ kind: 'pair', key: 'failed', value: '[]' },
|
||||
],
|
||||
}] })
|
||||
})
|
||||
|
||||
it('对象字面量的各项:各种键、简写(键和值是同一个节点)、展开、方法(头部到主体之前)、嵌套对象连它的各项;空对象是空数组,别的值没有', async () => {
|
||||
const source = [
|
||||
'const o = {',
|
||||
' a: 1,',
|
||||
' \'b-c\': x,',
|
||||
' [k]: 2,',
|
||||
' s,',
|
||||
' ...rest,',
|
||||
' // 注释不算一项',
|
||||
' async load(id: string): Promise<void> {',
|
||||
' await run(id)',
|
||||
' },',
|
||||
' get size() { return 1 },',
|
||||
' n: { d: 4, e: {} },',
|
||||
'}',
|
||||
'const empty = {}',
|
||||
'const list = [1]',
|
||||
].join('\n')
|
||||
const at = (text: string): number => source.indexOf(text)
|
||||
const found = await facts(source)
|
||||
expect(found[1]).toMatchObject({ declarators: [{ entries: [
|
||||
{ kind: 'pair', key: 'a', value: '1' },
|
||||
{ kind: 'pair', key: '\'b-c\'', value: 'x' },
|
||||
{ kind: 'pair', key: '[k]', value: '2' },
|
||||
{ kind: 'pair', key: 's', value: 's' },
|
||||
{ kind: 'spread', value: 'rest' },
|
||||
{
|
||||
kind: 'method',
|
||||
name: 'load',
|
||||
node: 'async load(id: string): Promise<void> {\n await run(id)\n }',
|
||||
header: { startIndex: at('async load'), endIndex: at(' {\n await'), startRow: 7, endRow: 7 },
|
||||
},
|
||||
{ kind: 'method', name: 'size', header: { startIndex: at('get size'), endIndex: at(' { return 1'), startRow: 10, endRow: 10 } },
|
||||
{ kind: 'pair', key: 'n', value: '{ d: 4, e: {} }', entries: [
|
||||
{ kind: 'pair', key: 'd', value: '4' },
|
||||
{ kind: 'pair', key: 'e', value: '{}', entries: [] },
|
||||
] },
|
||||
] }] })
|
||||
expect(found[14]).toEqual({ access: 'private', declarators: [{ name: 'empty', value: '{}', entries: [] }] })
|
||||
expect(found[15]).toEqual({ access: 'private', declarators: [{ name: 'list', value: '[1]' }] })
|
||||
it('声明的值照源码给,字面量的各项不在 facts 里(见 LanguageAdapter.collection)', async () => {
|
||||
const found = await facts('const empty = {}\nconst list = [1]')
|
||||
expect(found[1]).toEqual({ access: 'private', declarators: [{ name: 'empty', value: '{}' }] })
|
||||
expect(found[2]).toEqual({ access: 'private', declarators: [{ name: 'list', value: '[1]' }] })
|
||||
})
|
||||
|
||||
it('导入:默认导入、* as、仅类型、副作用导入、require', async () => {
|
||||
@@ -485,6 +444,72 @@ describe('typeScript 属性表信息(facts)', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('typeScript 字面量的各项', () => {
|
||||
const collections = (source: string): Promise<unknown[]> => nodeFacts(adapter, 'typescript', source, node => adapter.collection?.(node))
|
||||
|
||||
it('对象:各种键、简写(键和值是同一个节点)、展开、方法(头部到主体之前),注释不算一项;值里的对象另有自己的各项,空对象没有项', async () => {
|
||||
const source = [
|
||||
'const o = {',
|
||||
' a: 1,',
|
||||
' \'b-c\': x,',
|
||||
' [k]: 2,',
|
||||
' s,',
|
||||
' ...rest,',
|
||||
' // 注释不算一项',
|
||||
' async load(id: string): Promise<void> {',
|
||||
' await run(id)',
|
||||
' },',
|
||||
' get size() { return 1 },',
|
||||
' n: { d: 4, e: {} },',
|
||||
'}',
|
||||
].join('\n')
|
||||
const at = (text: string): number => source.indexOf(text)
|
||||
expect(await collections(source)).toEqual([
|
||||
{ kind: 'map', entries: [
|
||||
{ kind: 'pair', key: 'a', value: '1' },
|
||||
{ kind: 'pair', key: '\'b-c\'', value: 'x' },
|
||||
{ kind: 'pair', key: '[k]', value: '2' },
|
||||
{ kind: 'pair', key: 's', value: 's' },
|
||||
{ kind: 'spread', value: 'rest' },
|
||||
{
|
||||
kind: 'method',
|
||||
name: 'load',
|
||||
node: 'async load(id: string): Promise<void> {\n await run(id)\n }',
|
||||
header: { startIndex: at('async load'), endIndex: at(' {\n await'), startRow: 7, endRow: 7 },
|
||||
},
|
||||
{ kind: 'method', name: 'size', node: 'get size() { return 1 }', header: { startIndex: at('get size'), endIndex: at(' { return 1'), startRow: 10, endRow: 10 } },
|
||||
{ kind: 'pair', key: 'n', value: '{ d: 4, e: {} }' },
|
||||
] },
|
||||
{ kind: 'map', entries: [{ kind: 'pair', key: 'd', value: '4' }, { kind: 'pair', key: 'e', value: '{}' }] },
|
||||
{ kind: 'map', entries: [] },
|
||||
])
|
||||
})
|
||||
|
||||
it('数组:各项照源码顺序,...x 是展开,注释不算一项;值里的字面量另有自己的各项,空数组没有项', async () => {
|
||||
const source = [
|
||||
'const filters = [',
|
||||
' new Filter({ field: \'user_id\' }),',
|
||||
' // 注释不算一项',
|
||||
' ...rest,',
|
||||
' [],',
|
||||
']',
|
||||
].join('\n')
|
||||
expect(await collections(source)).toEqual([
|
||||
{ kind: 'sequence', elements: [
|
||||
{ kind: 'item', value: 'new Filter({ field: \'user_id\' })' },
|
||||
{ kind: 'spread', value: 'rest' },
|
||||
{ kind: 'item', value: '[]' },
|
||||
] },
|
||||
{ kind: 'map', entries: [{ kind: 'pair', key: 'field', value: '\'user_id\'' }] },
|
||||
{ kind: 'sequence', elements: [] },
|
||||
])
|
||||
})
|
||||
|
||||
it('数组形式的解构、类型里的元组、别的值不是', async () => {
|
||||
expect(await collections('const [a, b] = pair\nlet t: [string, number]\nf(a, b)')).toEqual([])
|
||||
})
|
||||
})
|
||||
|
||||
describe('typeScript 类', () => {
|
||||
it('classes.ts:紧贴的 JSDoc(隔着装饰器也算)算进类头部;类表达式赋给变量、类字段也是类块;函数里的类照常成块', async () => {
|
||||
expect(await tree(readFixture('classes.ts.txt'))).toEqual([
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { Access, BaseFacts, BlockShape, CallArgument, CallFacts, ClassFacts, CodeBlock, CommentBlock, DeclaratorFacts, EntryFacts, EnumMemberFacts, FunctionFacts, ImportFacts, ImportItem, LanguageAdapter, Node, ParameterFacts, TextKind, TokenRole } from '../contract'
|
||||
import type { Access, BaseFacts, BlockShape, CallArgument, CallFacts, ClassFacts, CodeBlock, CollectionFacts, CommentBlock, DeclaratorFacts, ElementFacts, EntryFacts, EnumMemberFacts, FunctionFacts, ImportFacts, ImportItem, LanguageAdapter, Node, ParameterFacts, TextKind, TokenRole } from '../contract'
|
||||
import type { CommentRules, ExceptionRules } from './shared'
|
||||
import * as path from 'node:path'
|
||||
import { attachComments, children, compoundShape, extendHeader, headerSpan, leafBlock, leafShape, listBlocks, named, nodeSpan, sameNode, syntaxChildren, withExceptions } from './shared'
|
||||
@@ -153,36 +153,47 @@ function declaratorFacts(declarator: Node): DeclaratorFacts {
|
||||
name: declarator.childForFieldName('name') ?? declarator,
|
||||
type: annotatedType(declarator.childForFieldName('type')),
|
||||
value: value ?? undefined,
|
||||
entries: objectEntries(value),
|
||||
}
|
||||
}
|
||||
|
||||
/** 对象字面量的各项:`键: 值`(值又是对象时连它的各项)、简写 `{ a }`、`...other`、方法;值不是对象字面量时没有。 */
|
||||
function objectEntries(value: Node | null): EntryFacts[] | undefined {
|
||||
if (value?.type !== 'object')
|
||||
/** 字面量的各项:对象的 `键: 值`、简写 `{ a }`、`...other`、方法,数组的各项、`...other`;别的节点没有。 */
|
||||
function typescriptCollection(node: Node): CollectionFacts | undefined {
|
||||
if (node.type === 'object')
|
||||
return { kind: 'map', entries: syntaxChildren(node).flatMap(objectEntry) }
|
||||
if (node.type !== 'array')
|
||||
return undefined
|
||||
return syntaxChildren(value).flatMap((member): EntryFacts[] => {
|
||||
switch (member.type) {
|
||||
case 'pair': {
|
||||
const key = member.childForFieldName('key')
|
||||
const memberValue = member.childForFieldName('value')
|
||||
return key && memberValue ? [{ kind: 'pair', key, value: memberValue, entries: objectEntries(memberValue) }] : []
|
||||
}
|
||||
case 'shorthand_property_identifier':
|
||||
return [{ kind: 'pair', key: member, value: member }]
|
||||
case 'spread_element': {
|
||||
const [spread] = syntaxChildren(member)
|
||||
return spread ? [{ kind: 'spread', value: spread }] : []
|
||||
}
|
||||
case 'method_definition': {
|
||||
const name = member.childForFieldName('name')
|
||||
const body = member.childForFieldName('body')
|
||||
return name ? [{ kind: 'method', name, node: member, header: body ? headerSpan(member, body.startIndex) : nodeSpan(member) }] : []
|
||||
}
|
||||
default:
|
||||
return []
|
||||
return {
|
||||
kind: 'sequence',
|
||||
elements: syntaxChildren(node).flatMap((element): ElementFacts[] => {
|
||||
if (element.type !== 'spread_element')
|
||||
return [{ kind: 'item', value: element }]
|
||||
const [spread] = syntaxChildren(element)
|
||||
return spread ? [{ kind: 'spread', value: spread }] : []
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
function objectEntry(member: Node): EntryFacts[] {
|
||||
switch (member.type) {
|
||||
case 'pair': {
|
||||
const key = member.childForFieldName('key')
|
||||
const value = member.childForFieldName('value')
|
||||
return key && value ? [{ kind: 'pair', key, value }] : []
|
||||
}
|
||||
})
|
||||
case 'shorthand_property_identifier':
|
||||
return [{ kind: 'pair', key: member, value: member }]
|
||||
case 'spread_element': {
|
||||
const [spread] = syntaxChildren(member)
|
||||
return spread ? [{ kind: 'spread', value: spread }] : []
|
||||
}
|
||||
case 'method_definition': {
|
||||
const name = member.childForFieldName('name')
|
||||
const body = member.childForFieldName('body')
|
||||
return name ? [{ kind: 'method', name, node: member, header: body ? headerSpan(member, body.startIndex) : nodeSpan(member) }] : []
|
||||
}
|
||||
default:
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
/** import 语句:默认导入、`* as x`、`{ a as b }`、`import x = require('m')`;TypeScript(typed)的 `import type` 只导入类型。 */
|
||||
@@ -658,6 +669,7 @@ export function createTypeScriptAdapter(wasmDir: string): LanguageAdapter {
|
||||
lineComment: '//',
|
||||
freshDoc,
|
||||
call: typescriptCall,
|
||||
collection: typescriptCollection,
|
||||
displayNames: typescriptNameReader,
|
||||
}
|
||||
}
|
||||
@@ -551,7 +551,7 @@ export const bashZhTranslator: Translator = createZhTemplateTranslator({
|
||||
operators: {},
|
||||
operatorParents: [],
|
||||
emptyLiterals: {},
|
||||
mapLiterals: {},
|
||||
collections: {},
|
||||
stringNodes: [],
|
||||
commentText,
|
||||
nodeTemplates: {},
|
||||
|
||||
@@ -45,7 +45,7 @@ const translator = createZhTemplateTranslator({
|
||||
operators: {},
|
||||
operatorParents: [],
|
||||
emptyLiterals: {},
|
||||
mapLiterals: {},
|
||||
collections: {},
|
||||
stringNodes: [],
|
||||
commentText: node => node.text,
|
||||
nodeTemplates: {},
|
||||
|
||||
@@ -392,7 +392,7 @@ export const dockerfileZhTranslator = createZhTemplateTranslator({
|
||||
operators: {},
|
||||
operatorParents: [],
|
||||
emptyLiterals: {},
|
||||
mapLiterals: {},
|
||||
collections: {},
|
||||
stringNodes: [],
|
||||
commentText: (_node, text) => text.replace(/^#[ \t]?/, ''),
|
||||
nodeTemplates: {},
|
||||
|
||||
@@ -89,7 +89,7 @@ export const pythonZhTranslator: Translator = createZhTemplateTranslator({
|
||||
operators: { '==': '等于', '!=': '不等于' },
|
||||
operatorParents: ['comparison_operator'],
|
||||
emptyLiterals: { list: '空列表', dictionary: '空字典' },
|
||||
mapLiterals: { dictionary: '字典' },
|
||||
collections: { dictionary: '字典', list: '列表', tuple: '元组', set: '集合' },
|
||||
stringNodes: ['string'],
|
||||
commentText,
|
||||
// `a if c else b` 的三个具名子节点依次是 a、c、b,语法没给它们起字段名
|
||||
|
||||
@@ -50,7 +50,7 @@ const template = createZhTemplateTranslator({
|
||||
operators: {},
|
||||
operatorParents: [],
|
||||
emptyLiterals: {},
|
||||
mapLiterals: {},
|
||||
collections: { array_expression: '数组', tuple_expression: '元组' },
|
||||
stringNodes: ['string_literal'],
|
||||
commentText,
|
||||
docNodes,
|
||||
|
||||
@@ -83,7 +83,7 @@ export const typescriptZhTranslator: Translator = createZhTemplateTranslator({
|
||||
operators: { '===': '全等', '!==': '不全等', '==': '等于', '!=': '不等于', '&&': '并且', '||': '或者', '!': '非' },
|
||||
operatorParents: ['binary_expression', 'unary_expression'],
|
||||
emptyLiterals: { array: '空数组', object: '空对象' },
|
||||
mapLiterals: { object: '对象' },
|
||||
collections: { object: '对象', array: '数组' },
|
||||
stringNodes: ['string', 'template_string', 'regex'],
|
||||
commentText,
|
||||
nodeTemplates: {
|
||||
|
||||
@@ -1336,7 +1336,8 @@ describe('跨几行写的调用', () => {
|
||||
expect(value(1)).toBe('初始值: (调用 make · 1 个参数)[0]')
|
||||
expect(value(4)).toBe('初始值: (调用 make · 1 个参数) 或者 fallback')
|
||||
expect(value(7)).toBe('初始值: fallback 或者 调用 make · 1 个参数')
|
||||
expect(value(10)).toBe('初始值: [ 调用 make · 1 个参数, b ]')
|
||||
// 跨几行写的列表按项拆开(见「跨几行写的列表」),整项是调用的不括
|
||||
expect(propertyLines(values.get(10) ?? []).slice(2)).toEqual(['初始值: 列表 2 项', ' 第 1 项 | 调用 make · 1 个参数', ' 第 1 个 | a', ' 第 2 项 | b'])
|
||||
const cell = (await translateAll(py, 'python', pythonZhTranslator, source)).get(16)
|
||||
expect(tableLines(cell?.table)).toEqual(['key | (调用 Model · 1 个参数).model_dump()', ' a | 1'])
|
||||
})
|
||||
@@ -1398,6 +1399,183 @@ describe('跨几行写的调用', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('跨几行写的列表', () => {
|
||||
/** Filter 的定义是类,别的名字还没查到。 */
|
||||
const calleeKind = (call: CallFacts): BlockKind | undefined => call.callee.text === 'Filter' ? 'class' : undefined
|
||||
|
||||
const FILTERS = [
|
||||
'async def list_sessions(archived: bool, current_user: User):',
|
||||
' filters = [',
|
||||
' Filter(field="user_id", operator=FilterOperator.EQ, value=current_user.id),',
|
||||
' Filter(',
|
||||
' field="archived_at",',
|
||||
' operator=FilterOperator.NE if archived else FilterOperator.EQ,',
|
||||
' value=None,',
|
||||
' ),',
|
||||
' ]',
|
||||
].join('\n')
|
||||
|
||||
it('初始值写「列表 N 项」,每项一个子行「第 N 项 | 值」;整项是调用的写成一行还是几行都排成「新建 / 调用 …」,实参表跟着这一项', async () => {
|
||||
const rows = (await describeAll(py, 'python', pythonZhTranslator, FILTERS, calleeKind)).get(2) ?? []
|
||||
expect(propertyLines(rows)).toEqual([
|
||||
'名称: filters',
|
||||
'初始值: 列表 2 项',
|
||||
' 第 1 项 | 新建 Filter · 3 个参数',
|
||||
' field | "user_id"',
|
||||
' operator | FilterOperator.EQ',
|
||||
' value | current_user.id',
|
||||
' 第 2 项 | 新建 Filter · 3 个参数',
|
||||
' field | "archived_at"',
|
||||
' operator | 如果 archived 则 FilterOperator.NE 否则 FilterOperator.EQ',
|
||||
' value | 空',
|
||||
])
|
||||
const value = rows.find(row => row.label === '初始值')
|
||||
expect(value?.columns).toBeUndefined()
|
||||
expect(colors(value?.value)).toEqual([{ text: '列表 2 项', role: 'hint' }])
|
||||
const [first, second] = value?.items ?? []
|
||||
expect(first?.cells[0]).toEqual([{ text: '第 1 项', role: 'hint' }])
|
||||
expect(first).not.toHaveProperty('items')
|
||||
expect(first?.table?.columns).toEqual(['参数', '值'])
|
||||
expect(second?.table?.columns).toEqual(['参数', '值'])
|
||||
// 拆开以后名字照样带源码下标(跳到定义、悬停、中文名、出现处高亮都靠它):被调用的名字、按名字传的名字、值里的名字
|
||||
const multiline = FILTERS.indexOf('Filter(\n')
|
||||
expect(inlineOf(second?.cells[1] ?? []).find(piece => 'role' in piece && piece.text === 'Filter')).toEqual({ text: 'Filter', role: 'type', offset: multiline })
|
||||
expect(second?.table?.items[0].cells[0]).toEqual([{ text: 'field', role: 'variable', offset: FILTERS.indexOf('field="archived_at"') }])
|
||||
const user = FILTERS.indexOf('current_user.id')
|
||||
expect(linked(first?.table?.items[2].cells[1])).toEqual([['current_user', user], ['id', user + 13]])
|
||||
})
|
||||
|
||||
it('写在一行里的列表照旧一行译文,里面的调用也照旧;空列表写「空列表」', async () => {
|
||||
const source = 'ids = [a, Filter(field="x")]\nempty = []'
|
||||
const rows = await describeAll(py, 'python', pythonZhTranslator, source, calleeKind)
|
||||
expect(propertyLines(rows.get(1) ?? [])).toEqual(['名称: ids', '访问级别: 公共', '初始值: [a, Filter(field="x")]'])
|
||||
expect(propertyLines(rows.get(2) ?? [])).toEqual(['名称: empty', '访问级别: 公共', '初始值: 空列表'])
|
||||
})
|
||||
|
||||
it('元组、集合各用自己的叫法,*x 写「展开」、「第 N 项」只数不是展开的;项是字典照字典拆,是跨几行的列表接着拆,一层层往下套', async () => {
|
||||
const source = [
|
||||
'PAIR = (',
|
||||
' first,',
|
||||
' *rest,',
|
||||
' second,',
|
||||
')',
|
||||
'TAGS = {',
|
||||
' "a",',
|
||||
' "b",',
|
||||
'}',
|
||||
'CONFIG = [',
|
||||
' {"name": "a", "tags": [',
|
||||
' "x",',
|
||||
' "y",',
|
||||
' ]},',
|
||||
' [1, 2],',
|
||||
' [',
|
||||
' 3,',
|
||||
' 4,',
|
||||
' ],',
|
||||
']',
|
||||
].join('\n')
|
||||
const rows = await describeAll(py, 'python', pythonZhTranslator, source)
|
||||
expect(propertyLines(rows.get(1) ?? []).slice(2)).toEqual(['值: 元组 3 项', ' 第 1 项 | first', ' 展开 | rest', ' 第 2 项 | second'])
|
||||
expect(propertyLines(rows.get(6) ?? []).slice(2)).toEqual(['值: 集合 2 项', ' 第 1 项 | "a"', ' 第 2 项 | "b"'])
|
||||
expect(propertyLines(rows.get(10) ?? []).slice(2)).toEqual([
|
||||
'值: 列表 3 项',
|
||||
' 第 1 项 | 字典 2 项',
|
||||
' "name" | "a"',
|
||||
' "tags" | 列表 2 项',
|
||||
' 第 1 项 | "x"',
|
||||
' 第 2 项 | "y"',
|
||||
' 第 2 项 | [1, 2]',
|
||||
' 第 3 项 | 列表 2 项',
|
||||
' 第 1 项 | 3',
|
||||
' 第 2 项 | 4',
|
||||
])
|
||||
// 字典的项照旧标出它讲的定义(键),列表的项不讲哪个定义
|
||||
const config = rows.get(10)?.find(row => row.label === '值')
|
||||
expect(config?.items?.[0]).not.toHaveProperty('defines')
|
||||
const tags = source.indexOf('"tags"')
|
||||
expect(config?.items?.[0].items?.[1].defines).toEqual({ startIndex: tags, endIndex: tags + 6 })
|
||||
})
|
||||
|
||||
it('字典一项的值是跨几行写的列表时也按项拆', async () => {
|
||||
const source = 'ROUTES = {"admin": [\n "users",\n "roles",\n]}'
|
||||
expect(propertyLines((await describeAll(py, 'python', pythonZhTranslator, source)).get(1) ?? []).slice(2)).toEqual([
|
||||
'值: 字典 1 项',
|
||||
' "admin" | 列表 2 项',
|
||||
' 第 1 项 | "users"',
|
||||
' 第 2 项 | "roles"',
|
||||
])
|
||||
})
|
||||
|
||||
it('实参的值是跨几行写的列表时也按项拆,嵌在那一格下面;写在一行里的列表、实参里的字典照旧一行', async () => {
|
||||
const source = [
|
||||
'session.query(',
|
||||
' filters=[',
|
||||
' Filter(field="a"),',
|
||||
' other,',
|
||||
' ],',
|
||||
' ids=[1, 2],',
|
||||
' meta={"a": 1},',
|
||||
')',
|
||||
].join('\n')
|
||||
const line = (await translateAll(py, 'python', pythonZhTranslator, source, calleeKind)).get(1)
|
||||
expect(text(line?.segments)).toBe('调用 session.query · 3 个参数')
|
||||
expect(tableLines(line?.table)).toEqual([
|
||||
'filters | 列表 2 项',
|
||||
' 第 1 项 | 新建 Filter · 1 个参数',
|
||||
' field | "a"',
|
||||
' 第 2 项 | other',
|
||||
'ids | [1, 2]',
|
||||
'meta | {"a": 1}',
|
||||
])
|
||||
expect(line?.table?.items[0].items?.[0].table?.columns).toEqual(['参数', '值'])
|
||||
})
|
||||
|
||||
it('typeScript:数组写「数组 N 项」,new 是新建、别的调用是调用,整项是调用的都排开;...x 写「展开」,对象照对象拆;实参的值是跨几行的数组也拆', async () => {
|
||||
const source = [
|
||||
'const filters = [',
|
||||
' new Filter({ field: \'user_id\' }),',
|
||||
' makeFilter(',
|
||||
' \'archived_at\',',
|
||||
' archived,',
|
||||
' ),',
|
||||
' ...rest,',
|
||||
' [1, 2],',
|
||||
' {',
|
||||
' a: 1,',
|
||||
' },',
|
||||
']',
|
||||
'const ids = [a, f(b)]',
|
||||
'register(',
|
||||
' \'x\',',
|
||||
' [',
|
||||
' a,',
|
||||
' b,',
|
||||
' ],',
|
||||
')',
|
||||
].join('\n')
|
||||
const rows = await describeAll(ts, 'typescript', typescriptZhTranslator, source)
|
||||
expect(propertyLines(rows.get(1) ?? [])).toEqual([
|
||||
'名称: filters',
|
||||
'访问级别: 私有',
|
||||
'值: 数组 5 项',
|
||||
' 第 1 项 | 新建 Filter · 1 个参数',
|
||||
' 第 1 个 | { field: \'user_id\' }',
|
||||
' 第 2 项 | 调用 makeFilter · 2 个参数',
|
||||
' 第 1 个 | \'archived_at\'',
|
||||
' 第 2 个 | archived',
|
||||
' 展开 | rest',
|
||||
' 第 3 项 | [1, 2]',
|
||||
' 第 4 项 | 对象 1 项',
|
||||
' a | 1',
|
||||
])
|
||||
expect(propertyLines(rows.get(13) ?? []).at(-1)).toBe('值: [a, f(b)]')
|
||||
const line = (await translateAll(ts, 'typescript', typescriptZhTranslator, source)).get(14)
|
||||
expect(text(line?.segments)).toBe('调用 register · 2 个参数')
|
||||
expect(tableLines(line?.table)).toEqual(['第 1 个 | \'x\'', '第 2 个 | 数组 2 项', ' 第 1 项 | a', ' 第 2 项 | b'])
|
||||
})
|
||||
})
|
||||
|
||||
describe('中文模板引擎', () => {
|
||||
function testTranslator(table: Partial<ZhTable>): Translator {
|
||||
return createZhTemplateTranslator({
|
||||
@@ -1411,7 +1589,7 @@ describe('中文模板引擎', () => {
|
||||
operators: {},
|
||||
operatorParents: [],
|
||||
emptyLiterals: {},
|
||||
mapLiterals: {},
|
||||
collections: {},
|
||||
stringNodes: [],
|
||||
commentText: node => node.text,
|
||||
nodeTemplates: {},
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
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 { Access, AttributeNames, BaseFacts, BlockKind, CallArgument, CallFacts, ClassFacts, CommandContext, CommandFacts, CommandForm, CommandReading, CommandRule, CommandWord, CommonAttribute, DeclaratorFacts, Defines, DirectiveFacts, Doc, ElementFacts, 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'
|
||||
@@ -43,8 +43,8 @@ export interface ZhTable {
|
||||
operatorParents: string[]
|
||||
/** 空字面量(没有具名子节点)整个换成一个词:空数组 / 空列表 / 空字典 / 空对象。 */
|
||||
emptyLiterals: Record<string, string>
|
||||
/** 字典 / 对象字面量(语言插件给了 entries 的值)的叫法,按节点类型查:属性表里的值写成「字典 3 项」,各项展开成子行。 */
|
||||
mapLiterals: Record<string, string>
|
||||
/** 写出各项的字面量(语言插件给了各项,见 LanguageAdapter.collection)的叫法,按节点类型查:属性表里拆开的值写成「字典 3 项」「列表 2 项」。 */
|
||||
collections: Record<string, string>
|
||||
/** 整段原样照抄、按字符串着色的节点。 */
|
||||
stringNodes: string[]
|
||||
/** 注释(或 docstring)节点去掉注释符号后的原文,可以多行;text 是节点的源码去掉代码阅读的记号(SourceDocument.markers)以后剩下的。 */
|
||||
@@ -310,7 +310,7 @@ export function createZhTemplateTranslator(table: ZhTable): Translator {
|
||||
/** 节点命中模板时,模板各项换成译词或子节点;不该重排时返回 undefined。 */
|
||||
function templateParts(node: Node, span: SourceSpan): (string | Node)[] | undefined {
|
||||
const template = table.nodeTemplates[node.type]
|
||||
if (!template || node.startIndex < span.startIndex || node.endIndex > span.endIndex)
|
||||
if (!template || !inside(node, span))
|
||||
return undefined
|
||||
const parts = template.map(part => typeof part === 'string' ? part : slotChild(node, part))
|
||||
return parts.every((part): part is string | Node => part != null) ? parts : undefined
|
||||
@@ -340,7 +340,7 @@ export function createZhTemplateTranslator(table: ZhTable): Translator {
|
||||
|
||||
/** 语言插件认出的写在表达式里的遍历;没整个落在要翻的这一段里就不重排、照常翻。 */
|
||||
function iterationIn(node: Node, span: SourceSpan, language: LanguageAdapter): IterationFacts | undefined {
|
||||
if (!language.iteration || node.startIndex < span.startIndex || node.endIndex > span.endIndex)
|
||||
if (!language.iteration || !inside(node, span))
|
||||
return undefined
|
||||
return language.iteration(node)
|
||||
}
|
||||
@@ -414,26 +414,86 @@ export function createZhTemplateTranslator(table: ZhTable): Translator {
|
||||
|
||||
/** 要排成一行、实参摊成表的调用:实参列表跨了几行,整个落在要翻的这一段里,这一段还没排过别的调用。 */
|
||||
function expandedCall(node: Node, span: SourceSpan, slot: TableSlot, { language: { call } }: SourceDocument): CallFacts | undefined {
|
||||
if (slot.table || !call || node.startIndex < span.startIndex || node.endIndex > span.endIndex)
|
||||
if (slot.table || !call || !inside(node, span))
|
||||
return undefined
|
||||
const facts = call(node)
|
||||
return facts && facts.argumentList.startPosition.row !== facts.argumentList.endPosition.row ? facts : undefined
|
||||
}
|
||||
|
||||
/**
|
||||
* node 落在 span 里的那一段翻成一行译文,里面跨几行写的第一个调用排成一行「新建 / 调用 …」,实参摊成表(见 emitCall)。
|
||||
* 一行译文的块、常量 / 变量的值、实参表里每个值各翻各的,各自排自己里面的那个调用。
|
||||
* node 落在 span 里的那一段翻成一行译文,里面跨几行写的第一个调用排成一行「新建 / 调用 …」,实参摊成表(见 emitCall);
|
||||
* 给了 call(node 整个就是这次调用)时不管写成几行都这样排。一行译文的块、属性表里每一格的值各翻各的,各自排自己里面的那个调用。
|
||||
*/
|
||||
function expandedText(node: Node, span: SourceSpan, document: SourceDocument): { text: Inline[], table?: PreviewTable } {
|
||||
function expandedText(node: Node, span: SourceSpan, document: SourceDocument, call?: CallFacts): CellValue {
|
||||
const slot: TableSlot = {}
|
||||
const text = joinTokens(tokensIn(node, span, document, slot))
|
||||
return { text, ...(slot.table && { table: slot.table }) }
|
||||
const out = collector(node, span, document, slot)
|
||||
if (call)
|
||||
emitCall(out, slot, node, call, span)
|
||||
else
|
||||
collect(node, span, out)
|
||||
return { text: joinTokens(out.tokens), ...(slot.table && { table: slot.table }) }
|
||||
}
|
||||
|
||||
/**
|
||||
* 属性表里一格的值(常量 / 变量的值、字典一项的值、列表的一项、实参的值),只翻落在 span 里的部分:
|
||||
* 语言插件给了各项的字面量(LanguageAdapter.collection)写叫法和项数,各项拆成下一层子行(见 entryItem、elementItems)——
|
||||
* 字典 / 对象一直拆(maps 为假时照旧一行:实参的值),列表 / 元组 / 集合 / 数组跨几行写才拆;
|
||||
* 空字面量和别的值是一行译文,里面跨几行写的调用排成「新建 / 调用 …」,实参表跟着这一格(见 expandedText)。
|
||||
*/
|
||||
function cellValue(node: Node, span: SourceSpan, document: SourceDocument, maps = true): CellValue {
|
||||
const collection = document.language.collection?.(node)
|
||||
if (collection?.kind === 'map' && maps && collection.entries.length > 0)
|
||||
return { text: itemCount(node, collection.entries.length), items: collection.entries.map(entry => entryItem(entry, span, document)) }
|
||||
if (collection?.kind === 'sequence' && collection.elements.length > 0 && node.startPosition.row !== node.endPosition.row)
|
||||
return { text: itemCount(node, collection.elements.length), items: elementItems(collection.elements, span, document) }
|
||||
return expandedText(node, span, document)
|
||||
}
|
||||
|
||||
/** 拆开的字面量写叫法和项数(展开的也算一项):「字典 3 项」,叫法按字面量的节点类型查表(ZhTable.collections)。 */
|
||||
function itemCount(literal: Node, size: number): Segment[] {
|
||||
const name = table.collections[literal.type]
|
||||
return [hint(name ? `${name} ${size} 项` : `${size} 项`)]
|
||||
}
|
||||
|
||||
/**
|
||||
* 字典 / 对象的一项是一个子行,两列:键 | 值。键照源码写法(字符串带引号,计算属性名 `[k]` 照常翻),值见 cellValue;
|
||||
* `**x`、`...x` 的键写「展开」、值是展开的表达式,方法的值是它的头部。讲的定义是键(对象的属性名)。
|
||||
*/
|
||||
function entryItem(entry: EntryFacts, span: SourceSpan, document: SourceDocument): PropertyItem {
|
||||
const say = (node: Node, part = span): Inline[] => joinTokens(tokensIn(node, part, document))
|
||||
switch (entry.kind) {
|
||||
case 'pair': {
|
||||
const { text, ...nested } = cellValue(entry.value, span, document)
|
||||
return { cells: [say(entry.key), text], ...nested, ...defining(entry.key) }
|
||||
}
|
||||
case 'spread':
|
||||
return { cells: [[hint('展开')], say(entry.value)] }
|
||||
case 'method':
|
||||
return { cells: [say(entry.name), say(entry.node, clip(entry.header, span))], ...defining(entry.name) }
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 列表的各项各是一个子行,两列:第 N 项 | 值(只数不是展开的项),`*x`、`...x` 写「展开」、值是展开的表达式。
|
||||
* 整项是一次调用的不管写成几行都排成「新建 / 调用 …」,实参表跟着这一项,同一个列表里样子一致;别的值见 cellValue。
|
||||
*/
|
||||
function elementItems(elements: ElementFacts[], span: SourceSpan, document: SourceDocument): PropertyItem[] {
|
||||
let position = 0
|
||||
return elements.map((element): PropertyItem => {
|
||||
if (element.kind === 'spread')
|
||||
return { cells: [[hint('展开')], joinTokens(tokensIn(element.value, span, document))] }
|
||||
position++
|
||||
const { value } = element
|
||||
const call = inside(value, span) ? document.language.call?.(value) : undefined
|
||||
const { text, ...nested } = call ? expandedText(value, span, document, call) : cellValue(value, span, document)
|
||||
return { cells: [[hint(`第 ${position} 项`)], text], ...nested }
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 实参表:参数 | 值,每个实参一行,照源码顺序。按名字传的写名字,按位置传的写「第 N 个」(只数按位置传的),
|
||||
* 展开的写「展开」(*args、...args)、「展开关键字」(**kwargs)。值照常翻;值里有跨几行写的调用时同样排成一行,
|
||||
* 它的实参表排在值那一格里(PropertyItem.table),一层层往下套。
|
||||
* 展开的写「展开」(*args、...args)、「展开关键字」(**kwargs)。值见 cellValue(字典 / 对象照旧一行):值里有跨几行写的调用时
|
||||
* 同样排成一行,它的实参表排在值那一格里(PropertyItem.table);值是跨几行写的列表时各项拆在那一格里(PropertyItem.items)。一层层往下套。
|
||||
*/
|
||||
function argumentTable(facts: CallFacts, span: SourceSpan, document: SourceDocument): PreviewTable {
|
||||
let position = 0
|
||||
@@ -451,8 +511,8 @@ export function createZhTemplateTranslator(table: ZhTable): Translator {
|
||||
}
|
||||
}
|
||||
const row = (arg: CallArgument): PropertyItem => {
|
||||
const { text, table } = expandedText(arg.value, span, document)
|
||||
return { cells: [label(arg), text], ...(table && { table }) }
|
||||
const { text, ...nested } = cellValue(arg.value, span, document, false)
|
||||
return { cells: [label(arg), text], ...nested }
|
||||
}
|
||||
return { columns: ARGUMENT_COLUMNS, items: facts.arguments.map(row) }
|
||||
}
|
||||
@@ -492,7 +552,7 @@ export function createZhTemplateTranslator(table: ZhTable): Translator {
|
||||
return {
|
||||
document,
|
||||
say: node => joinTokens(tokensIn(node, span, document)),
|
||||
whole: node => node.startIndex >= span.startIndex && node.endIndex <= span.endIndex,
|
||||
whole: node => inside(node, span),
|
||||
name: (node, role) => segment(node.text, role ?? document.language.tokenRole(node), node.startIndex),
|
||||
command: (node) => {
|
||||
const facts = document.language.command?.(node)
|
||||
@@ -605,9 +665,14 @@ export function createZhTemplateTranslator(table: ZhTable): Translator {
|
||||
}
|
||||
}
|
||||
|
||||
/** 按译文顺序收集 node 落在 span 里的词;给了 slot,跨几行写的调用排成一行、实参摊成表(见 emitCall)。 */
|
||||
/** 翻 node 落在 span 里的那一段用的收集器;给了 slot,跨几行写的调用排成一行、实参摊成表(见 emitCall)。 */
|
||||
function collector(node: Node, span: SourceSpan, document: SourceDocument, slot?: TableSlot): Collector {
|
||||
return { document, root: node, tokens: [], cursor: span.startIndex, gap: false, ...(slot && { slot }) }
|
||||
}
|
||||
|
||||
/** 按译文顺序收集 node 落在 span 里的词;slot 见 collector。 */
|
||||
function tokensIn(node: Node, span: SourceSpan, document: SourceDocument, slot?: TableSlot): Token[] {
|
||||
const out: Collector = { document, root: node, tokens: [], cursor: span.startIndex, gap: false, ...(slot && { slot }) }
|
||||
const out = collector(node, span, document, slot)
|
||||
collect(node, span, out)
|
||||
return out.tokens
|
||||
}
|
||||
@@ -652,10 +717,8 @@ export function createZhTemplateTranslator(table: ZhTable): Translator {
|
||||
if (block.kind === 'comment')
|
||||
return { text: translateComment(block, document) }
|
||||
const group = block.group && table.groups?.[block.node.type]
|
||||
if (block.group && group) {
|
||||
const out: Collector = { document, root: block.node, tokens: [], cursor: block.header.startIndex, gap: false }
|
||||
return group(block.group, readerContext(out, block.header))
|
||||
}
|
||||
if (block.group && group)
|
||||
return group(block.group, readerContext(collector(block.node, block.header, document), block.header))
|
||||
const slot: TableSlot = {}
|
||||
const tokens = tokensIn(block.node, block.header, document, slot)
|
||||
const compound = block.children.length > 0 || block.header.endIndex < block.node.endIndex
|
||||
@@ -677,22 +740,15 @@ export function createZhTemplateTranslator(table: ZhTable): Translator {
|
||||
function describe(block: FactBlock, document: SourceDocument): PropertyRow[] {
|
||||
const { header } = block
|
||||
const { language } = document
|
||||
// 值里带语句块主体的回调(useMemo(() => { … }))只翻到主体开始之前,和块头部一致
|
||||
const clipped = (part: SourceSpan): SourceSpan => part.endIndex > header.endIndex ? { ...part, endIndex: header.endIndex, endRow: header.endRow } : part
|
||||
const within = (node: Node, part: SourceSpan): Inline[] => joinTokens(tokensIn(node, clipped(part), document))
|
||||
const clipped = (part: SourceSpan): SourceSpan => clip(part, header)
|
||||
const phrases: Phrases = {
|
||||
translated: node => within(node, nodeSpan(node)),
|
||||
within,
|
||||
expanded: node => expandedText(node, clipped(nodeSpan(node)), document),
|
||||
translated: node => joinTokens(tokensIn(node, clipped(nodeSpan(node)), document)),
|
||||
value: node => cellValue(node, clipped(nodeSpan(node)), document),
|
||||
body: node => expression(node, document),
|
||||
returned: (node) => {
|
||||
const [value] = language.command?.(node)?.arguments ?? []
|
||||
return value ? expression(value.node, document) : []
|
||||
},
|
||||
entryCount: (literal, size) => {
|
||||
const name = table.mapLiterals[literal.type]
|
||||
return [hint(name ? `${name} ${size} 项` : `${size} 项`)]
|
||||
},
|
||||
verbatim: node => verbatim(node, document),
|
||||
// 点分模块名逐节照抄,每一节各自能跳到定义,和 VS Code 里一样
|
||||
module: node => stringNodes.has(node.type) ? [segment(node.text, 'string', offsetOf(node, language))] : verbatim(node, document, 'type'),
|
||||
@@ -756,6 +812,16 @@ function nodeSpan(node: Node): SourceSpan {
|
||||
return { startIndex: node.startIndex, endIndex: node.endIndex, startRow: node.startPosition.row, endRow: node.endPosition.row }
|
||||
}
|
||||
|
||||
/** node 整个落在 span 里。 */
|
||||
function inside(node: Node, span: SourceSpan): boolean {
|
||||
return node.startIndex >= span.startIndex && node.endIndex <= span.endIndex
|
||||
}
|
||||
|
||||
/** part 超出 limit 结尾的部分截掉:值里带语句块主体的回调(useMemo(() => { … }))只翻到主体开始之前,和块头部一致。 */
|
||||
function clip(part: SourceSpan, limit: SourceSpan): SourceSpan {
|
||||
return part.endIndex > limit.endIndex ? { ...part, endIndex: limit.endIndex, endRow: limit.endRow } : part
|
||||
}
|
||||
|
||||
function segment(text: string, role: TokenRole, offset: number | undefined): Segment {
|
||||
return offset === undefined ? { text, role } : { text, role, offset }
|
||||
}
|
||||
@@ -805,20 +871,23 @@ function joinTokens(tokens: Token[]): Inline[] {
|
||||
|
||||
// ─── 属性表 ───────────────────────────────────────────────────────────────────
|
||||
|
||||
/** 属性表里一格的值:一行译文,带下一层子行(拆开的字面量的各项)或那次调用的实参表。 */
|
||||
interface CellValue {
|
||||
text: Inline[]
|
||||
items?: PropertyItem[]
|
||||
table?: PreviewTable
|
||||
}
|
||||
|
||||
/** 属性表里的值怎么写。 */
|
||||
interface Phrases {
|
||||
/** 按表翻译,只翻落在块头部里的那一段。 */
|
||||
translated: (node: Node) => Inline[]
|
||||
/** 同 translated,只翻 node 落在 span 里的那一段(对象字面量里方法的头部)。 */
|
||||
within: (node: Node, span: SourceSpan) => Inline[]
|
||||
/** 同 translated,里面跨几行写的第一个调用排成一行「新建 / 调用 …」,实参摊成表(见 emitCall)。 */
|
||||
expanded: (node: Node) => { text: Inline[], table?: PreviewTable }
|
||||
/** 一格里的值,只翻落在块头部里的那一段:字面量的各项拆成下一层子行,跨几行写的调用排成「新建 / 调用 …」带实参表(见 cellValue)。 */
|
||||
value: (node: Node) => CellValue
|
||||
/** 函数主体里的一句(不在块头部里)整句照常翻:函数打印的那几句。 */
|
||||
body: (node: Node) => Inline[]
|
||||
/** 一句 return 返回的值(语言插件读出的命令的第一个词);只写了 return 时是空的。 */
|
||||
returned: (node: Node) => Inline[]
|
||||
/** 字典 / 对象字面量的项数:「字典 3 项」,叫法按字面量的节点类型查表。 */
|
||||
entryCount: (literal: Node, size: number) => Segment[]
|
||||
/** 源码原样照抄、按源码着色(装饰器)。 */
|
||||
verbatim: (node: Node) => Segment[]
|
||||
/** 导入来源:字符串字面量按字符串着色,模块名按类型着色。 */
|
||||
@@ -1271,44 +1340,16 @@ function variableRows(block: VariableBlock, say: Phrases): PropertyRow[] {
|
||||
...(i === 0 ? docBodyRows(block.facts.doc && say.doc(block.facts.doc)) : []),
|
||||
...accessRows(block.facts.access),
|
||||
...(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, say) : { value: [hint('无')] }) },
|
||||
])
|
||||
}
|
||||
|
||||
/**
|
||||
* 声明的值:字典 / 对象字面量见 literalValue;别的值是一行译文,里面跨几行写的调用排成「新建 / 调用 …」,
|
||||
* 实参摊成表(参数 | 值)展开在这一行下面。
|
||||
* 声明的值(见 Phrases.value):拆开的字面量的各项、跨几行写的调用的实参表(参数 | 值,带列名)都展开在这一行下面。
|
||||
*/
|
||||
function declaredValue(value: Node, entries: EntryFacts[] | undefined, say: Phrases): Pick<PropertyRow, 'value' | 'items' | 'columns'> {
|
||||
if (entries?.length)
|
||||
return literalValue(value, entries, say)
|
||||
const { text, table } = say.expanded(value)
|
||||
return table ? { value: text, ...table } : { value: text }
|
||||
}
|
||||
|
||||
/**
|
||||
* 值:字典 / 对象字面量(语言插件给了 entries)写项数,各项展开成下一层子行;
|
||||
* 空字面量和别的值是一行译文(空的译成「空字典」「空对象」)。
|
||||
*/
|
||||
function literalValue(value: Node, entries: EntryFacts[] | undefined, say: Phrases): { value: Inline[], items?: PropertyItem[] } {
|
||||
if (!entries?.length)
|
||||
return { value: say.translated(value) }
|
||||
return { value: say.entryCount(value, entries.length), items: entries.map(entry => entryItem(entry, say)) }
|
||||
}
|
||||
|
||||
/**
|
||||
* 字典 / 对象字面量的一项是一个子行,两列:键 | 值。键照源码写法(字符串带引号,计算属性名 `[k]` 照常翻),
|
||||
* `**x`、`...x` 的键写「展开」、值是展开的表达式,方法的值是它的头部。讲的定义是键(对象的属性名)。
|
||||
*/
|
||||
function entryItem(entry: EntryFacts, say: Phrases): PropertyItem {
|
||||
switch (entry.kind) {
|
||||
case 'pair': {
|
||||
const { value, ...nested } = literalValue(entry.value, entry.entries, say)
|
||||
return { cells: [say.translated(entry.key), value], ...nested, ...defining(entry.key) }
|
||||
}
|
||||
case 'spread':
|
||||
return { cells: [[hint('展开')], say.translated(entry.value)] }
|
||||
case 'method':
|
||||
return { cells: [say.translated(entry.name), say.within(entry.node, entry.header)], ...defining(entry.name) }
|
||||
}
|
||||
function declaredValue(value: Node, say: Phrases): Pick<PropertyRow, 'value' | 'items' | 'columns'> {
|
||||
const { text, items, table } = say.value(value)
|
||||
if (table)
|
||||
return { value: text, ...table }
|
||||
return items ? { value: text, items } : { value: text }
|
||||
}
|
||||
@@ -244,7 +244,7 @@ function BlockPreview({ block, tree, marks }: { block: BlockView, tree: BlockTre
|
||||
{marks?.translation ? <DiffText diff={marks.translation} /> : <TextPreview text={block.translation} />}
|
||||
{block.table && (
|
||||
<div className="mt-[3px]">
|
||||
<PropertyTable parentId={tableId(block.id)} columns={block.table.columns} items={block.table.items} marks={marks?.table} />
|
||||
<PropertyTable parentId={tableId(block.id)} columns={block.table.columns} items={block.table.items} marks={marks?.table} folding={{ collapsed: tree.collapsed, onToggleFold: tree.onToggleFold }} />
|
||||
</div>
|
||||
)}
|
||||
{marks?.doc ? <DiffText diff={marks.doc} className="mt-0.5" /> : block.doc && <TextPreview text={block.doc} className="mt-0.5" />}
|
||||
|
||||
@@ -0,0 +1,125 @@
|
||||
// @vitest-environment happy-dom
|
||||
|
||||
import type { Root } from 'react-dom/client'
|
||||
import type { BlockKind, CallFacts } from '../../../src/codeReader/contract'
|
||||
import type { PropertyRow } from './types'
|
||||
import { act } from 'react'
|
||||
import { createRoot } from 'react-dom/client'
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { toBlockViews } from '../../../src/codeReader/blockView'
|
||||
import { createPythonAdapter } from '../../../src/codeReader/languages/python'
|
||||
import { WASM_DIR, withBlocks } from '../../../src/codeReader/testSupport'
|
||||
import { pythonZhTranslator } from '../../../src/codeReader/translators/pythonZh'
|
||||
import { PropertyGrid } from './PropertyGrid'
|
||||
import { propertyRowId } from './viewState'
|
||||
|
||||
const py = createPythonAdapter(WASM_DIR)
|
||||
|
||||
let container: HTMLDivElement
|
||||
let root: Root
|
||||
|
||||
beforeEach(() => {
|
||||
vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true)
|
||||
container = document.createElement('div')
|
||||
document.body.append(container)
|
||||
root = createRoot(container)
|
||||
})
|
||||
|
||||
afterEach(async () => {
|
||||
await act(async () => root.unmount())
|
||||
container.remove()
|
||||
vi.unstubAllGlobals()
|
||||
})
|
||||
|
||||
/** Filter 的定义是类。 */
|
||||
const calleeKind = (call: CallFacts): BlockKind | undefined => call.callee.text === 'Filter' ? 'class' : undefined
|
||||
|
||||
/** 一段 Python 里第一个变量块的属性表(经过宿主转成视图数据)。 */
|
||||
async function variableRows(source: string): Promise<PropertyRow[]> {
|
||||
const views = await withBlocks(py, 'python', source, blocks => toBlockViews(blocks, { source, language: py, markers: [], calleeKind }, pythonZhTranslator))
|
||||
const variable = views.find(view => view.kind === 'variable')
|
||||
if (!variable || !('properties' in variable))
|
||||
throw new Error('没有变量块')
|
||||
return variable.properties
|
||||
}
|
||||
|
||||
async function render(rows: PropertyRow[], collapsed: ReadonlySet<string> = new Set(), onToggleFold: (id: string) => void = () => {}): Promise<void> {
|
||||
await act(async () => root.render(<PropertyGrid blockId="0" rows={rows} collapsed={collapsed} onToggleFold={onToggleFold} />))
|
||||
}
|
||||
|
||||
function byId(id: string): HTMLElement {
|
||||
const found = container.querySelector<HTMLElement>(`[data-id="${id}"]`)
|
||||
if (!found)
|
||||
throw new Error(`页面上没有 ${id}`)
|
||||
return found
|
||||
}
|
||||
|
||||
/** a 在页面上排在 b 前面。 */
|
||||
function before(a: Element, b: Element): boolean {
|
||||
return (a.compareDocumentPosition(b) & Node.DOCUMENT_POSITION_FOLLOWING) !== 0
|
||||
}
|
||||
|
||||
describe('拆开的列表', () => {
|
||||
it('列表的项下面挂着它的实参表:表排在这一项下面、下一项上面,往右缩一层、左边一条竖线,带表头', async () => {
|
||||
const rows = await variableRows([
|
||||
'filters = [',
|
||||
' Filter(field="user_id", value=current_user.id),',
|
||||
' Filter(',
|
||||
' field="archived_at",',
|
||||
' value=None,',
|
||||
' ),',
|
||||
']',
|
||||
].join('\n'))
|
||||
await render(rows)
|
||||
const row = propertyRowId('0', rows.findIndex(item => item.label === '初始值'))
|
||||
const first = byId(`${row}/0`)
|
||||
const second = byId(`${row}/1`)
|
||||
expect(first.textContent).toBe('第 1 项新建 Filter · 2 个参数')
|
||||
expect(second.textContent).toBe('第 2 项新建 Filter · 2 个参数')
|
||||
const cell = byId(`${row}/0/t/0`)
|
||||
expect(cell.textContent).toBe('field"user_id"')
|
||||
expect(byId(`${row}/1/t/1`).textContent).toBe('value空')
|
||||
expect(before(first, cell) && before(cell, second)).toBe(true)
|
||||
const table = cell.closest('.cr-table')
|
||||
expect([...table?.querySelectorAll('[role="columnheader"]') ?? []].map(head => head.textContent)).toEqual(['参数', '值'])
|
||||
// 表不在项那一行里,在它下面那一层:和下一层子行一样左边一条竖线
|
||||
expect(first.contains(table)).toBe(false)
|
||||
let level = table?.parentElement
|
||||
while (level && !level.firstElementChild?.classList.contains('cr-guide'))
|
||||
level = level.parentElement
|
||||
expect(level?.contains(cell)).toBe(true)
|
||||
expect(level?.contains(first)).toBe(false)
|
||||
})
|
||||
|
||||
it('实参表的格子里拆开的列表:排在那一格里、文字下面;项下面挂着自己的实参表,字典项的下一层能折叠', async () => {
|
||||
const rows = await variableRows([
|
||||
'result = session.query(',
|
||||
' filters=[',
|
||||
' Filter(field="a"),',
|
||||
' {"k": v},',
|
||||
' ],',
|
||||
')',
|
||||
].join('\n'))
|
||||
const onToggleFold = vi.fn()
|
||||
await render(rows, new Set(), onToggleFold)
|
||||
const row = propertyRowId('0', rows.findIndex(item => item.label === '初始值'))
|
||||
const argument = byId(`${row}/0`)
|
||||
const cells = argument.querySelectorAll(':scope > [role="cell"]')
|
||||
const valueCell = cells[cells.length - 1]
|
||||
expect(valueCell.firstElementChild?.textContent).toBe('列表 2 项')
|
||||
const call = byId(`${row}/0/0`)
|
||||
const map = byId(`${row}/0/1`)
|
||||
expect(valueCell.contains(call) && valueCell.contains(map)).toBe(true)
|
||||
expect(call.textContent).toBe('第 1 项新建 Filter · 1 个参数')
|
||||
expect(byId(`${row}/0/0/t/0`).textContent).toBe('field"a"')
|
||||
expect(byId(`${row}/0/1/0`).textContent).toBe('"k"v')
|
||||
const fold = map.querySelector('button')
|
||||
expect(fold?.getAttribute('aria-expanded')).toBe('true')
|
||||
await act(async () => fold?.click())
|
||||
expect(onToggleFold).toHaveBeenCalledWith(`${row}/0/1`)
|
||||
|
||||
await render(rows, new Set([`${row}/0/1`]))
|
||||
expect(container.querySelector(`[data-id="${row}/0/1/0"]`)).toBeNull()
|
||||
expect(container.querySelector(`[data-id="${row}/0/0/t/0"]`)).not.toBeNull()
|
||||
})
|
||||
})
|
||||
@@ -122,9 +122,20 @@ function ItemHead({ id, cell, foldable, folding }: { id: string, cell: PreviewTe
|
||||
)
|
||||
}
|
||||
|
||||
/** 往右缩进一层、左边一条竖线:一行或一个子行下面的一层子行,列表的一项下面挂的实参表 */
|
||||
function Nested({ children }: { children: ReactNode }): ReactElement {
|
||||
return (
|
||||
<div className="flex gap-2.5 pl-[5px]">
|
||||
<div className="cr-guide w-px shrink-0" />
|
||||
<div className="flex min-w-0 flex-1 flex-col gap-0.5">{children}</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* 展开在一行或一个子行(parentId)下面的一层子行:左边一条竖线,每个子行几列对齐。
|
||||
* 带下一层的子行(字典 / 对象的值又是字典 / 对象),下一层展开在它下面、再往右缩进一层,默认展开。
|
||||
* 带下一层的子行(字典 / 对象、跨几行写的列表的各项),下一层展开在它下面、再往右缩进一层,默认展开,能折叠;
|
||||
* 带实参表的子行(列表里整项是调用),表排在它下面、同样往右缩进一层,总是展开。
|
||||
* 标差异时(marks)新加的子行淡绿底、左边 +,删掉的放回原来的位置、淡红底划掉、左边 −,改了的格子逐截标。
|
||||
*/
|
||||
function PropertyItems({ parentId, items, folding, marks }: { parentId: string, items: PropertyItem[], folding: Folding, marks?: ItemsDiff }): ReactElement {
|
||||
@@ -133,6 +144,17 @@ function PropertyItems({ parentId, items, folding, marks }: { parentId: string,
|
||||
const itemRow = (item: PropertyItem, id: string, change: 'add' | 'del' | undefined, mark: ItemDiff | undefined): ReactElement => {
|
||||
const children = item.items ?? []
|
||||
const folds = change === 'del' ? FIXED : folding
|
||||
const open = children.length > 0 && !folds.collapsed.has(id)
|
||||
const below = (
|
||||
<>
|
||||
{open && <PropertyItems parentId={id} items={children} folding={folds} marks={mark?.items} />}
|
||||
{item.table && (
|
||||
<Nested>
|
||||
<PropertyTable parentId={tableId(id)} columns={item.table.columns} items={item.table.items} marks={mark?.table} folding={folds} />
|
||||
</Nested>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
@@ -145,49 +167,43 @@ function PropertyItems({ parentId, items, folding, marks }: { parentId: string,
|
||||
? <ItemHead key={i} id={id} cell={cell} foldable={children.length > 0 && change !== 'del'} folding={folds} />
|
||||
: <CellBox key={i} cell={cell} parts={mark?.cells?.[i]} />)}
|
||||
</div>
|
||||
{children.length > 0 && !folds.collapsed.has(id) && (change === 'del'
|
||||
? (
|
||||
<div className="cr-diff-gone">
|
||||
<PropertyItems parentId={id} items={children} folding={folds} />
|
||||
</div>
|
||||
)
|
||||
: <PropertyItems parentId={id} items={children} folding={folds} marks={mark?.items} />)}
|
||||
{(open || item.table) && (change === 'del' ? <div className="cr-diff-gone">{below}</div> : below)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
const removed = (after: number): ReactElement[] => removedAt(marks?.removed, after)
|
||||
.map(({ value, index }) => <Fragment key={`removed-${index}`}>{itemRow(value, `${parentId}/x${index}`, 'del', undefined)}</Fragment>)
|
||||
return (
|
||||
<div className="flex gap-2.5 pl-[5px]">
|
||||
<div className="cr-guide w-px shrink-0" />
|
||||
<div className="flex min-w-0 flex-1 flex-col gap-0.5">
|
||||
{removed(-1)}
|
||||
{items.map((item, index) => {
|
||||
const mark = marks?.changed[index]
|
||||
return (
|
||||
<Fragment key={index}>
|
||||
{itemRow(item, propertyItemId(parentId, index), mark?.added && 'add', mark)}
|
||||
{removed(index)}
|
||||
</Fragment>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
<Nested>
|
||||
{removed(-1)}
|
||||
{items.map((item, index) => {
|
||||
const mark = marks?.changed[index]
|
||||
return (
|
||||
<Fragment key={index}>
|
||||
{itemRow(item, propertyItemId(parentId, index), mark?.added && 'add', mark)}
|
||||
{removed(index)}
|
||||
</Fragment>
|
||||
)
|
||||
})}
|
||||
</Nested>
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* 给了列名的子行(输入参数、数据表的字段)排成表格:第一行是表头(列名),下面每个子行一行;外框圆角,格子之间、行之间一条分隔线。
|
||||
* 列宽按整列的内容定(见 tableTemplate),最后一列分剩下的,长了在格子里换行。一行译文下面摊开实参的表也用它;
|
||||
* 行带 table(实参的值里跨几行写的调用)时,那张表排在最后一格里、文字下面,往右缩一点,一层层往下套。
|
||||
* 行带 items(实参的值是跨几行写的列表)时各项排在最后一格里、文字下面,同属性行下面的子行(项下面的下一层按 folding 折叠);
|
||||
* 行带 table(实参的值里跨几行写的调用)时那张表排在它们后面,往右缩一点。一层层往下套。
|
||||
* 标差异时(marks)有加行、删行的表每行第一格前多一个记号位(+ / −,没变的空着),加的行淡绿底,删的行放回原来的位置、淡红底划掉。
|
||||
* 讲某个子块的行(toggles,Rust 实现块的方法表)第一格是按钮,点它在表下面展开那一块,展开时这一行淡淡垫上那一块的种类色。
|
||||
*/
|
||||
export function PropertyTable({ parentId, columns, items, marks, toggles }: {
|
||||
export function PropertyTable({ parentId, columns, items, marks, folding, toggles }: {
|
||||
parentId: string
|
||||
columns: string[]
|
||||
items: PropertyItem[]
|
||||
marks?: ItemsDiff
|
||||
/** 格子里拆开的子行下面的下一层眼下折叠着没有 */
|
||||
folding: Folding
|
||||
/** 按子行下标:讲它的子块(点第一格展开,见 GridSlots) */
|
||||
toggles?: ReadonlyMap<number, ItemToggle>
|
||||
}): ReactElement {
|
||||
@@ -196,6 +212,7 @@ export function PropertyTable({ parentId, columns, items, marks, toggles }: {
|
||||
/** 一行:删掉的(change 是 del)不带 id,套着的表不再标;toggle 是这一行讲的子块,第一格成了展开它的按钮 */
|
||||
const tableRow = (item: PropertyItem, id: string, change: 'add' | 'del' | undefined, mark: ItemDiff | undefined, toggle?: ItemToggle): ReactElement => {
|
||||
const cells = tableCells(item, columns)
|
||||
const listed = (item.items?.length ?? 0) > 0
|
||||
const sign = (i: number): ReactNode => signed && i === 0 && <span className="cr-diff-sign" data-change={change} aria-hidden>{SIGNS[change ?? 'none']}</span>
|
||||
/** 一格:讲子块的那一格是展开它的按钮(只放着色的词),别的照常(见 CellBox) */
|
||||
const cellBox = (cell: PreviewText, i: number, className?: string): ReactElement => toggle && i === 0
|
||||
@@ -213,11 +230,12 @@ export function PropertyTable({ parentId, columns, items, marks, toggles }: {
|
||||
: {}
|
||||
return (
|
||||
<div className="cr-table-row" role="row" data-id={change === 'del' ? undefined : id} data-diff={change} data-open={toggle?.open ? '' : undefined} {...tint}>
|
||||
{cells.map((cell, i) => item.table && i === cells.length - 1
|
||||
{cells.map((cell, i) => (listed || item.table) && i === cells.length - 1
|
||||
? (
|
||||
<div key={i} className="cr-table-cell flex min-w-0 flex-col gap-[3px]" role="cell">
|
||||
<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} />
|
||||
{listed && <PropertyItems parentId={id} items={item.items ?? []} folding={change === 'del' ? FIXED : folding} marks={mark?.items} />}
|
||||
{item.table && <PropertyTable parentId={tableId(id)} columns={item.table.columns} items={item.table.items} marks={mark?.table} folding={change === 'del' ? FIXED : folding} />}
|
||||
</div>
|
||||
)
|
||||
: <Fragment key={i}>{cellBox(cell, i, 'cr-table-cell')}</Fragment>)}
|
||||
@@ -262,6 +280,7 @@ export function PropertyGrid({ blockId, rows, collapsed, onToggleFold, marks, sl
|
||||
/** 挂在这张属性表子行上的子块(库规则的卡片),见 GridSlots */
|
||||
slots?: GridSlots
|
||||
}): ReactElement {
|
||||
const folding: Folding = { collapsed, onToggleFold }
|
||||
const removed = (after: number): ReactElement[] => removedAt(marks?.removed, after)
|
||||
.map(({ value, index }) => <RemovedRow key={`removed-${index}`} id={`${blockId}/x${index}`} row={value} />)
|
||||
return (
|
||||
@@ -298,8 +317,8 @@ export function PropertyGrid({ blockId, rows, collapsed, onToggleFold, marks, sl
|
||||
: <span className="cr-prop-label flex h-5 items-center">{row.label}</span>}
|
||||
<RowValue row={row} mark={mark} />
|
||||
</div>
|
||||
{!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 === 'table' && <PropertyTable parentId={id} columns={row.columns ?? []} items={row.items ?? []} marks={mark?.items} folding={folding} toggles={toggles} />}
|
||||
{!folded && layout === 'list' && <PropertyItems parentId={id} items={row.items ?? []} folding={folding} marks={mark?.items} />}
|
||||
{!folded && slots && shownBlocks.length > 0 && <div className="cr-slot">{slots.render(shownBlocks)}</div>}
|
||||
{removed(index)}
|
||||
</Fragment>
|
||||
@@ -337,7 +356,7 @@ function RemovedRow({ id, row }: { id: string, row: PropertyRow }): ReactElement
|
||||
{layout && (
|
||||
<div className="cr-diff-gone">
|
||||
{layout === 'table'
|
||||
? <PropertyTable parentId={id} columns={row.columns ?? []} items={row.items ?? []} />
|
||||
? <PropertyTable parentId={id} columns={row.columns ?? []} items={row.items ?? []} folding={FIXED} />
|
||||
: <PropertyItems parentId={id} items={row.items ?? []} folding={FIXED} />}
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -116,6 +116,24 @@ describe('跳到定义:滚到哪块、展开哪些', () => {
|
||||
expect(occurrencePlan(blocks, { start: 70, end: 83, kind: 'read' }, id => id === '0')).toEqual({ id: '1', expand: ['1/p0'] })
|
||||
})
|
||||
|
||||
it('名字在拆开的列表里:列表项下面的实参表总是展开的;实参表格子里拆开的列表总是展开的,它的项下面的子行要展开那一项', () => {
|
||||
const filter = (at: number): PropertyItem => ({ cells: [[word('第 1 项')], [word('新建'), name('Filter', at)]], table: { columns: ['参数', '值'], items: [{ cells: [[name('field', at + 10)], [name('user_id', at + 20)]] }] } })
|
||||
const map = (at: number): PropertyItem => ({ cells: [[word('第 2 项')], [word('字典 1 项')]], items: [{ cells: [[name('key', at)], [name('value', at + 5)]], defines: { startIndex: at, endIndex: at + 3 } }] })
|
||||
const blocks: BlockView[] = [
|
||||
factBlock('0', 'variable', 0, [{ label: '初始值', value: [word('列表 2 项')], items: [filter(10), map(40)] }]),
|
||||
{ ...textBlock('1', 'statement', 5, [word('调用'), name('query', 100)]), table: { columns: ['参数', '值'], items: [{ cells: [[name('filters', 110)], [word('列表 2 项')]], items: [filter(120), map(160)] }] } },
|
||||
factBlock('2', 'variable', 9, [{ label: '初始值', value: [word('调用'), name('query', 200)], columns: ['参数', '值'], items: [{ cells: [[name('filters', 210)], [word('列表 2 项')]], items: [filter(220), map(260)] }] }]),
|
||||
]
|
||||
expect(revealPlan(blocks, 0, 30)).toEqual({ id: '0', expand: ['0/p0'] })
|
||||
expect(revealPlan(blocks, 0, 40)).toEqual({ id: '0', row: '0/p0/1/0', expand: ['0/p0', '0/p0/1'] })
|
||||
expect(revealPlan(blocks, 5, 140)).toEqual({ id: '1', expand: [] })
|
||||
expect(revealPlan(blocks, 5, 165)).toEqual({ id: '1', expand: ['1/t/0/1'] })
|
||||
expect(revealPlan(blocks, 9, 240)).toEqual({ id: '2', expand: ['2/p0'] })
|
||||
expect(revealPlan(blocks, 9, 260)).toEqual({ id: '2', row: '2/p0/0/1/0', expand: ['2/p0', '2/p0/0/1'] })
|
||||
expect(occurrencePlan(blocks, { start: 165, end: 170, kind: 'read' }, () => false)).toEqual({ id: '1', expand: ['1/t/0/1'] })
|
||||
expect(occurrencePlan(blocks, { start: 140, end: 147, kind: 'read' }, () => false)).toEqual({ id: '1', expand: [] })
|
||||
})
|
||||
|
||||
it('名字在框里(套了几层都算)也是显示着它', () => {
|
||||
const inner = { style: 'iteration' as const, label: '列表', segments: [word('遍历'), name('row', 30), word('取'), name('y', 40)] }
|
||||
const blocks: BlockView[] = [
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { BlockView, Inline, Occurrence, PreviewTable, PreviewText, PropertyItem, SourceRange } from './types'
|
||||
import type { BlockView, Inline, Occurrence, PreviewTable, PreviewText, PropertyItem, PropertyRow, SourceRange } from './types'
|
||||
import { blockPath } from './filter'
|
||||
import { showsOccurrence, sourceOf } from './occurrences'
|
||||
import { propertyItemId, propertyRowId, unfoldToShow } from './viewState'
|
||||
import { propertyItemId, propertyRowId, tableId, unfoldToShow } from './viewState'
|
||||
|
||||
/** 跳到定义时要滚到的块,就是这处定义的属性行 / 子行(只闪它),以及要展开的块、属性行和子行:它们折叠着,目标就不在页面上。 */
|
||||
export interface RevealPlan {
|
||||
@@ -62,24 +62,34 @@ function width(range: SourceRange): number {
|
||||
return range.endIndex - range.startIndex
|
||||
}
|
||||
|
||||
/** 实参表(一行译文下面那张、值那一格里套着的,都总是展开的)里有格子盖住 offset 时,那段有多长;套着的表一层层都算。 */
|
||||
function tableSpan(table: PreviewTable | undefined, offset: number): number | undefined {
|
||||
for (const item of table?.items ?? []) {
|
||||
for (const cell of item.cells) {
|
||||
const span = textSpan(cell, offset)
|
||||
if (span !== undefined)
|
||||
return span
|
||||
}
|
||||
const nested = tableSpan(item.table, offset)
|
||||
if (nested !== undefined)
|
||||
return nested
|
||||
}
|
||||
return undefined
|
||||
/** 页面上的一个子行:它的 id,和要让它露出来得展开的属性行、子行(从外往里)。 */
|
||||
interface PlacedItem {
|
||||
item: PropertyItem
|
||||
id: string
|
||||
expand: string[]
|
||||
}
|
||||
|
||||
/** 实参表(一行译文下面那张、值那一格里套着的,都总是展开的)里有格子满足 test,套着的表一层层都算。 */
|
||||
function tableShows(table: PreviewTable | undefined, test: (cell: PreviewText) => boolean): boolean {
|
||||
return table?.items.some(item => item.cells.some(test) || tableShows(item.table, test)) ?? false
|
||||
/**
|
||||
* parentId 下面一层层的子行,按页面上的先后(子行、它下一层的子行、它的实参表);expand 是让这一层露出来得展开的。
|
||||
* 排成列表的子行(listed)下面的下一层折叠在它自己的 id 上;表格的行格子里拆开的子行、实参表(id 见 tableId)总是展开的。
|
||||
*/
|
||||
function* nestedItems(items: PropertyItem[] | undefined, parentId: string, expand: string[], listed: boolean): Generator<PlacedItem> {
|
||||
for (const [index, item] of (items ?? []).entries()) {
|
||||
const id = propertyItemId(parentId, index)
|
||||
yield { item, id, expand }
|
||||
yield* nestedItems(item.items, id, listed ? [...expand, id] : expand, true)
|
||||
yield* nestedItems(item.table?.items, tableId(id), expand, false)
|
||||
}
|
||||
}
|
||||
|
||||
/** 属性行下面一层层的子行:子行折叠在属性行上,给了列名的是表格的行。 */
|
||||
function rowItems(row: PropertyRow, id: string): Generator<PlacedItem> {
|
||||
return nestedItems(row.items, id, [id], !row.columns)
|
||||
}
|
||||
|
||||
/** 一行译文下面那张实参表(总是展开的)里一层层的子行。 */
|
||||
function tableItems(blockId: string, table: PreviewTable | undefined): Generator<PlacedItem> {
|
||||
return nestedItems(table?.items, tableId(blockId), [], false)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -94,71 +104,46 @@ function locate(block: BlockView, offset: number): Spot | undefined {
|
||||
if (block.defines && covers(block.defines, offset))
|
||||
return { expand: [], span: width(block.defines) }
|
||||
if ('translation' in block) {
|
||||
const span = textSpan(block.translation, offset) ?? tableSpan(block.table, offset)
|
||||
return span === undefined ? undefined : { expand: [], span }
|
||||
const span = textSpan(block.translation, offset)
|
||||
return span === undefined ? shownItem(tableItems(block.id, block.table), offset) : { expand: [], span }
|
||||
}
|
||||
const rows = block.properties.map((row, index) => ({ row, id: propertyRowId(block.id, index) }))
|
||||
for (const { row, id } of rows) {
|
||||
if (row.defines && covers(row.defines, offset))
|
||||
return { row: id, expand: [], span: width(row.defines) }
|
||||
const item = definingItem(row.items, id, offset)
|
||||
if (item)
|
||||
return item
|
||||
for (const { item, id: itemId, expand } of rowItems(row, id)) {
|
||||
if (item.defines && covers(item.defines, offset))
|
||||
return { row: itemId, expand, span: width(item.defines) }
|
||||
}
|
||||
}
|
||||
for (const { row, id } of rows) {
|
||||
const value = textSpan(row.value, offset)
|
||||
if (value !== undefined)
|
||||
return { expand: [], span: value }
|
||||
const shown = shownItem(row.items, id, offset)
|
||||
const shown = shownItem(rowItems(row, id), offset)
|
||||
if (shown)
|
||||
return shown
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
|
||||
/** parentId 下面一层层的子行里,defines 盖住 offset 的那个:闪它,展开它上面的各层(parentId 和它所在的那串上层子行)。 */
|
||||
function definingItem(items: PropertyItem[] | undefined, parentId: string, offset: number): Spot | undefined {
|
||||
for (const [index, item] of (items ?? []).entries()) {
|
||||
const id = propertyItemId(parentId, index)
|
||||
if (item.defines && covers(item.defines, offset))
|
||||
return { row: id, expand: [parentId], span: width(item.defines) }
|
||||
const inner = definingItem(item.items, id, offset)
|
||||
if (inner)
|
||||
return { ...inner, expand: [parentId, ...inner.expand] }
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
|
||||
/** parentId 下面一层层的子行里,格子(连值那一格里套着的实参表)盖住 offset 的:展开它上面的各层,span 是那段有多长。 */
|
||||
function shownItem(items: PropertyItem[] | undefined, parentId: string, offset: number): Spot | undefined {
|
||||
for (const [index, item] of (items ?? []).entries()) {
|
||||
let span: number | undefined
|
||||
/** 一层层的子行里,格子盖住 offset 的第一个:要展开的各层,span 是那段有多长。 */
|
||||
function shownItem(items: Iterable<PlacedItem>, offset: number): Spot | undefined {
|
||||
for (const { item, expand } of items) {
|
||||
for (const cell of item.cells) {
|
||||
span = textSpan(cell, offset)
|
||||
const span = textSpan(cell, offset)
|
||||
if (span !== undefined)
|
||||
break
|
||||
return { expand, span }
|
||||
}
|
||||
span ??= tableSpan(item.table, offset)
|
||||
if (span !== undefined)
|
||||
return { expand: [parentId], span }
|
||||
const inner = shownItem(item.items, propertyItemId(parentId, index), offset)
|
||||
if (inner)
|
||||
return { ...inner, expand: [parentId, ...inner.expand] }
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
|
||||
/**
|
||||
* parentId 下面一层层的子行里,哪个的格子(连它值那一格里套着的实参表)满足 test(显示着要找的名字):
|
||||
* 要展开的各层(parentId 到那个子行所在的那一层)。
|
||||
*/
|
||||
function showingItem(items: PropertyItem[] | undefined, parentId: string, test: (cell: PreviewText) => boolean): string[] | undefined {
|
||||
for (const [index, item] of (items ?? []).entries()) {
|
||||
if (item.cells.some(test) || tableShows(item.table, test))
|
||||
return [parentId]
|
||||
const inner = showingItem(item.items, propertyItemId(parentId, index), test)
|
||||
if (inner)
|
||||
return [parentId, ...inner]
|
||||
/** 一层层的子行里,格子满足 test(显示着要找的名字)的第一个:要展开的各层。 */
|
||||
function showingItem(items: Iterable<PlacedItem>, test: (cell: PreviewText) => boolean): string[] | undefined {
|
||||
for (const { item, expand } of items) {
|
||||
if (item.cells.some(test))
|
||||
return expand
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
@@ -204,11 +189,11 @@ export function uncoveredPlan(plan: RevealPlan, covered: ReadonlyMap<string, str
|
||||
/** 块里哪儿显示着满足 test 的文字(译文和它下面的表、属性行的值、各层子行的格子):要展开的属性行和子行;不在这块里是 undefined。 */
|
||||
function showingSpot(block: BlockView, test: (text: PreviewText) => boolean): string[] | undefined {
|
||||
if ('translation' in block)
|
||||
return test(block.translation) || tableShows(block.table, test) ? [] : undefined
|
||||
return test(block.translation) ? [] : showingItem(tableItems(block.id, block.table), test)
|
||||
for (const [index, row] of block.properties.entries()) {
|
||||
if (test(row.value))
|
||||
return []
|
||||
const expand = showingItem(row.items, propertyRowId(block.id, index), test)
|
||||
const expand = showingItem(rowItems(row, propertyRowId(block.id, index)), test)
|
||||
if (expand)
|
||||
return expand
|
||||
}
|
||||
|
||||
@@ -70,18 +70,21 @@ export interface SourceRange { startIndex: number, endIndex: number }
|
||||
*/
|
||||
export interface Defines { defines?: SourceRange }
|
||||
|
||||
/** 属性表里展开的一个子行:几列对齐的格子,例如参数的名称 / 类型 / 默认值、字典一项的键 / 值;格子是着色的词或 Markdown(参数的说明、异常的触发条件) */
|
||||
/** 属性表里展开的一个子行:几列对齐的格子,例如参数的名称 / 类型 / 默认值、字典一项的键 / 值、列表的第 N 项 / 值;格子是着色的词或 Markdown(参数的说明、异常的触发条件) */
|
||||
export interface PropertyItem extends Defines {
|
||||
cells: PreviewText[]
|
||||
/** 展开在这个子行下面的下一层子行(字典 / 对象的值又是字典 / 对象时,它的各项),一层层可以一直往下套 */
|
||||
/**
|
||||
* 最后一格的值拆开的下一层子行(字典 / 对象、跨几行写的列表的各项):排成列表的子行展开在它下面、能折叠,
|
||||
* 表格的行排在最后一格里、文字下面、总是展开;一层层可以一直往下套
|
||||
*/
|
||||
items?: PropertyItem[]
|
||||
/** 排在这一行最后一格里、那格文字下面的表(实参的值里跨几行写的调用的实参表),表里的行又可以带 table;总是展开 */
|
||||
/** 最后一格的值里那次调用的实参表:表格的行排在最后一格里、文字下面,排成列表的子行排在它下面;总是展开,表里的行又可以带 items、table */
|
||||
table?: PreviewTable
|
||||
}
|
||||
|
||||
/**
|
||||
* 属性表的一行:左列属性名,右列着色的值或 Markdown 说明;items 是展开在这一行下面的子行(输入参数、导入项、字典 / 对象的各项)。
|
||||
* columns 是子行各列的列名(输入参数的名称 / 类型 / 默认值):给了列名的子行排成带表头和边框的表格,没给的排成列表
|
||||
* 属性表的一行:左列属性名,右列着色的值或 Markdown 说明;items 是展开在这一行下面的子行(输入参数、导入项、字典 / 对象、列表的各项)。
|
||||
* columns 是子行各列的列名(输入参数的名称 / 类型 / 默认值):给了列名的子行排成带表头和边框的表格,没给的排成列表;两种子行都能再带下一层
|
||||
*/
|
||||
export interface PropertyRow extends Defines {
|
||||
label: string
|
||||
|
||||
Reference in new issue
Block a user