From 2c490cac2d514456204d19808bf7bb0e28c275d7 Mon Sep 17 00:00:00 2001 From: cruldra Date: Tue, 13 Oct 2026 13:32:45 +1100 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20feat(vscode):=20=E4=BB=A3=E7=A0=81?= =?UTF-8?q?=E9=98=85=E8=AF=BB=E9=A2=84=E8=A7=88=E9=87=8C=E8=B7=A8=E5=87=A0?= =?UTF-8?q?=E8=A1=8C=E5=86=99=E7=9A=84=E5=88=97=E8=A1=A8=E6=8C=89=E9=A1=B9?= =?UTF-8?q?=E6=8B=86=E5=BC=80=EF=BC=8C=E5=88=97=E8=A1=A8=E9=87=8C=E6=95=B4?= =?UTF-8?q?=E9=A1=B9=E6=98=AF=E8=B0=83=E7=94=A8=E7=9A=84=E5=B8=A6=E7=9D=80?= =?UTF-8?q?=E8=87=AA=E5=B7=B1=E7=9A=84=E5=AE=9E=E5=8F=82=E8=A1=A8=EF=BC=8C?= =?UTF-8?q?=E4=B8=80=E5=B1=82=E5=B1=82=E8=83=BD=E5=BE=80=E4=B8=8B=E5=A5=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/superpowers/specs/code-reader/spec.md | 66 +++++-- vscode/src/codeReader/contract.ts | 39 ++-- .../src/codeReader/languages/python.test.ts | 69 +++++-- vscode/src/codeReader/languages/python.ts | 44 +++-- vscode/src/codeReader/languages/rust.test.ts | 25 ++- vscode/src/codeReader/languages/rust.ts | 13 +- .../codeReader/languages/typescript.test.ts | 115 ++++++----- vscode/src/codeReader/languages/typescript.ts | 64 +++--- vscode/src/codeReader/translators/bashZh.ts | 2 +- .../translators/contractOnly.test.ts | 2 +- .../codeReader/translators/dockerfileZh.ts | 2 +- vscode/src/codeReader/translators/pythonZh.ts | 2 +- vscode/src/codeReader/translators/rustZh.ts | 2 +- .../codeReader/translators/typescriptZh.ts | 2 +- .../codeReader/translators/zhTemplate.test.ts | 182 ++++++++++++++++- .../src/codeReader/translators/zhTemplate.ts | 187 +++++++++++------- .../webview-ui/src/codeReader/BlockTree.tsx | 2 +- .../src/codeReader/PropertyGrid.test.tsx | 125 ++++++++++++ .../src/codeReader/PropertyGrid.tsx | 79 +++++--- .../webview-ui/src/codeReader/reveal.test.ts | 18 ++ vscode/webview-ui/src/codeReader/reveal.ts | 109 +++++----- vscode/webview-ui/src/codeReader/types.ts | 13 +- 22 files changed, 848 insertions(+), 314 deletions(-) create mode 100644 vscode/webview-ui/src/codeReader/PropertyGrid.test.tsx diff --git a/docs/superpowers/specs/code-reader/spec.md b/docs/superpowers/specs/code-reader/spec.md index ab413c2..16530d5 100644 --- a/docs/superpowers/specs/code-reader/spec.md +++ b/docs/superpowers/specs/code-reader/spec.md @@ -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 不认识任何语言。 diff --git a/vscode/src/codeReader/contract.ts b/vscode/src/codeReader/contract.ts index 19a4ed9..06d060c 100644 --- a/vscode/src/codeReader/contract.ts +++ b/vscode/src/codeReader/contract.ts @@ -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 有这种语言的定义、高亮提供者时用不上,不填。 */ diff --git a/vscode/src/codeReader/languages/python.test.ts b/vscode/src/codeReader/languages/python.test.ts index ddd4209..86055a7 100644 --- a/vscode/src/codeReader/languages/python.test.ts +++ b/vscode/src/codeReader/languages/python.test.ts @@ -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 => 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([]) }) }) diff --git a/vscode/src/codeReader/languages/python.ts b/vscode/src/codeReader/languages/python.ts index f71ccb7..3de0269 100644 --- a/vscode/src/codeReader/languages/python.ts +++ b/vscode/src/codeReader/languages/python.ts @@ -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 = { @@ -671,6 +684,7 @@ export function createPythonAdapter(wasmDir: string): LanguageAdapter { freshDoc, iteration: pythonIteration, call: pythonCall, + collection: pythonCollection, displayNames: pythonNameReader, } } diff --git a/vscode/src/codeReader/languages/rust.test.ts b/vscode/src/codeReader/languages/rust.test.ts index 1d00f51..c5d218f 100644 --- a/vscode/src/codeReader/languages/rust.test.ts +++ b/vscode/src/codeReader/languages/rust.test.ts @@ -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')) diff --git a/vscode/src/codeReader/languages/rust.ts b/vscode/src/codeReader/languages/rust.ts index 2376a64..f289523 100644 --- a/vscode/src/codeReader/languages/rust.ts +++ b/vscode/src/codeReader/languages/rust.ts @@ -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') diff --git a/vscode/src/codeReader/languages/typescript.test.ts b/vscode/src/codeReader/languages/typescript.test.ts index f876ebb..1c27af0 100644 --- a/vscode/src/codeReader/languages/typescript.test.ts +++ b/vscode/src/codeReader/languages/typescript.test.ts @@ -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 {', - ' 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 {\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 => 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 {', + ' 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 {\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([ diff --git a/vscode/src/codeReader/languages/typescript.ts b/vscode/src/codeReader/languages/typescript.ts index bf32db3..0854d86 100644 --- a/vscode/src/codeReader/languages/typescript.ts +++ b/vscode/src/codeReader/languages/typescript.ts @@ -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, } } diff --git a/vscode/src/codeReader/translators/bashZh.ts b/vscode/src/codeReader/translators/bashZh.ts index de1e33e..4cc51b5 100644 --- a/vscode/src/codeReader/translators/bashZh.ts +++ b/vscode/src/codeReader/translators/bashZh.ts @@ -551,7 +551,7 @@ export const bashZhTranslator: Translator = createZhTemplateTranslator({ operators: {}, operatorParents: [], emptyLiterals: {}, - mapLiterals: {}, + collections: {}, stringNodes: [], commentText, nodeTemplates: {}, diff --git a/vscode/src/codeReader/translators/contractOnly.test.ts b/vscode/src/codeReader/translators/contractOnly.test.ts index 4cf755c..8eab34d 100644 --- a/vscode/src/codeReader/translators/contractOnly.test.ts +++ b/vscode/src/codeReader/translators/contractOnly.test.ts @@ -45,7 +45,7 @@ const translator = createZhTemplateTranslator({ operators: {}, operatorParents: [], emptyLiterals: {}, - mapLiterals: {}, + collections: {}, stringNodes: [], commentText: node => node.text, nodeTemplates: {}, diff --git a/vscode/src/codeReader/translators/dockerfileZh.ts b/vscode/src/codeReader/translators/dockerfileZh.ts index c5c636e..02ab13f 100644 --- a/vscode/src/codeReader/translators/dockerfileZh.ts +++ b/vscode/src/codeReader/translators/dockerfileZh.ts @@ -392,7 +392,7 @@ export const dockerfileZhTranslator = createZhTemplateTranslator({ operators: {}, operatorParents: [], emptyLiterals: {}, - mapLiterals: {}, + collections: {}, stringNodes: [], commentText: (_node, text) => text.replace(/^#[ \t]?/, ''), nodeTemplates: {}, diff --git a/vscode/src/codeReader/translators/pythonZh.ts b/vscode/src/codeReader/translators/pythonZh.ts index 60bbfa1..1ee3539 100644 --- a/vscode/src/codeReader/translators/pythonZh.ts +++ b/vscode/src/codeReader/translators/pythonZh.ts @@ -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,语法没给它们起字段名 diff --git a/vscode/src/codeReader/translators/rustZh.ts b/vscode/src/codeReader/translators/rustZh.ts index 8c36bec..9358a8e 100644 --- a/vscode/src/codeReader/translators/rustZh.ts +++ b/vscode/src/codeReader/translators/rustZh.ts @@ -50,7 +50,7 @@ const template = createZhTemplateTranslator({ operators: {}, operatorParents: [], emptyLiterals: {}, - mapLiterals: {}, + collections: { array_expression: '数组', tuple_expression: '元组' }, stringNodes: ['string_literal'], commentText, docNodes, diff --git a/vscode/src/codeReader/translators/typescriptZh.ts b/vscode/src/codeReader/translators/typescriptZh.ts index be3553e..08895bf 100644 --- a/vscode/src/codeReader/translators/typescriptZh.ts +++ b/vscode/src/codeReader/translators/typescriptZh.ts @@ -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: { diff --git a/vscode/src/codeReader/translators/zhTemplate.test.ts b/vscode/src/codeReader/translators/zhTemplate.test.ts index a435803..0a24866 100644 --- a/vscode/src/codeReader/translators/zhTemplate.test.ts +++ b/vscode/src/codeReader/translators/zhTemplate.test.ts @@ -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): Translator { return createZhTemplateTranslator({ @@ -1411,7 +1589,7 @@ describe('中文模板引擎', () => { operators: {}, operatorParents: [], emptyLiterals: {}, - mapLiterals: {}, + collections: {}, stringNodes: [], commentText: node => node.text, nodeTemplates: {}, diff --git a/vscode/src/codeReader/translators/zhTemplate.ts b/vscode/src/codeReader/translators/zhTemplate.ts index b76c298..cbf36d1 100644 --- a/vscode/src/codeReader/translators/zhTemplate.ts +++ b/vscode/src/codeReader/translators/zhTemplate.ts @@ -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 - /** 字典 / 对象字面量(语言插件给了 entries 的值)的叫法,按节点类型查:属性表里的值写成「字典 3 项」,各项展开成子行。 */ - mapLiterals: Record + /** 写出各项的字面量(语言插件给了各项,见 LanguageAdapter.collection)的叫法,按节点类型查:属性表里拆开的值写成「字典 3 项」「列表 2 项」。 */ + collections: Record /** 整段原样照抄、按字符串着色的节点。 */ 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 { - 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 { + const { text, items, table } = say.value(value) + if (table) + return { value: text, ...table } + return items ? { value: text, items } : { value: text } } diff --git a/vscode/webview-ui/src/codeReader/BlockTree.tsx b/vscode/webview-ui/src/codeReader/BlockTree.tsx index 8b6a842..4b4ba10 100644 --- a/vscode/webview-ui/src/codeReader/BlockTree.tsx +++ b/vscode/webview-ui/src/codeReader/BlockTree.tsx @@ -244,7 +244,7 @@ function BlockPreview({ block, tree, marks }: { block: BlockView, tree: BlockTre {marks?.translation ? : } {block.table && (
- +
)} {marks?.doc ? : block.doc && } diff --git a/vscode/webview-ui/src/codeReader/PropertyGrid.test.tsx b/vscode/webview-ui/src/codeReader/PropertyGrid.test.tsx new file mode 100644 index 0000000..f7bf4ab --- /dev/null +++ b/vscode/webview-ui/src/codeReader/PropertyGrid.test.tsx @@ -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 { + 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 = new Set(), onToggleFold: (id: string) => void = () => {}): Promise { + await act(async () => root.render()) +} + +function byId(id: string): HTMLElement { + const found = container.querySelector(`[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() + }) +}) diff --git a/vscode/webview-ui/src/codeReader/PropertyGrid.tsx b/vscode/webview-ui/src/codeReader/PropertyGrid.tsx index 70b0d6b..8dbe00c 100644 --- a/vscode/webview-ui/src/codeReader/PropertyGrid.tsx +++ b/vscode/webview-ui/src/codeReader/PropertyGrid.tsx @@ -122,9 +122,20 @@ function ItemHead({ id, cell, foldable, folding }: { id: string, cell: PreviewTe ) } +/** 往右缩进一层、左边一条竖线:一行或一个子行下面的一层子行,列表的一项下面挂的实参表 */ +function Nested({ children }: { children: ReactNode }): ReactElement { + return ( +
+
+
{children}
+
+ ) +} + /** * 展开在一行或一个子行(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 && } + {item.table && ( + + + + )} + + ) return ( <>
0 && change !== 'del'} folding={folds} /> : )}
- {children.length > 0 && !folds.collapsed.has(id) && (change === 'del' - ? ( -
- -
- ) - : )} + {(open || item.table) && (change === 'del' ?
{below}
: below)} ) } const removed = (after: number): ReactElement[] => removedAt(marks?.removed, after) .map(({ value, index }) => {itemRow(value, `${parentId}/x${index}`, 'del', undefined)}) return ( -
-
-
- {removed(-1)} - {items.map((item, index) => { - const mark = marks?.changed[index] - return ( - - {itemRow(item, propertyItemId(parentId, index), mark?.added && 'add', mark)} - {removed(index)} - - ) - })} -
-
+ + {removed(-1)} + {items.map((item, index) => { + const mark = marks?.changed[index] + return ( + + {itemRow(item, propertyItemId(parentId, index), mark?.added && 'add', mark)} + {removed(index)} + + ) + })} + ) } /** * 给了列名的子行(输入参数、数据表的字段)排成表格:第一行是表头(列名),下面每个子行一行;外框圆角,格子之间、行之间一条分隔线。 * 列宽按整列的内容定(见 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 }): 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 && {SIGNS[change ?? 'none']} /** 一格:讲子块的那一格是展开它的按钮(只放着色的词),别的照常(见 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 (
- {cells.map((cell, i) => item.table && i === cells.length - 1 + {cells.map((cell, i) => (listed || item.table) && i === cells.length - 1 ? (
- + {listed && } + {item.table && }
) : {cellBox(cell, i, 'cr-table-cell')})} @@ -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 }) => ) return ( @@ -298,8 +317,8 @@ export function PropertyGrid({ blockId, rows, collapsed, onToggleFold, marks, sl : {row.label}}
- {!folded && layout === 'table' && } - {!folded && layout === 'list' && } + {!folded && layout === 'table' && } + {!folded && layout === 'list' && } {!folded && slots && shownBlocks.length > 0 &&
{slots.render(shownBlocks)}
} {removed(index)} @@ -337,7 +356,7 @@ function RemovedRow({ id, row }: { id: string, row: PropertyRow }): ReactElement {layout && (
{layout === 'table' - ? + ? : }
)} diff --git a/vscode/webview-ui/src/codeReader/reveal.test.ts b/vscode/webview-ui/src/codeReader/reveal.test.ts index 0a36b48..f7c36f4 100644 --- a/vscode/webview-ui/src/codeReader/reveal.test.ts +++ b/vscode/webview-ui/src/codeReader/reveal.test.ts @@ -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[] = [ diff --git a/vscode/webview-ui/src/codeReader/reveal.ts b/vscode/webview-ui/src/codeReader/reveal.ts index 13320c2..390777b 100644 --- a/vscode/webview-ui/src/codeReader/reveal.ts +++ b/vscode/webview-ui/src/codeReader/reveal.ts @@ -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 { + 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 { + return nestedItems(row.items, id, [id], !row.columns) +} + +/** 一行译文下面那张实参表(总是展开的)里一层层的子行。 */ +function tableItems(blockId: string, table: PreviewTable | undefined): Generator { + 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, 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, 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 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 } diff --git a/vscode/webview-ui/src/codeReader/types.ts b/vscode/webview-ui/src/codeReader/types.ts index 7da57e9..34bee55 100644 --- a/vscode/webview-ui/src/codeReader/types.ts +++ b/vscode/webview-ui/src/codeReader/types.ts @@ -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