✨ feat(vscode): 代码阅读把字典、对象字面量的值展开成键值树

This commit is contained in:
cruldra committed 2026-09-25 20:57:27 +08:00
1 parent 51f2ef7bfa
commit 4930dc0d42
20 files changed
+578 -103

No files matched your search

+48 -21
View File
@@ -19,7 +19,7 @@
| 多种翻译怎么选 | 预览顶部下拉框,整个文件一起换,按语言记住选择;只有一种时不显示 | | 多种翻译怎么选 | 预览顶部下拉框,整个文件一起换,按语言记住选择;只有一种时不显示 |
| 侧栏点文件 | 支持的语言默认打开成「预览」;不支持的语言用普通编辑器打开 | | 侧栏点文件 | 支持的语言默认打开成「预览」;不支持的语言用普通编辑器打开 |
| 块内修改 | 改的就是文件本身:未保存标记、⌘S 保存、⌘Z 撤销都照常,切回「预览」按新代码重新翻译 | | 块内修改 | 改的就是文件本身:未保存标记、⌘S 保存、⌘Z 撤销都照常,切回「预览」按新代码重新翻译 |
| 折叠 | 有子块的块、属性表里带子行的行能折叠;函数块(含方法、嵌套函数)和类块默认收起主体、属性表照常显示,其余块和属性行默认展开;用户点过的按他点的记住 | | 折叠 | 有子块的块、属性表里带子行的行、带下一层的子行(字典 / 对象里嵌套的字典 / 对象)能折叠;函数块(含方法、嵌套函数)和类块默认收起主体、属性表照常显示,其余块、属性行和子行默认展开;用户点过的按他点的记住 |
| 函数、类的说明 | TypeScript 紧贴在函数、类前面的 JSDoc,Python 函数、类主体开头的 docstring 是它自己的说明:进属性表「说明」一行,不再单独成一块注释;它的源码行算在函数、类块的头部里,「编辑」时和签名一起改 | | 函数、类的说明 | TypeScript 紧贴在函数、类前面的 JSDoc,Python 函数、类主体开头的 docstring 是它自己的说明:进属性表「说明」一行,不再单独成一块注释;它的源码行算在函数、类块的头部里,「编辑」时和签名一起改 |
| 注释按 Markdown 显示 | 注释块和函数、类的「说明」按 Markdown 渲染,像 Kotlin 的 KDoc:去掉注释符号、引号和共同缩进后的原文原样交给 Markdown,不按语言转换(reStructuredText 的 `:func:`、JSDoc 的 `@param` 都照抄,注释以后慢慢改成 Markdown 写法);段内单个换行是软换行,空行分段 | | 注释按 Markdown 显示 | 注释块和函数、类的「说明」按 Markdown 渲染,像 Kotlin 的 KDoc:去掉注释符号、引号和共同缩进后的原文原样交给 Markdown,不按语言转换(reStructuredText 的 `:func:`、JSDoc 的 `@param` 都照抄,注释以后慢慢改成 Markdown 写法);段内单个换行是软换行,空行分段 |
| 跳到定义 | 和普通编辑器一样 ⌘ / Ctrl + 点击名字跳到定义,定义由 VS Code 装的语言扩展(Pylance、TypeScript 语言服务……)查;目标语言有代码阅读就在预览里打开 | | 跳到定义 | 和普通编辑器一样 ⌘ / Ctrl + 点击名字跳到定义,定义由 VS Code 装的语言扩展(Pylance、TypeScript 语言服务……)查;目标语言有代码阅读就在预览里打开 |
@@ -27,6 +27,7 @@
| 文件注释 | 读代码时给目录 / 文件随手记一句:存在各工作区文件夹的 `.spx/code-notes.json`,跟着仓库走;在 VS Code 里改名、挪位置、删除时注释跟着变 | | 文件注释 | 读代码时给目录 / 文件随手记一句:存在各工作区文件夹的 `.spx/code-notes.json`,跟着仓库走;在 VS Code 里改名、挪位置、删除时注释跟着变 |
| 译文用词 | 结构照抄原语言,只翻关键字、内置类型、运算符;用专业术语(`export` → 公共,`catch` → 捕获,`===` → 全等) | | 译文用词 | 结构照抄原语言,只翻关键字、内置类型、运算符;用专业术语(`export` → 公共,`catch` → 捕获,`===` → 全等) |
| 函数、类、导入、常量、变量块 | 预览不显示一行译文,显示树形属性表;属性值里的类型、表达式照常直译 | | 函数、类、导入、常量、变量块 | 预览不显示一行译文,显示树形属性表;属性值里的类型、表达式照常直译 |
| 字典 / 对象字面量 | 常量、变量的值是 Python 字典 / TypeScript 对象字面量时不挤成一长行:值写「字典 N 项」/「对象 N 项」,各项展开成「键 \| 值」两列的子行,值又是字典 / 对象时一层层往下展开;参数默认值照旧一行 |
## 用户可见行为 ## 用户可见行为
@@ -75,14 +76,14 @@
### 属性表(函数、类、导入、常量、变量块) ### 属性表(函数、类、导入、常量、变量块)
卡片外观(折叠箭头、种类标签、底色、悬停高亮、「编辑 / 预览」切换)和其他块一样,只是内容换成一张属性表:左列属性名(界面字体、`descriptionForeground` 色,宽 76px),右列是值(等宽字体,按词性着色,长了换行),两列间距 12px,行高 20px。「输入参数」「导入项」下面展开子行,属性名前带折叠箭头(图标、大小、颜色同块的折叠箭头;点箭头或属性名切换,默认展开,折叠后值「N 个」照常显示,只收起子行);子行往右缩进 5px,左边一条 1px 竖线(`editorIndentGuide-background1`)。输入参数的子行分名称 / 类型 / 默认值三列对齐,前两列宽 150px / 220px,编辑区窄时按 28% / 38% 收,默认值列占剩下的。 卡片外观(折叠箭头、种类标签、底色、悬停高亮、「编辑 / 预览」切换)和其他块一样,只是内容换成一张属性表:左列属性名(界面字体、`descriptionForeground` 色,宽 76px),右列是值(等宽字体,按词性着色,长了换行),两列间距 12px,行高 20px。「输入参数」「导入项」和值是字典 / 对象字面量的「值」「初始值」下面展开子行,属性名前带折叠箭头(图标、大小、颜色同块的折叠箭头;点箭头或属性名切换,默认展开,折叠后值「N 个」「字典 N 项」照常显示,只收起子行);子行往右缩进 5px,左边一条 1px 竖线(`editorIndentGuide-background1`)。输入参数的子行分名称 / 类型 / 默认值三列对齐,前两列宽 150px / 220px,编辑区窄时按 28% / 38% 收,默认值列占剩下的;字典 / 对象的子行分键 / 值两列,键列同名称列。子行下面还能再展开一层(嵌套的字典 / 对象):带下一层的子行第一列前面有折叠箭头(点箭头或第一列切换,默认展开),同一层里其余子行在同样的位置留空、第一列对齐;下一层同样往右缩进 5px、左边一条竖线,竖线对着上一层的箭头,每层自己的两列对齐。同一层里没有带下一层的子行时不留箭头的位置(和参数、导入项的子行一样)。
| 种类 | 行 | | 种类 | 行 |
| --- | --- | | --- | --- |
| 函数 | 名称(函数色;匿名函数写「匿名」)/ 说明(有说明才显示:JSDoc 去掉 `/**`、`*/` 和每行开头的 `*`,docstring 去掉引号;再去掉共同缩进和首尾空行,`@param` 这类标签行照抄;得到的原文按 Markdown 渲染,见「注释的 Markdown 排版」;里面的名字不能点)/ 访问级别 / 异步(是 / 否)/ 装饰器(有才显示,源码原样照抄、按源码着色,多个换行)/ 输入参数(`N 个` 或「无」,每个参数一行:名称 \| 类型(没标注写「未标注」)\| 默认值(`默认 ` + 译文,没有留空))/ 输出参数(返回类型的译文,没标注写「未标注」) | | 函数 | 名称(函数色;匿名函数写「匿名」)/ 说明(有说明才显示:JSDoc 去掉 `/**`、`*/` 和每行开头的 `*`,docstring 去掉引号;再去掉共同缩进和首尾空行,`@param` 这类标签行照抄;得到的原文按 Markdown 渲染,见「注释的 Markdown 排版」;里面的名字不能点)/ 访问级别 / 异步(是 / 否)/ 装饰器(有才显示,源码原样照抄、按源码着色,多个换行)/ 输入参数(`N 个` 或「无」,每个参数一行:名称 \| 类型(没标注写「未标注」)\| 默认值(`默认 ` + 译文,没有留空))/ 输出参数(返回类型的译文,没标注写「未标注」) |
| 类 | 名称(类型色;匿名类 `export default class {}` 写「匿名」;赋给变量、类字段的类表达式写变量名、字段名)/ 说明(有说明才显示,规则同函数)/ 访问级别 / 抽象(是 / 否,只有 TypeScript 有,看 `abstract class`,TypeScript 的类都有这一行)/ 装饰器(同函数)/ 类型参数(有才显示:TypeScript `<K, T extends X = Y>` 每个参数的译文,逗号隔开,如 `K, T 限定为 X = Y`;Python 3.12 的 `class G[T]` 同样)/ 父类(有才显示:TypeScript `extends` 后面的表达式连着泛型实参的译文,如 `Base<字符串>`;Python 类名后面括号里的参数列表照写的顺序逐项翻,关键字参数、`*bases` 也在这一行,如 `Base, metaclass=ABCMeta`;逗号隔开)/ 实现的接口(有才显示:TypeScript `implements` 后面每个类型的译文,逗号隔开) | | 类 | 名称(类型色;匿名类 `export default class {}` 写「匿名」;赋给变量、类字段的类表达式写变量名、字段名)/ 说明(有说明才显示,规则同函数)/ 访问级别 / 抽象(是 / 否,只有 TypeScript 有,看 `abstract class`,TypeScript 的类都有这一行)/ 装饰器(同函数)/ 类型参数(有才显示:TypeScript `<K, T extends X = Y>` 每个参数的译文,逗号隔开,如 `K, T 限定为 X = Y`;Python 3.12 的 `class G[T]` 同样)/ 父类(有才显示:TypeScript `extends` 后面的表达式连着泛型实参的译文,如 `Base<字符串>`;Python 类名后面括号里的参数列表照写的顺序逐项翻,关键字参数、`*bases` 也在这一行,如 `Base, metaclass=ABCMeta`;逗号隔开)/ 实现的接口(有才显示:TypeScript `implements` 后面每个类型的译文,逗号隔开) |
| 导入 | 来源(TypeScript 是字符串字面量原样,如 `'node:fs'`,字符串色;Python 是点分模块名,如 `fastapi`、`.models`,类型色)/ 仅类型(是 / 否,只有 TypeScript 有,看 `import type`)/ 导入项(`N 个` 或「无」,每项一行:`name`、`name 作为 alias`;默认导入写「默认导出 作为 x」,`* as x` 写「全部 作为 x」,Python `from m import *` 写「全部」,Python `import a.b as c` 写「整个模块 作为 c」,一条 `import a, b` 导入多个模块时写模块名) | | 导入 | 来源(TypeScript 是字符串字面量原样,如 `'node:fs'`,字符串色;Python 是点分模块名,如 `fastapi`、`.models`,类型色)/ 仅类型(是 / 否,只有 TypeScript 有,看 `import type`)/ 导入项(`N 个` 或「无」,每项一行:`name`、`name 作为 alias`;默认导入写「默认导出 作为 x」,`* as x` 写「全部 作为 x」,Python `from m import *` 写「全部」,Python `import a.b as c` 写「整个模块 作为 c」,一条 `import a, b` 导入多个模块时写模块名) |
| 常量 / 变量 | 名称(解构声明写整个模式,如 `{ p, q }`)/ 访问级别 / 类型(写了标注才有)/ 值(常量)或初始值(变量):初始化表达式的译文,没有写「无」。一条语句声明几个变量(`let a = 1, b = 2`)就重复几组 | | 常量 / 变量 | 名称(解构声明写整个模式,如 `{ p, q }`)/ 访问级别 / 类型(写了标注才有)/ 值(常量)或初始值(变量):初始化表达式的译文,没有写「无」;是字典 / 对象字面量时见下面「字典 / 对象字面量」。一条语句声明几个变量(`let a = 1, b = 2`)就重复几组 |
访问级别(公共 / 私有 / 受保护): 访问级别(公共 / 私有 / 受保护):
@@ -92,6 +93,16 @@
值里的表达式只翻落在块头部里的那一段:`const v = useMemo(() => { … })` 这种值带语句块主体回调的块,回调主体是子块,值只翻到 `useMemo(() =>`,和块头部一致。 值里的表达式只翻落在块头部里的那一段:`const v = useMemo(() => { … })` 这种值带语句块主体回调的块,回调主体是子块,值只翻到 `useMemo(() =>`,和块头部一致。
字典 / 对象字面量(常量、变量的值,含 Python 类主体里不带注解的类属性;参数默认值照旧是一行译文):
- 值写项数:Python `字典 N 项`、TypeScript `对象 N 项`(按 hint 着色),各项展开成子行,照源码顺序,注释不算一项。
- 每项两列:键 | 值。键照源码写法:字符串键带引号、按字符串着色,名字键按名字着色,计算属性名 `[k]` 照常翻;值照常直译(`等待 …`、`空数组`……)。
- 值又是字典 / 对象字面量时,它这一项的值也写项数,它的各项展开成下一层子行,一层层往下套。
- 展开:Python `**other`、TypeScript `...other` 是一项,键写「展开」(hint),值是展开的表达式。
- TypeScript 简写 `{ a }`:键 `a`,值 `a`。对象里的方法、getter、setter:键是方法名(函数色),值是方法头部(到主体开始之前)的译文,如 `异步 load(id: 字符串): Promise<无>`、`get size()`。
- 空字面量不展开:值是一行 `空字典` / `空对象`(一行译文里也这样翻,同 `空数组`)。
- `as const`、`satisfies` 包着的对象(`{ … } as const`)不算字面量本身,照旧一行译文。
### 注释的 Markdown 排版 ### 注释的 Markdown 排版
- 用在:注释块(`#`、`//`、`/* */`、不挂在函数、类上的 JSDoc、Python 模块开头的 docstring)的预览,函数、类属性表的「说明」。视图数据里它们是 `{ markdown }`(见「通用契约」的 `Markdown`),webview 按数据决定怎么渲染,不猜。 - 用在:注释块(`#`、`//`、`/* */`、不挂在函数、类上的 JSDoc、Python 模块开头的 docstring)的预览,函数、类属性表的「说明」。视图数据里它们是 `{ markdown }`(见「通用契约」的 `Markdown`),webview 按数据决定怎么渲染,不猜。
@@ -103,7 +114,7 @@
### 跳到定义 ### 跳到定义
- 能点的:预览里(属性表的名称、类型、来源、导入项、默认值、值、类型参数、父类、实现的接口,以及一行译文)照抄自源码的名字——标识符、属性名、类型名、点分模块名的每一节(`luffy_agent.core.db` 三节各自能点,和 VS Code 一样)、字符串(TypeScript 的模块路径 `'./x'` 就是字符串)。翻出来的关键字、运算符、说明词不能点。 - 能点的:预览里(属性表的名称、类型、来源、导入项、默认值、值、字典 / 对象各层子行的键和值、类型参数、父类、实现的接口,以及一行译文)照抄自源码的名字——标识符、属性名、类型名、点分模块名的每一节(`luffy_agent.core.db` 三节各自能点,和 VS Code 一样)、字符串(TypeScript 的模块路径 `'./x'` 就是字符串)。翻出来的关键字、运算符、说明词不能点。
- 链接和编辑器里一样:只有按住修饰键(macOS ⌘,其余平台 Ctrl)、鼠标停在**查得到定义**的名字上时,名字加下划线、换成链接色(`--vscode-editorLink-activeForeground`)、光标变手形;松开修饰键或移开就恢复。有没有定义问扩展端(`vscode.executeDefinitionProvider`,文档 uri + 名字在源码里的位置);和编辑器一样,每回指到一个名字上(或指着名字按下修饰键、换了文档版本)都现问,回答不留到下次用——Pylance、tsserver 刚启动那几秒会答「没有」;只认最近一次询问的回答,迟到的丢掉。 - 链接和编辑器里一样:只有按住修饰键(macOS ⌘,其余平台 Ctrl)、鼠标停在**查得到定义**的名字上时,名字加下划线、换成链接色(`--vscode-editorLink-activeForeground`)、光标变手形;松开修饰键或移开就恢复。有没有定义问扩展端(`vscode.executeDefinitionProvider`,文档 uri + 名字在源码里的位置);和编辑器一样,每回指到一个名字上(或指着名字按下修饰键、换了文档版本)都现问,回答不留到下次用——Pylance、tsserver 刚启动那几秒会答「没有」;只认最近一次询问的回答,迟到的丢掉。
- ⌘ / Ctrl + 点击: - ⌘ / Ctrl + 点击:
- 一处定义,在当前文件:原地滚过去; - 一处定义,在当前文件:原地滚过去;
@@ -114,10 +125,10 @@
- 和普通编辑器里 ⌘ + 点击一样:目标是否按「预览标签」打开听 `workbench.editor.enablePreview`;`workbench.editor.enablePreviewFromCodeNavigation` 为假(默认)时先把当前预览标签固定住,免得被目标替掉。 - 和普通编辑器里 ⌘ + 点击一样:目标是否按「预览标签」打开听 `workbench.editor.enablePreview`;`workbench.editor.enablePreviewFromCodeNavigation` 为假(默认)时先把当前预览标签固定住,免得被目标替掉。
- 闪哪一处:页面上**就是这处定义**的最具体的那一处,只按源码位置判断(定义提供者给的定义处下标,对上块和属性行带的源码范围),不看属性名文字: - 闪哪一处:页面上**就是这处定义**的最具体的那一处,只按源码位置判断(定义提供者给的定义处下标,对上块和属性行带的源码范围),不看属性名文字:
1. 定义是块自己的名字(函数名、类名;只声明一个变量时的变量名,解构模式整个算)→ 整块。块的这段范围由扩展端从 facts 取(`BlockView.defines`),和翻译插件无关。 1. 定义是块自己的名字(函数名、类名;只声明一个变量时的变量名,解构模式整个算)→ 整块。块的这段范围由扩展端从 facts 取(`BlockView.defines`),和翻译插件无关。
2. 定义是属性表里某一行 / 子行讲的东西(参数、导入项、一条语句声明几个变量时的某一个、类的泛型参数)→ 只闪这一行 / 子行。范围是翻译插件从 facts 取的节点(`PropertyRow.defines`、`PropertyItem.defines`),定义处落在范围里就算(`*args` 的定义在 `args` 上、解构参数里的名字都对得上)。 2. 定义是属性表里某一行 / 子行讲的东西(参数、导入项、一条语句声明几个变量时的某一个、类的泛型参数、对象字面量一项的键——TypeScript 对象的属性名就是定义,嵌套几层都算)→ 只闪这一行 / 子行。范围是翻译插件从 facts 取的节点(`PropertyRow.defines`、`PropertyItem.defines`;字典 / 对象的项标的是键,展开项不标),定义处落在范围里就算(`*args` 的定义在 `args` 上、解构参数里的名字都对得上)。
3. 其余(定义在一行译文里,例如循环变量;翻译插件没填 `defines`)→ 显示着定义处那个名字的块,名字在属性行的子行里时展开那一行;页面上找不到这个名字就取包含定义那一行的块。 3. 其余(定义在一行译文里,例如循环变量;翻译插件没填 `defines`)→ 显示着定义处那个名字的块,名字在属性行的子行里时展开那一行(在嵌套的子行里时连它上面各层一起展开);页面上找不到这个名字就取包含定义那一行的块。
- 同一文件原地跳、跳进另一个文件的预览,规则一样。 - 同一文件原地跳、跳进另一个文件的预览,规则一样。
- 滚过去:目标所在块的祖先块折叠着就展开,目标在折叠的属性行子行里(参数、导入项)就展开那一行。目标已在视口里不滚,否则滚到视口正中(比视口高就顶部对齐)。然后目标的边框和底色往聚焦色(`--vscode-focusBorder`)闪三下,共约 1.5 秒后复原;同一处再跳一次从头闪;系统开了「减少动态效果」时改成静止高亮 1.5 秒。目标是属性行 / 子行但块正在编辑(页面上没有属性表)时闪整块。 - 滚过去:目标所在块的祖先块折叠着就展开,目标在折叠的属性行子行里(参数、导入项、字典 / 对象的项)就展开那一行,在嵌套的子行里就连它上面折叠着的各层子行一起展开。目标已在视口里不滚,否则滚到视口正中(比视口高就顶部对齐)。然后目标的边框和底色往聚焦色(`--vscode-focusBorder`)闪三下,共约 1.5 秒后复原;同一处再跳一次从头闪;系统开了「减少动态效果」时改成静止高亮 1.5 秒。目标是属性行 / 子行但块正在编辑(页面上没有属性表)时闪整块。
- 不按修饰键的点击照旧;⌘ / Ctrl + 点击名字不会触发块的编辑切换、折叠。 - 不按修饰键的点击照旧;⌘ / Ctrl + 点击名字不会触发块的编辑切换、折叠。
## 通用契约 ## 通用契约
@@ -182,7 +193,18 @@ export interface ImportFacts {
items: ImportItem[] items: ImportItem[]
} }
export interface DeclaratorFacts { name: Node, type?: Node, value?: Node } /** 字典 / 对象字面量里的一项,照源码顺序;注释不算。 */
export type EntryFacts =
| { kind: 'pair', key: Node, value: Node, entries?: EntryFacts[] } // 'a': 1、a: 1、[k]: v;TS 简写 { a } 键和值是同一个节点;entries 同 DeclaratorFacts.entries
| { kind: 'spread', value: Node } // **other、...other:value 是展开的表达式
| { kind: 'method', name: Node, node: Node, header: SourceSpan } // 对象里的方法、getter、setter:header 同 BlockShape.header,只到主体开始之前
export interface DeclaratorFacts {
name: Node
type?: Node
value?: Node
entries?: EntryFacts[] // 值是字典 / 对象字面量(Python dictionary、TS object)时是它的各项,空字面量是空数组;别的值没有
}
export interface VariableFacts { export interface VariableFacts {
access?: Access access?: Access
@@ -238,15 +260,15 @@ export type PreviewText = Segment[] | Markdown
/** /**
* 属性表里的一行或一个子行讲的是哪个定义:定义出来的那个名字的节点(取自 facts)在源码里的范围—— * 属性表里的一行或一个子行讲的是哪个定义:定义出来的那个名字的节点(取自 facts)在源码里的范围——
* 参数名(连 `*` 前缀、整个解构模式)、导入项(名称到别名)、声明的变量名、类的泛型参数(整串)。跳到的定义落在里面就只闪这一行; * 参数名(连 `*` 前缀、整个解构模式)、导入项(名称到别名)、声明的变量名、类的泛型参数(整串)、字典 / 对象一项的键。跳到的定义落在里面就只闪这一行;
* 第三方翻译插件可以不填,不填就闪整块。 * 第三方翻译插件可以不填,不填就闪整块。
*/ */
export interface Defines { defines?: SourceRange } export interface Defines { defines?: SourceRange }
/** 属性表里展开的一个子行:几列对齐的格子。 */ /** 属性表里展开的一个子行:几列对齐的格子;items 是展开在它下面的下一层子行(嵌套的字典 / 对象),一层层可以一直往下套。 */
export interface PropertyItem extends Defines { cells: Segment[][] } export interface PropertyItem extends Defines { cells: Segment[][], items?: PropertyItem[] }
/** 属性表的一行:属性名、着色的值或 Markdown 说明(函数、类的「说明」)、展开在下面的子行(输入参数、导入项)。 */ /** 属性表的一行:属性名、着色的值或 Markdown 说明(函数、类的「说明」)、展开在下面的子行(输入参数、导入项、字典 / 对象的各项)。 */
export interface PropertyRow extends Defines { label: string, value: PreviewText, items?: PropertyItem[] } export interface PropertyRow extends Defines { label: string, value: PreviewText, items?: PropertyItem[] }
/** 语言插件:语法树 → 代码块树。 */ /** 语言插件:语法树 → 代码块树。 */
@@ -279,7 +301,7 @@ export interface CodeReaderApi {
- `activate()` 返回 `{ codeReader: CodeReaderApi }`;其他扩展声明 `extensionDependencies` 后用 `extensions.getExtension('clurdra.superpowers-vscode-clurdra')!.exports.codeReader` 注册。 - `activate()` 返回 `{ codeReader: CodeReaderApi }`;其他扩展声明 `extensionDependencies` 后用 `extensions.getExtension('clurdra.superpowers-vscode-clurdra')!.exports.codeReader` 注册。
- 内置的 TypeScript、Python 语言插件和两个中文直译翻译插件在 `activate()` 里通过同一个 API 注册,没有旁路。 - 内置的 TypeScript、Python 语言插件和两个中文直译翻译插件在 `activate()` 里通过同一个 API 注册,没有旁路。
- 注册表变化时更新 `superpowers.codeReader.languageIds` 上下文,并让已打开的预览刷新。 - 注册表变化时更新 `superpowers.codeReader.languageIds` 上下文,并让已打开的预览刷新。
- 分工:语言插件只从语法树读出和语言无关的事实(facts,引用源码节点,不做正则匹配文字);翻译插件决定属性名、枚举词(公共 / 是 / 默认导出)和值怎么译。第三方语言插件给函数、类、导入、常量、变量块填 facts 就能用上任何翻译插件的属性表。 - 分工:语言插件只从语法树读出和语言无关的事实(facts,引用源码节点,不做正则匹配文字);翻译插件决定属性名、枚举词(公共 / 是 / 默认导出)和值怎么译。第三方语言插件给函数、类、导入、常量、变量块填 facts 就能用上任何翻译插件的属性表;自己语言的字典 / 映射字面量填进 `DeclaratorFacts.entries` 就能展开成子行(中文模板直译按字面量的节点类型查叫法,见 `ZhTable.mapLiterals`)。
## 解析运行时 ## 解析运行时
@@ -327,11 +349,11 @@ export interface CodeReaderApi {
- TypeScript 函数:`function_declaration` / `generator_function_declaration` / `function_signature`、类里的 `method_definition` / 方法签名、值是带语句块主体函数的变量和类字段、`export default` 的函数值。名称取声明名 / 变量名 / 字段名;说明取紧贴在前面的 JSDoc 注释节点(见上面代码块树规则);参数来自 `formal_parameters`(`required_parameter` / `optional_parameter` 的 pattern、type、value,JavaScript 的 `assignment_pattern`),不带括号的单参数箭头函数取 `parameter`;返回类型取 `return_type` 里的类型;异步看有没有 `async`;类成员的装饰器是类主体里排在它前面的 `decorator` 兄弟节点。 - TypeScript 函数:`function_declaration` / `generator_function_declaration` / `function_signature`、类里的 `method_definition` / 方法签名、值是带语句块主体函数的变量和类字段、`export default` 的函数值。名称取声明名 / 变量名 / 字段名;说明取紧贴在前面的 JSDoc 注释节点(见上面代码块树规则);参数来自 `formal_parameters`(`required_parameter` / `optional_parameter` 的 pattern、type、value,JavaScript 的 `assignment_pattern`),不带括号的单参数箭头函数取 `parameter`;返回类型取 `return_type` 里的类型;异步看有没有 `async`;类成员的装饰器是类主体里排在它前面的 `decorator` 兄弟节点。
- TypeScript 类:`class_declaration` / `abstract_class_declaration`、类表达式 `class`(`export default class {}`、变量的值、类字段的值)。名称取类名 / 变量名 / 字段名;说明同函数;访问级别同函数(类字段按类成员);抽象看节点类型是不是 `abstract_class_declaration`;装饰器是 `export_statement` 和类声明自己的 `decorator` 子节点(`@d export class`、`export @d class`、`@d class` 都算),类字段的值是类表达式时还有类主体里排在它前面的 `decorator`;泛型参数是 `type_parameters` 里每个 `type_parameter`;父类、实现的接口在 `class_heritage` 里:`extends_clause` 的每个 value 一项,跟在它后面的 `type_arguments` 归它,`implements_clause` 里每个类型一项;JavaScript 的 `class_heritage` 直接是 `extends` 后面的表达式。 - TypeScript 类:`class_declaration` / `abstract_class_declaration`、类表达式 `class`(`export default class {}`、变量的值、类字段的值)。名称取类名 / 变量名 / 字段名;说明同函数;访问级别同函数(类字段按类成员);抽象看节点类型是不是 `abstract_class_declaration`;装饰器是 `export_statement` 和类声明自己的 `decorator` 子节点(`@d export class`、`export @d class`、`@d class` 都算),类字段的值是类表达式时还有类主体里排在它前面的 `decorator`;泛型参数是 `type_parameters` 里每个 `type_parameter`;父类、实现的接口在 `class_heritage` 里:`extends_clause` 的每个 value 一项,跟在它后面的 `type_arguments` 归它,`implements_clause` 里每个类型一项;JavaScript 的 `class_heritage` 直接是 `extends` 后面的表达式。
- TypeScript 导入:`import_statement` 的 `source`(`import x = require()` 取 `import_require_clause` 里的);`import_clause` 里的标识符是默认导入,`namespace_import` 是 `* as`,`named_imports` 里每个 `import_specifier` 的 name / alias;语句本身带 `type` 是仅类型。 - TypeScript 导入:`import_statement` 的 `source`(`import x = require()` 取 `import_require_clause` 里的);`import_clause` 里的标识符是默认导入,`namespace_import` 是 `* as`,`named_imports` 里每个 `import_specifier` 的 name / alias;语句本身带 `type` 是仅类型。
- TypeScript 变量:每个 `variable_declarator` 的 name / type / value。 - TypeScript 变量:每个 `variable_declarator` 的 name / type / value;value 是 `object` 时 entries 是它的具名子节点(注释除外):`pair` 的 key / value(value 又是 `object` 时连它的 entries)、`shorthand_property_identifier`(键和值都是它)、`spread_element` 里的表达式、`method_definition` 的 name 和头部(到 body 之前)。
- Python 函数:`function_definition` 的 name / parameters / return_type,`async` 子节点;说明是主体开头 docstring 的字符串节点(`string` / `concatenated_string`);装饰器是 `decorated_definition` 里的 `decorator`;参数 `identifier`、`typed_parameter`(名字是第一个具名子节点)、`default_parameter`、`typed_default_parameter`、`*args` / `**kwargs`,`*` 和 `/` 分隔符不算参数。 - Python 函数:`function_definition` 的 name / parameters / return_type,`async` 子节点;说明是主体开头 docstring 的字符串节点(`string` / `concatenated_string`);装饰器是 `decorated_definition` 里的 `decorator`;参数 `identifier`、`typed_parameter`(名字是第一个具名子节点)、`default_parameter`、`typed_default_parameter`、`*args` / `**kwargs`,`*` 和 `/` 分隔符不算参数。
- Python 类:`class_definition` 的 name;说明是主体开头 docstring 的字符串节点;装饰器同函数;泛型参数是 Python 3.12 `type_parameters` 里的每一项;父类是 `superclasses`(`argument_list`)里的每个具名子节点,照写的顺序,关键字参数(`metaclass=ABCMeta`)、`*bases`、`**kw` 也算;没有抽象、实现的接口。 - Python 类:`class_definition` 的 name;说明是主体开头 docstring 的字符串节点;装饰器同函数;泛型参数是 Python 3.12 `type_parameters` 里的每一项;父类是 `superclasses`(`argument_list`)里的每个具名子节点,照写的顺序,关键字参数(`metaclass=ABCMeta`)、`*bases`、`**kw` 也算;没有抽象、实现的接口。
- Python 导入:`import_statement` 每个 name 一个来源、导入项是整个模块;`import_from_statement` 的 module_name 和各 name(`aliased_import` 拆出 alias),`wildcard_import` 是全部;`future_import_statement` 的来源是 `__future__`。 - Python 导入:`import_statement` 每个 name 一个来源、导入项是整个模块;`import_from_statement` 的 module_name 和各 name(`aliased_import` 拆出 alias),`wildcard_import` 是全部;`future_import_statement` 的来源是 `__future__`。
- Python 变量:`assignment` 的 left / type / right。 - Python 变量:`assignment` 的 left / type / right;right 是 `dictionary` 时 entries 是它的具名子节点(注释除外):`pair` 的 key / value(value 又是 `dictionary` 时连它的 entries)、`dictionary_splat` 里的表达式。
## 中文模板直译 ## 中文模板直译
@@ -341,9 +363,9 @@ export interface CodeReaderApi {
2. 内置类型:只在类型位置翻(TS 的 `predefined_type`、Python 注解里的内置类型名)。 2. 内置类型:只在类型位置翻(TS 的 `predefined_type`、Python 注解里的内置类型名)。
3. 运算符:`===` 全等、`!==` 不全等、`==` 等于、`!=` 不等于、`&&` 并且、`||` 或者、`!` 非(Python 的 `not` / `and` / `or` 本身是关键字)。 3. 运算符:`===` 全等、`!==` 不全等、`==` 等于、`!=` 不等于、`&&` 并且、`||` 或者、`!` 非(Python 的 `not` / `and` / `or` 本身是关键字)。
同一套表和节点模板也用来翻属性表里的值:引擎能翻任意一个节点(只取它落在块头部里的那一段),参数类型、默认值、返回类型、变量类型和初始值都走这条路。 同一套表和节点模板也用来翻属性表里的值:引擎能翻任意一个节点(只取它落在块头部里的那一段),参数类型、默认值、返回类型、变量类型和初始值、字典 / 对象各项的键和值都走这条路。
其他规则:空数组字面量 → 空数组(Python 空列表 → 空列表、空字典 → 空字典);复合语句头部去掉结尾的 `{` 或 `:`;多行合成一行(连续空白压成一个空格,右括号前的尾逗号去掉);注释块给去掉注释符号的 Markdown 原文(见「注释的 Markdown 排版」)。关键字与前后内容之间保留源码里的空格(`!` 翻成「非」后补一个空格)。 其他规则:空数组字面量 → 空数组、空对象字面量 → 空对象(Python 空列表 → 空列表、空字典 → 空字典);复合语句头部去掉结尾的 `{` 或 `:`;多行合成一行(连续空白压成一个空格,右括号前的尾逗号去掉);注释块给去掉注释符号的 Markdown 原文(见「注释的 Markdown 排版」)。关键字与前后内容之间保留源码里的空格(`!` 翻成「非」后补一个空格)。
关键字按所在语法节点选择译法:同一个关键字在不同语法结构里意思不同时,表里写一个默认译法,再加若干「语境 → 译法」。语境是从关键字节点往上的节点类型链,外层在前:`raise_statement` 指父节点是 raise_statement,`for_statement > else_clause` 指父节点是 else_clause、祖父节点是 for_statement(关键字包在一层子句节点里时要看到祖父节点)。几条语境同时匹配时取最长的,都不匹配用默认译法。节点类型以 tree-sitter 语法为准。下面两张表里写成「(语境)译法 / (语境)译法」的关键字就是按这条规则区分的,排第一个的是默认译法。 关键字按所在语法节点选择译法:同一个关键字在不同语法结构里意思不同时,表里写一个默认译法,再加若干「语境 → 译法」。语境是从关键字节点往上的节点类型链,外层在前:`raise_statement` 指父节点是 raise_statement,`for_statement > else_clause` 指父节点是 else_clause、祖父节点是 for_statement(关键字包在一层子句节点里时要看到祖父节点)。几条语境同时匹配时取最长的,都不匹配用默认译法。节点类型以 tree-sitter 语法为准。下面两张表里写成「(语境)译法 / (语境)译法」的关键字就是按这条规则区分的,排第一个的是默认译法。
@@ -390,6 +412,10 @@ Python 内置类型(仅注解里):str 字符串、int 整数、float 浮
| `from .schemas import (⏎ UserAvatarUploadResponse, … )`(routes.py 19–23 行) | 来源 `.schemas` / 导入项 3 个:UserAvatarUploadResponse、UserDetailResponse、UserUpdateRequest | | `from .schemas import (⏎ UserAvatarUploadResponse, … )`(routes.py 19–23 行) | 来源 `.schemas` / 导入项 3 个:UserAvatarUploadResponse、UserDetailResponse、UserUpdateRequest |
| `payload = await file.read()`(routes.py,函数体里) | 名称 payload / 初始值 `等待 file.read()` | | `payload = await file.read()`(routes.py,函数体里) | 名称 payload / 初始值 `等待 file.read()` |
| `_default_port = 5680 if _role == "beat" else 5679` | 名称 _default_port / 访问级别 私有 / 初始值 `如果 _role 等于 "beat" 则 5680 否则 5679` | | `_default_port = 5680 if _role == "beat" else 5679` | 名称 _default_port / 访问级别 私有 / 初始值 `如果 _role 等于 "beat" 则 5680 否则 5679` |
| `_CONTEST_STATUS_KEYS = {⏎ "published": "contests:publish", "ended": "contests:end", "draft": "contests:update" }` | 名称 _CONTEST_STATUS_KEYS / 访问级别 私有 / 初始值 字典 3 项:`"published" \| "contests:publish"`、`"ended" \| "contests:end"`、`"draft" \| "contests:update"` |
| `CONFIG = {"db": {"host": host, "opts": {}}, **defaults, 1: await load()}` | 名称 CONFIG / 访问级别 公共 / 值 字典 3 项:`"db" \| 字典 2 项`(下一层 `"host" \| host`、`"opts" \| 空字典`)、`展开 \| defaults`、`1 \| 等待 load()` |
| `const result: EnvLockResult = { total: files.length, ok: [], failed: [] }`(envLock.ts 40 行) | 名称 result / 类型 EnvLockResult / 初始值 对象 3 项:`total \| files.length`、`ok \| 空数组`、`failed \| 空数组` |
| `export const handlers = { total: files.length, 'x-y': await load(), [key]: !ok, files, ...rest, async load(id: string): Promise<void> { … }, nested: { a: [], b: { c: 1 } } }` | 名称 handlers / 访问级别 公共 / 值 对象 7 项:`total \| files.length`、`'x-y' \| 等待 load()`、`[key] \| 非 ok`、`files \| files`、`展开 \| rest`、`load \| 异步 load(id: 字符串): Promise<无>`、`nested \| 对象 2 项`(下一层 `a \| 空数组`、`b \| 对象 1 项`,再下一层 `c \| 1`) |
| `export abstract class Repository<K, T extends Entity = Entity> extends Base<string> implements Loader<T>, Disposable {`(classes.ts 3–9 行,连同前面的 JSDoc 和 `@Injectable()`) | 名称 Repository / 说明 `仓库基类。…` / 访问级别 公共 / 抽象 是 / 装饰器 `@Injectable()` / 类型参数 `K, T 限定为 Entity = Entity` / 父类 `Base<字符串>` / 实现的接口 `Loader<T>, Disposable` | | `export abstract class Repository<K, T extends Entity = Entity> extends Base<string> implements Loader<T>, Disposable {`(classes.ts 3–9 行,连同前面的 JSDoc 和 `@Injectable()`) | 名称 Repository / 说明 `仓库基类。…` / 访问级别 公共 / 抽象 是 / 装饰器 `@Injectable()` / 类型参数 `K, T 限定为 Entity = Entity` / 父类 `Base<字符串>` / 实现的接口 `Loader<T>, Disposable` |
| `export default class {` | 名称 匿名 / 访问级别 公共 / 抽象 否 | | `export default class {` | 名称 匿名 / 访问级别 公共 / 抽象 否 |
| `class Meta(Base, metaclass=ABCMeta):`(classes.py,带 `@dataclass(frozen=True)`) | 名称 Meta / 访问级别 公共 / 装饰器 `@dataclass(frozen=True)` / 父类 `Base, metaclass=ABCMeta` | | `class Meta(Base, metaclass=ABCMeta):`(classes.py,带 `@dataclass(frozen=True)`) | 名称 Meta / 访问级别 公共 / 装饰器 `@dataclass(frozen=True)` / 父类 `Base, metaclass=ABCMeta` |
@@ -425,7 +451,7 @@ type BlockView = BlockViewBase & ({ translation: PreviewText } | { properties: P
- `{ type: 'ready' }` - `{ type: 'ready' }`
- `{ type: 'edit', startLine, endLine, text }`:用 `WorkspaceEdit` 把这几行整行替换成 `text`(每行补回原来第一行的缩进)。 - `{ type: 'edit', startLine, endLine, text }`:用 `WorkspaceEdit` 把这几行整行替换成 `text`(每行补回原来第一行的缩进)。
- `{ type: 'selectTranslator', id }`:按 `languageId` 记到 `globalState`,重发文档。 - `{ type: 'selectTranslator', id }`:按 `languageId` 记到 `globalState`,重发文档。
- 块的编辑状态和用户的折叠选择在 webview 里按块 id 记(`vscode.setState`):`{ editing: string[], folds: Record<id, boolean>, scroll? }`。`folds` 只记用户点过的(id → 折叠着没有),属性行也记在里面,id 是 `<块 id>/p<行序号>`(例如 `6.1.0/p3`);没记的按默认:函数、类块折叠,其余块和属性行展开。眼下折叠着哪些由「默认 + folds」算出来,界面、跳到定义的展开都只看这一份。跳到定义展开祖先块、属性行时也记进 `folds`(记成展开)。读回来的状态逐项核对,旧版本存的、形状不对的项当没存过。文档刷新后保留;正在编辑且有焦点的块,刷新时不覆盖输入框内容。 - 块的编辑状态和用户的折叠选择在 webview 里按块 id 记(`vscode.setState`):`{ editing: string[], folds: Record<id, boolean>, scroll? }`。`folds` 只记用户点过的(id → 折叠着没有),属性行和带下一层的子行也记在里面:属性行 id 是 `<块 id>/p<行序号>`(例如 `6.1.0/p3`),子行 id 是上一层(行或子行)的 id 加 `/<子行序号>`,一层层往后接(例如 `6.1.0/p3/1` 是第 3 行的第 1 个子行,`6.1.0/p3/1/0` 是它下面的第 0 个);块 id 只有数字和点,只有行那一节带 `p`、其余各节是纯数字,所以块、行、各层子行的 id 互不相撞。子行 id 也是跳到定义时在页面上找要闪的那一项用的。没记的按默认:函数、类块折叠,其余块、属性行和子行展开。眼下折叠着哪些由「默认 + folds」算出来,界面、跳到定义的展开都只看这一份。跳到定义展开祖先块、属性行时也记进 `folds`(记成展开)。读回来的状态逐项核对,旧版本存的、形状不对的项当没存过。文档刷新后保留;正在编辑且有焦点的块,刷新时不覆盖输入框内容。
## 颜色 ## 颜色
@@ -444,7 +470,7 @@ type BlockView = BlockViewBase & ({ translation: PreviewText } | { properties: P
| hint | `--vscode-descriptionForeground` | 同左 | | hint | `--vscode-descriptionForeground` | 同左 |
| plain | `--vscode-editor-foreground` | 同左 | | plain | `--vscode-editor-foreground` | 同左 |
属性表里:说明按 comment,访问级别(公共 / 私有 / 受保护)和「作为」按 keyword,是 / 否按 storage,函数名称按 function,类名称按 type,未标注、无、N 个、默认、默认导出、全部、整个模块、匿名按 hint。父类名(TypeScript `extends` 后面、JavaScript `class_heritage` 里、Python 类名后面括号里直接写的名字)源码着色和属性表里都按 type。 属性表里:说明按 comment,访问级别(公共 / 私有 / 受保护)和「作为」按 keyword,是 / 否按 storage,函数名称按 function,类名称按 type,未标注、无、N 个、字典 N 项、对象 N 项、展开、默认、默认导出、全部、整个模块、匿名按 hint。父类名(TypeScript `extends` 后面、JavaScript `class_heritage` 里、Python 类名后面括号里直接写的名字)源码着色和属性表里都按 type。
种类(标签字色 + 标签底 18% + 块底 7% + 边框 35%;亮色主题同色系):import 导入 #c586c0;function 函数 #dcdcaa;class 类、type 类型 #4ec9b0;field 字段、const 常量、variable 变量 #9cdcfe;if 条件、else 否则 #ce9178;loop 循环 #8f9cff;try 尝试、catch 捕获、finally 最终 #f48771;return 返回 #89d185;statement 语句 #9d9d9d;comment 注释 #6a9955(虚线框)。编辑状态:底色 `--vscode-editor-background`,边框 `--vscode-focusBorder`。 种类(标签字色 + 标签底 18% + 块底 7% + 边框 35%;亮色主题同色系):import 导入 #c586c0;function 函数 #dcdcaa;class 类、type 类型 #4ec9b0;field 字段、const 常量、variable 变量 #9cdcfe;if 条件、else 否则 #ce9178;loop 循环 #8f9cff;try 尝试、catch 捕获、finally 最终 #f48771;return 返回 #89d185;statement 语句 #9d9d9d;comment 注释 #6a9955(虚线框)。编辑状态:底色 `--vscode-editor-background`,边框 `--vscode-focusBorder`。
@@ -479,8 +505,9 @@ vscode/webview-ui/code-reader.html + src/codeReader/ 预览界面(React,Vit
- 语言插件:`envLock.ts`、`issueLock.ts` 的代码块树(种类、header 行号、层级)与原型 `CODE_TREE` 一致,只是紧贴函数的 JSDoc 算进了函数头部;Python 样例覆盖 import / def / docstring / if-elif-else / for / try-except / with / 推导式;函数的说明:JSDoc 各种挂法(export、默认导出、方法隔着装饰器、函数值的变量、嵌套函数)和不挂的情况,docstring 连同它前面的注释进头部。类:`classes.ts`、`classes.py` 覆盖 export + 抽象 + 泛型 + extends / implements + 装饰器 + JSDoc、匿名默认导出、赋给变量 / 类字段的类表达式、函数里的类、类里的类、docstring、metaclass 关键字参数、下划线私有。 - 语言插件:`envLock.ts`、`issueLock.ts` 的代码块树(种类、header 行号、层级)与原型 `CODE_TREE` 一致,只是紧贴函数的 JSDoc 算进了函数头部;Python 样例覆盖 import / def / docstring / if-elif-else / for / try-except / with / 推导式;函数的说明:JSDoc 各种挂法(export、默认导出、方法隔着装饰器、函数值的变量、嵌套函数)和不挂的情况,docstring 连同它前面的注释进头部。类:`classes.ts`、`classes.py` 覆盖 export + 抽象 + 泛型 + extends / implements + 装饰器 + JSDoc、匿名默认导出、赋给变量 / 类字段的类表达式、函数里的类、类里的类、docstring、metaclass 关键字参数、下划线私有。
- 块头部:各样例里块的头部行互不重叠;函数、类说明所在的行在函数、类块里,「编辑」写回原样不变。 - 块头部:各样例里块的头部行互不重叠;函数、类说明所在的行在函数、类块里,「编辑」写回原样不变。
- 折叠:默认函数、类块折叠、其余展开;用户的选择盖过默认、重新加载后还在;旧版本存的状态不报错;跳到定义时默认折叠的函数、类也会展开。 - 折叠:默认函数、类块折叠、其余展开;用户的选择盖过默认、重新加载后还在(嵌套子行的也是);各层子行的 id 互不相撞;旧版本存的状态不报错;跳到定义时默认折叠的函数、类也会展开。
- 属性表信息:`envLock.ts`、`sample.py`、`routes.py`(原型里的 FastAPI 路由)加内联片段,覆盖各种导入写法、形参写法、类成员访问修饰符、解构和多变量声明、Python 下划线规则。 - 属性表信息:`envLock.ts`、`sample.py`、`routes.py`(原型里的 FastAPI 路由)加内联片段,覆盖各种导入写法、形参写法、类成员访问修饰符、解构和多变量声明、Python 下划线规则;字典 / 对象字面量的各项:Python 字符串键、名字键、嵌套字典、`**` 展开、空字典、类属性,TypeScript 名字键、字符串键、计算属性名、简写、展开、方法和 getter(头部到主体之前)、嵌套对象、空对象,不是字面量的值没有 entries。
- 字典 / 对象的属性表:项数、键 | 值两列、嵌套一层层展开、「展开」、方法头部、空的一行;键和值里的名字、字符串带下标(嵌套的也是);每项标的定义是键,展开项不标;跳到定义落在对象属性名上只闪那一项、连外层各层一起展开,只是显示着的名字闪整块、展开到它那一层。
- 翻译:上面两张样例表逐字一致;属性名不被翻;每个被翻的词单独成段;节点模板嵌套时正确、槽位里照样翻、没命中模板的节点照抄;属性表里的值和一行译文用同一套表和模板。 - 翻译:上面两张样例表逐字一致;属性名不被翻;每个被翻的词单独成段;节点模板嵌套时正确、槽位里照样翻、没命中模板的节点照抄;属性表里的值和一行译文用同一套表和模板。
- `blockView`:`codeTokens` 拼起来等于 `code`;去缩进正确;函数、类、导入、常量、变量块只带属性表,其余块只带译文。 - `blockView`:`codeTokens` 拼起来等于 `code`;去缩进正确;函数、类、导入、常量、变量块只带属性表,其余块只带译文。
- 注释的 Markdown:注释块、函数和类「说明」的视图数据是 `{ markdown }`,原文照抄(reST 角色、双反引号、JSDoc 标签、HTML 都不转换),行注释连成的块留得住缩进四格的代码块;渲染时 HTML 当文字、带协议的网址和邮箱成链接、不带协议的文件名不成链接,`javascript:` / `command:` / 相对路径的链接和图片不渲染;跳到定义不认 Markdown 里的名字。 - 注释的 Markdown:注释块、函数和类「说明」的视图数据是 `{ markdown }`,原文照抄(reST 角色、双反引号、JSDoc 标签、HTML 都不转换),行注释连成的块留得住缩进四格的代码块;渲染时 HTML 当文字、带协议的网址和邮箱成链接、不带协议的文件名不成链接,`javascript:` / `command:` / 相对路径的链接和图片不渲染;跳到定义不认 Markdown 里的名字。
+16 -4
View File
@@ -85,11 +85,19 @@ export interface ImportFacts {
items: ImportItem[] items: ImportItem[]
} }
/** 字典 / 对象字面量里的一项,照源码顺序;注释不算。 */
export type EntryFacts
= | { kind: 'pair', key: Node, value: Node, entries?: EntryFacts[] } // 'a': 1、a: 1、[k]: v;TypeScript 的简写 { a } 键和值是同一个节点;entries 同 DeclaratorFacts.entries
| { kind: 'spread', value: Node } // **other、...other:value 是展开的那个表达式
| { kind: 'method', name: Node, node: Node, header: SourceSpan } // 对象字面量里的方法、getter、setter:node 是整个方法,header 同 BlockShape.header,只到主体开始之前
export interface DeclaratorFacts { export interface DeclaratorFacts {
/** 变量名;解构声明是整个模式。 */ /** 变量名;解构声明是整个模式。 */
name: Node name: Node
type?: Node type?: Node
value?: Node value?: Node
/** 值是字典 / 对象字面量(Python 的 dictionary、TypeScript 的 object)时是它的各项,空字面量是空数组;别的值没有。 */
entries?: EntryFacts[]
} }
export interface VariableFacts { export interface VariableFacts {
@@ -152,14 +160,18 @@ export type PreviewText = Segment[] | Markdown
/** /**
* 属性表里的一行或一个子行讲的是哪个定义:定义出来的那个名字的节点(取自 facts,例如参数名、导入项的名称和别名、 * 属性表里的一行或一个子行讲的是哪个定义:定义出来的那个名字的节点(取自 facts,例如参数名、导入项的名称和别名、
* 声明的变量名、类的泛型参数)在源码里的范围。跳到定义时,定义处落在这个范围里就只闪这一行;不填(第三方翻译插件可以不填)就闪整块。 * 声明的变量名、类的泛型参数、字典 / 对象字面量一项的键)在源码里的范围。跳到定义时,定义处落在这个范围里就只闪这一行;不填(第三方翻译插件可以不填)就闪整块。
*/ */
export interface Defines { defines?: SourceRange } export interface Defines { defines?: SourceRange }
/** 属性表里展开的一个子行:几列对齐的格子,例如参数的名称 / 类型 / 默认值。 */ /** 属性表里展开的一个子行:几列对齐的格子,例如参数的名称 / 类型 / 默认值、字典一项的键 / 值。 */
export interface PropertyItem extends Defines { cells: Segment[][] } export interface PropertyItem extends Defines {
cells: Segment[][]
/** 展开在这个子行下面的下一层子行(字典 / 对象的值又是字典 / 对象时,它的各项);一层层可以一直往下套。 */
items?: PropertyItem[]
}
/** 属性表的一行:左列属性名,右列着色的值或 Markdown 说明;items 是展开在这一行下面的子行(输入参数、导入项)。 */ /** 属性表的一行:左列属性名,右列着色的值或 Markdown 说明;items 是展开在这一行下面的子行(输入参数、导入项、字典 / 对象的各项)。 */
export interface PropertyRow extends Defines { export interface PropertyRow extends Defines {
label: string label: string
value: PreviewText value: PreviewText
@@ -236,6 +236,35 @@ describe('python 属性表信息(facts)', () => {
expect(found[9]).toMatchObject({ name: '_helper', access: 'private' }) expect(found[9]).toMatchObject({ name: '_helper', access: 'private' })
expect(found[10]).not.toHaveProperty('access') expect(found[10]).not.toHaveProperty('access')
}) })
it('字典字面量的各项:字符串键、名字键、嵌套字典连它的各项、**展开;空字典是空数组,别的值没有;类属性也有', async () => {
const source = [
'_KEYS = {',
' "published": "contests:publish",',
' key: 1,',
' # 注释不算一项',
' "nested": {"a": {}, "b": [1]},',
' **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]' },
] },
{ 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' }] }] })
})
}) })
describe('python 类', () => { describe('python 类', () => {
+20 -2
View File
@@ -1,4 +1,4 @@
import type { Access, BlockShape, ClassFacts, CodeBlock, FunctionFacts, ImportFacts, ImportItem, LanguageAdapter, Node, ParameterFacts, TextKind, TokenRole } from '../contract' import type { Access, BlockShape, ClassFacts, CodeBlock, EntryFacts, FunctionFacts, ImportFacts, ImportItem, LanguageAdapter, Node, ParameterFacts, TextKind, TokenRole } from '../contract'
import type { CommentRules } from './shared' import type { CommentRules } from './shared'
import * as path from 'node:path' import * as path from 'node:path'
import { children, compoundShape, leafBlock, leafShape, listBlocks, named, sameNode, syntaxChildren } from './shared' import { children, compoundShape, leafBlock, leafShape, listBlocks, named, sameNode, syntaxChildren } from './shared'
@@ -230,16 +230,34 @@ function expressionBlock(node: Node, scope: Scope, docstring: boolean): CodeBloc
const type = expression.childForFieldName('type') const type = expression.childForFieldName('type')
if (scope === 'class' && type) if (scope === 'class' && type)
return leafBlock('field', node) return leafBlock('field', node)
const value = expression.childForFieldName('right')
return { return {
kind: scope === 'module' && isConstantTarget(left) ? 'const' : 'variable', kind: scope === 'module' && isConstantTarget(left) ? 'const' : 'variable',
...leafShape(node), ...leafShape(node),
facts: { facts: {
access: scopeAccess(left, scope), access: scopeAccess(left, scope),
declarators: [{ name: left, type: type ?? undefined, value: expression.childForFieldName('right') ?? undefined }], declarators: [{ name: left, type: type ?? undefined, value: value ?? undefined, entries: dictionaryEntries(value) }],
}, },
} }
} }
/** 字典字面量的各项:`键: 值`(值又是字典时连它的各项)、`**other`;值不是字典字面量时没有。 */
function dictionaryEntries(value: Node | null): EntryFacts[] | undefined {
if (value?.type !== 'dictionary')
return undefined
return syntaxChildren(value).flatMap((item): EntryFacts[] => {
if (item.type === 'dictionary_splat') {
const [spread] = syntaxChildren(item)
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) }]
})
}
/** elif / else 展平成紧跟 if 的兄弟块。 */ /** elif / else 展平成紧跟 if 的兄弟块。 */
function ifBlocks(node: Node): CodeBlock[] { function ifBlocks(node: Node): CodeBlock[] {
const blocks = [bodyBlock('if', node, bodyOf(node), 'other')] const blocks = [bodyBlock('if', node, bodyOf(node), 'other')]
@@ -325,7 +325,58 @@ describe('typeScript 属性表信息(facts)', () => {
}) })
expect(env[24]).toEqual({ declarators: [{ name: 'folder', value: 'workspace.workspaceFolders?.find(f => f.uri.fsPath === workspaceRoot)' }] }) expect(env[24]).toEqual({ declarators: [{ name: 'folder', value: 'workspace.workspaceFolders?.find(f => f.uri.fsPath === workspaceRoot)' }] })
expect(env[38]).toMatchObject({ name: 'chmodAll', access: 'private', async: true }) expect(env[38]).toMatchObject({ name: 'chmodAll', access: 'private', async: true })
expect(env[40]).toEqual({ declarators: [{ name: 'result', type: 'EnvLockResult', value: '{ total: files.length, ok: [], failed: [] }' }] }) expect(env[40]).toEqual({ declarators: [{
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('导入:默认导入、* as、仅类型、副作用导入、require', async () => { it('导入:默认导入、* as、仅类型、副作用导入、require', async () => {
+33 -3
View File
@@ -1,7 +1,7 @@
import type { Access, BaseFacts, BlockShape, ClassFacts, CodeBlock, DeclaratorFacts, FunctionFacts, ImportFacts, ImportItem, LanguageAdapter, Node, ParameterFacts, TextKind, TokenRole } from '../contract' import type { Access, BaseFacts, BlockShape, ClassFacts, CodeBlock, DeclaratorFacts, EntryFacts, FunctionFacts, ImportFacts, ImportItem, LanguageAdapter, Node, ParameterFacts, TextKind, TokenRole } from '../contract'
import type { CommentRules } from './shared' import type { CommentRules } from './shared'
import * as path from 'node:path' import * as path from 'node:path'
import { children, compoundShape, extendHeader, leafBlock, leafShape, listBlocks, named, sameNode, syntaxChildren } from './shared' import { children, compoundShape, extendHeader, headerSpan, leafBlock, leafShape, listBlocks, named, nodeSpan, sameNode, syntaxChildren } from './shared'
const GRAMMARS: Record<string, string> = { const GRAMMARS: Record<string, string> = {
typescript: 'tree-sitter-typescript.wasm', typescript: 'tree-sitter-typescript.wasm',
@@ -205,13 +205,43 @@ function parameterFacts(param: Node): ParameterFacts {
} }
function declaratorFacts(declarator: Node): DeclaratorFacts { function declaratorFacts(declarator: Node): DeclaratorFacts {
const value = declarator.childForFieldName('value')
return { return {
name: declarator.childForFieldName('name') ?? declarator, name: declarator.childForFieldName('name') ?? declarator,
type: annotatedType(declarator.childForFieldName('type')), type: annotatedType(declarator.childForFieldName('type')),
value: declarator.childForFieldName('value') ?? undefined, value: value ?? undefined,
entries: objectEntries(value),
} }
} }
/** 对象字面量的各项:`键: 值`(值又是对象时连它的各项)、简写 `{ a }`、`...other`、方法;值不是对象字面量时没有。 */
function objectEntries(value: Node | null): EntryFacts[] | undefined {
if (value?.type !== 'object')
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 []
}
})
}
/** import 语句:默认导入、`* as x`、`{ a as b }`、`import x = require('m')`;`import type` 只导入类型。 */ /** import 语句:默认导入、`* as x`、`{ a as b }`、`import x = require('m')`;`import type` 只导入类型。 */
function importFacts(node: Node): ImportFacts { function importFacts(node: Node): ImportFacts {
const require = named(node).find(child => child.type === 'import_require_clause') const require = named(node).find(child => child.type === 'import_require_clause')
+6 -5
View File
@@ -1,4 +1,4 @@
import type { BlockKind, CodeBlock, LanguageAdapter, PreviewText, PropertyRow, Segment } from './contract' import type { BlockKind, CodeBlock, LanguageAdapter, PreviewText, PropertyItem, PropertyRow, Segment } from './contract'
import { readFileSync } from 'node:fs' import { readFileSync } from 'node:fs'
import * as path from 'node:path' import * as path from 'node:path'
import { Node } from '@vscode/tree-sitter-wasm' import { Node } from '@vscode/tree-sitter-wasm'
@@ -54,12 +54,13 @@ export function previewText(text: PreviewText): string {
return 'markdown' in text ? text.markdown : segmentText(text) return 'markdown' in text ? text.markdown : segmentText(text)
} }
/** 属性表写成文字:每行 `名称: 值`,展开的子行缩进两格、各列用 ` | ` 隔开。 */ /** 属性表写成文字:每行 `名称: 值`,展开的子行比上一层多缩进两格、各列用 ` | ` 隔开。 */
export function propertyLines(rows: PropertyRow[]): string[] { export function propertyLines(rows: PropertyRow[]): string[] {
return rows.flatMap(row => [ const itemLines = (items: PropertyItem[] | undefined, indent: string): string[] => (items ?? []).flatMap(item => [
`${row.label}: ${previewText(row.value)}`, `${indent}${item.cells.map(segmentText).join(' | ')}`,
...(row.items ?? []).map(item => ` ${item.cells.map(segmentText).join(' | ')}`), ...itemLines(item.items, `${indent} `),
]) ])
return rows.flatMap(row => [`${row.label}: ${previewText(row.value)}`, ...itemLines(row.items, ' ')])
} }
/** 语法树节点换成它的源码,没填的可选字段去掉。 */ /** 语法树节点换成它的源码,没填的可选字段去掉。 */
@@ -80,6 +80,7 @@ export const pythonZhTranslator: Translator = createZhTemplateTranslator({
operators: { '==': '等于', '!=': '不等于' }, operators: { '==': '等于', '!=': '不等于' },
operatorParents: ['comparison_operator'], operatorParents: ['comparison_operator'],
emptyLiterals: { list: '空列表', dictionary: '空字典' }, emptyLiterals: { list: '空列表', dictionary: '空字典' },
mapLiterals: { dictionary: '字典' },
stringNodes: ['string'], stringNodes: ['string'],
commentText, commentText,
// `a if c else b` 的三个具名子节点依次是 a、c、b,语法没给它们起字段名 // `a if c else b` 的三个具名子节点依次是 a、c、b,语法没给它们起字段名
@@ -85,7 +85,8 @@ export const typescriptZhTranslator: Translator = createZhTemplateTranslator({
isTypePosition: leaf => leaf.parent?.type === 'predefined_type', isTypePosition: leaf => leaf.parent?.type === 'predefined_type',
operators: { '===': '全等', '!==': '不全等', '==': '等于', '!=': '不等于', '&&': '并且', '||': '或者', '!': '非' }, operators: { '===': '全等', '!==': '不全等', '==': '等于', '!=': '不等于', '&&': '并且', '||': '或者', '!': '非' },
operatorParents: ['binary_expression', 'unary_expression'], operatorParents: ['binary_expression', 'unary_expression'],
emptyLiterals: { array: '空数组' }, emptyLiterals: { array: '空数组', object: '空对象' },
mapLiterals: { object: '对象' },
stringNodes: ['string', 'template_string', 'regex'], stringNodes: ['string', 'template_string', 'regex'],
commentText, commentText,
nodeTemplates: { nodeTemplates: {
@@ -1,4 +1,4 @@
import type { LanguageAdapter, PropertyRow, Segment, Translator } from '../contract' import type { LanguageAdapter, PropertyItem, PropertyRow, Segment, Translator } from '../contract'
import type { ZhTable } from './zhTemplate' import type { ZhTable } from './zhTemplate'
import { describe, expect, it } from 'vitest' import { describe, expect, it } from 'vitest'
import { createPythonAdapter } from '../languages/python' import { createPythonAdapter } from '../languages/python'
@@ -362,7 +362,7 @@ describe('属性表', () => {
expect(env[15]).toEqual(['名称: ENV_PATTERN', '访问级别: 私有', '值: \'**/.env*\'']) expect(env[15]).toEqual(['名称: ENV_PATTERN', '访问级别: 私有', '值: \'**/.env*\''])
expect(env[27]).toEqual(['名称: include', '初始值: 新建 RelativePattern(folder, ENV_PATTERN)']) expect(env[27]).toEqual(['名称: include', '初始值: 新建 RelativePattern(folder, ENV_PATTERN)'])
expect(env[28]).toEqual(['名称: uris', '初始值: 等待 workspace.findFiles(include, EXCLUDE_PATTERN)']) expect(env[28]).toEqual(['名称: uris', '初始值: 等待 workspace.findFiles(include, EXCLUDE_PATTERN)'])
expect(env[40]).toEqual(['名称: result', '类型: EnvLockResult', '初始值: { total: files.length, ok: 空数组, failed: 空数组 }']) expect(env[40]).toEqual(['名称: result', '类型: EnvLockResult', '初始值: 对象 3 项', ' total | files.length', ' ok | 空数组', ' failed | 空数组'])
expect(issue[1]).toEqual(['来源: \'../gitea/types\'', '仅类型: 是', '导入项: 1 个', ' Issue']) expect(issue[1]).toEqual(['来源: \'../gitea/types\'', '仅类型: 是', '导入项: 1 个', ' Issue'])
expect(issue[14]).toEqual(['名称: issue', '初始值: issues.find(i => i.number 全等 issueNumber)']) expect(issue[14]).toEqual(['名称: issue', '初始值: issues.find(i => i.number 全等 issueNumber)'])
expect(routes[19]).toEqual([ expect(routes[19]).toEqual([
@@ -620,6 +620,86 @@ describe('属性表', () => {
expect(rows[1]).toEqual(['名称: i', '访问级别: 私有', '初始值: 0', '名称: n', '访问级别: 私有', '类型: 数值', '初始值: 无']) expect(rows[1]).toEqual(['名称: i', '访问级别: 私有', '初始值: 0', '名称: n', '访问级别: 私有', '类型: 数值', '初始值: 无'])
expect(rows[3]).toEqual(['名称: v', '初始值: useMemo(() =>']) expect(rows[3]).toEqual(['名称: v', '初始值: useMemo(() =>'])
}) })
it('字典:值写「字典 N 项」,每项一个子行「键 | 值」,嵌套的字典一层层往下展开;**展开的键写「展开」,空字典照旧一行', async () => {
const source = [
'_CONTEST_STATUS_KEYS = {',
' "published": "contests:publish",',
' "ended": "contests:end",',
' "draft": "contests:update",',
'}',
'CONFIG = {"db": {"host": host, "opts": {}}, **defaults, 1: await load()}',
'EMPTY = {}',
].join('\n')
const rows = rowTexts(await describeAll(py, 'python', pythonZhTranslator, source))
expect(rows[1]).toEqual([
'名称: _CONTEST_STATUS_KEYS',
'访问级别: 私有',
'初始值: 字典 3 项',
' "published" | "contests:publish"',
' "ended" | "contests:end"',
' "draft" | "contests:update"',
])
expect(rows[6]).toEqual([
'名称: CONFIG',
'访问级别: 公共',
'值: 字典 3 项',
' "db" | 字典 2 项',
' "host" | host',
' "opts" | 空字典',
' 展开 | defaults',
' 1 | 等待 load()',
])
expect(rows[7]).toEqual(['名称: EMPTY', '访问级别: 公共', '值: 空字典'])
})
it('对象:值写「对象 N 项」,键照源码写法(计算属性名照常翻),简写的值就是它自己,方法的值是它的头部,嵌套的一层层往下展开;空对象一行「空对象」', async () => {
const source = [
'export const handlers = {',
' total: files.length,',
' \'x-y\': await load(),',
' [key]: !ok,',
' files,',
' ...rest,',
' async load(id: string): Promise<void> {',
' await run(id)',
' },',
' nested: { a: [], b: { c: 1 } },',
'}',
'let none = {}',
].join('\n')
const described = await describeAll(ts, 'typescript', typescriptZhTranslator, source)
const rows = rowTexts(described)
expect(rows[1]).toEqual([
'名称: handlers',
'访问级别: 公共',
'值: 对象 7 项',
' total | files.length',
' \'x-y\' | 等待 load()',
' [key] | 非 ok',
' files | files',
' 展开 | rest',
' load | 异步 load(id: 字符串): Promise<无>',
' nested | 对象 2 项',
' a | 空数组',
' b | 对象 1 项',
' c | 1',
])
expect(rows[12]).toEqual(['名称: none', '访问级别: 私有', '初始值: 空对象'])
const value = colors(described.get(1) ?? []).find(row => row.label === '值')
expect(value?.value).toEqual([{ text: '对象 7 项', role: 'hint' }])
expect(value?.items?.map(item => item.cells[0])).toEqual([
[{ text: 'total', role: 'variable' }],
[{ text: '\'x-y\'', role: 'string' }],
[{ text: '[', role: 'plain' }, { text: 'key', role: 'variable' }, { text: ']', role: 'plain' }],
[{ text: 'files', role: 'variable' }],
[{ text: '展开', role: 'hint' }],
[{ text: 'load', role: 'function' }],
[{ text: 'nested', role: 'variable' }],
])
expect(value?.items?.[6].cells[1]).toEqual([{ text: '对象 2 项', role: 'hint' }])
})
}) })
describe('源码下标(跳到定义用)', () => { describe('源码下标(跳到定义用)', () => {
@@ -633,7 +713,8 @@ describe('源码下标(跳到定义用)', () => {
} }
function propertySegments(rows: PropertyRow[]): Segment[] { function propertySegments(rows: PropertyRow[]): Segment[] {
return rows.flatMap(row => [...('markdown' in row.value ? [] : row.value), ...(row.items ?? []).flatMap(item => item.cells.flat())]) const itemSegments = (items: PropertyItem[] | undefined): Segment[] => (items ?? []).flatMap(item => [...item.cells.flat(), ...itemSegments(item.items)])
return rows.flatMap(row => [...('markdown' in row.value ? [] : row.value), ...itemSegments(row.items)])
} }
it('python:点分模块名每一节、导入项、别名各自带下标,点号不带;前面有中文注释也对得上', async () => { it('python:点分模块名每一节、导入项、别名各自带下标,点号不带;前面有中文注释也对得上', async () => {
@@ -723,6 +804,37 @@ describe('源码下标(跳到定义用)', () => {
expect(marked(tsSource, tsRows.get(3))).toEqual(['名称: i', '名称: j']) expect(marked(tsSource, tsRows.get(3))).toEqual(['名称: i', '名称: j'])
}) })
it('字典 / 对象字面量:键和值里的名字、字符串带下标,嵌套的也是;每项标出它讲的定义是键,展开没有', async () => {
const source = [
'const o = {',
' total: files.length,',
' "k": v,',
' inner: { deep: x },',
' ...rest,',
' load() {},',
'}',
].join('\n')
const at = (word: string): number => source.indexOf(word)
const items = (await describeAll(ts, 'typescript', typescriptZhTranslator, source)).get(1)?.find(row => row.label === '值')?.items ?? []
expect(items.map(item => item.cells.map(linked))).toEqual([
[[['total', at('total')]], [['files', at('files')], ['length', at('length')]]],
[[['"k"', at('"k"')]], [['v', at('v,')]]],
[[['inner', at('inner')]], []],
[[], [['rest', at('rest')]]],
[[['load', at('load')]], [['load', at('load')]]],
])
expect(items[2].items?.map(item => item.cells.map(linked))).toEqual([[[['deep', at('deep')]], [['x', at('x }')]]]])
expect(items.map(item => item.defines && source.slice(item.defines.startIndex, item.defines.endIndex))).toEqual(['total', '"k"', 'inner', undefined, 'load'])
expect(items[2].items?.[0].defines).toEqual({ startIndex: at('deep'), endIndex: at('deep') + 'deep'.length })
const pySource = 'X = {"a": b, **c}'
const pyItems = (await describeAll(py, 'python', pythonZhTranslator, pySource)).get(1)?.find(row => row.label === '值')?.items ?? []
expect(pyItems.map(item => item.cells.map(linked))).toEqual([
[[['"a"', pySource.indexOf('"a"')]], [['b', pySource.indexOf('b')]]],
[[], [['c', pySource.indexOf('c')]]],
])
})
it('类:名称、类型参数、父类、实现的接口里的名字带下标;只有「类型参数」一行标出它讲的定义(这几个泛型参数),名称是块自己的名字、不标', async () => { it('类:名称、类型参数、父类、实现的接口里的名字带下标;只有「类型参数」一行标出它讲的定义(这几个泛型参数),名称是块自己的名字、不标', async () => {
const source = 'export class Repo<K, T extends Entity> extends Base<T> implements Loader<K>, Disposable {}' const source = 'export class Repo<K, T extends Entity> extends Base<T> implements Loader<K>, Disposable {}'
const at = (word: string): number => source.indexOf(word) const at = (word: string): number => source.indexOf(word)
@@ -789,6 +901,7 @@ describe('中文模板引擎', () => {
operators: {}, operators: {},
operatorParents: [], operatorParents: [],
emptyLiterals: {}, emptyLiterals: {},
mapLiterals: {},
stringNodes: [], stringNodes: [],
commentText: node => node.text, commentText: node => node.text,
nodeTemplates: {}, nodeTemplates: {},
+48 -15
View File
@@ -1,4 +1,4 @@
import type { Access, ClassFacts, Defines, FactBlock, FunctionFacts, ImportFacts, ImportItem, Markdown, Node, PreviewText, PropertyRow, Segment, SourceSpan, TextBlock, TokenRole, Translator, VariableBlock } from '../contract' import type { Access, ClassFacts, Defines, EntryFacts, FactBlock, FunctionFacts, ImportFacts, ImportItem, Markdown, Node, PreviewText, PropertyItem, PropertyRow, Segment, SourceSpan, TextBlock, TokenRole, Translator, VariableBlock } from '../contract'
import { commonIndent, splitLines } from '../editText' import { commonIndent, splitLines } from '../editText'
/** 按所在语法节点区分译法的关键字,例如 Python 的 else:if 后「否则」,for / while 后「未中断时」。 */ /** 按所在语法节点区分译法的关键字,例如 Python 的 else:if 后「否则」,for / while 后「未中断时」。 */
@@ -36,8 +36,10 @@ export interface ZhTable {
/** 翻成文字的运算符:只认父节点在 operatorParents 里的运算符节点(TS 的非空断言 `x!` 不算)。 */ /** 翻成文字的运算符:只认父节点在 operatorParents 里的运算符节点(TS 的非空断言 `x!` 不算)。 */
operators: Record<string, string> operators: Record<string, string>
operatorParents: string[] operatorParents: string[]
/** 空字面量(没有具名子节点)整个换成一个词:空数组 / 空列表 / 空字典。 */ /** 空字面量(没有具名子节点)整个换成一个词:空数组 / 空列表 / 空字典 / 空对象。 */
emptyLiterals: Record<string, string> emptyLiterals: Record<string, string>
/** 字典 / 对象字面量(语言插件给了 entries 的值)的叫法,按节点类型查:属性表里的值写成「字典 3 项」,各项展开成子行。 */
mapLiterals: Record<string, string>
/** 整段原样照抄、按字符串着色的节点。 */ /** 整段原样照抄、按字符串着色的节点。 */
stringNodes: string[] stringNodes: string[]
/** 注释(或 docstring)节点去掉注释符号后的原文,可以多行。 */ /** 注释(或 docstring)节点去掉注释符号后的原文,可以多行。 */
@@ -283,17 +285,17 @@ export function createZhTemplateTranslator(table: ZhTable): Translator {
function describe(block: FactBlock, source: string): PropertyRow[] { function describe(block: FactBlock, source: string): PropertyRow[] {
const { header } = block const { header } = block
// 值里带语句块主体的回调(useMemo(() => { … }))只翻到主体开始之前,和块头部一致
const within = (node: Node, part: SourceSpan): Segment[] => {
const span = part.endIndex > header.endIndex ? { ...part, endIndex: header.endIndex, endRow: header.endRow } : part
return joinTokens(tokensIn(node, span, source))
}
const phrases: Phrases = { const phrases: Phrases = {
// 值里带语句块主体的回调(useMemo(() => { … }))只翻到主体开始之前,和块头部一致 translated: node => within(node, { startIndex: node.startIndex, endIndex: node.endIndex, startRow: node.startPosition.row, endRow: node.endPosition.row }),
translated: (node) => { within,
const clipped = node.endIndex > header.endIndex entryCount: (literal, size) => {
const span = { const name = table.mapLiterals[literal.type]
startIndex: node.startIndex, return [hint(name ? `${name} ${size} 项` : `${size} 项`)]
endIndex: clipped ? header.endIndex : node.endIndex,
startRow: node.startPosition.row,
endRow: clipped ? header.endRow : node.endPosition.row,
}
return joinTokens(tokensIn(node, span, source))
}, },
verbatim: node => verbatim(node, source), verbatim: node => verbatim(node, source),
// 点分模块名逐节照抄,每一节各自能跳到定义,和 VS Code 里一样 // 点分模块名逐节照抄,每一节各自能跳到定义,和 VS Code 里一样
@@ -355,10 +357,14 @@ function joinTokens(tokens: Token[]): Segment[] {
// ─── 属性表 ─────────────────────────────────────────────────────────────────── // ─── 属性表 ───────────────────────────────────────────────────────────────────
/** 属性表里值的三种来源。 */ /** 属性表里的值怎么写。 */
interface Phrases { interface Phrases {
/** 按表翻译,只翻落在块头部里的那一段。 */ /** 按表翻译,只翻落在块头部里的那一段。 */
translated: (node: Node) => Segment[] translated: (node: Node) => Segment[]
/** 同 translated,只翻 node 落在 span 里的那一段(对象字面量里方法的头部)。 */
within: (node: Node, span: SourceSpan) => Segment[]
/** 字典 / 对象字面量的项数:「字典 3 项」,叫法按字面量的节点类型查表。 */
entryCount: (literal: Node, size: number) => Segment[]
/** 源码原样照抄、按源码着色(装饰器)。 */ /** 源码原样照抄、按源码着色(装饰器)。 */
verbatim: (node: Node) => Segment[] verbatim: (node: Node) => Segment[]
/** 导入来源:字符串字面量按字符串着色,模块名按类型着色。 */ /** 导入来源:字符串字面量按字符串着色,模块名按类型着色。 */
@@ -492,13 +498,40 @@ function importRows(facts: ImportFacts, say: Phrases): PropertyRow[] {
] ]
} }
/** 常量 / 变量:每个声明的变量依次是名称 / 访问级别 / 类型(有标注才有)/ 值(变量叫初始值)。 */ /** 常量 / 变量:每个声明的变量依次是名称 / 访问级别 / 类型(有标注才有)/ 值(变量叫初始值,字典 / 对象字面量见 literalValue)。 */
function variableRows(block: VariableBlock, say: Phrases): PropertyRow[] { function variableRows(block: VariableBlock, say: Phrases): PropertyRow[] {
const valueLabel = block.kind === 'const' ? '值' : '初始值' const valueLabel = block.kind === 'const' ? '值' : '初始值'
return block.facts.declarators.flatMap(declarator => [ return block.facts.declarators.flatMap(declarator => [
{ label: '名称', value: say.translated(declarator.name), ...defining(declarator.name) }, { label: '名称', value: say.translated(declarator.name), ...defining(declarator.name) },
...accessRows(block.facts.access), ...accessRows(block.facts.access),
...(declarator.type ? [{ label: '类型', value: say.translated(declarator.type) }] : []), ...(declarator.type ? [{ label: '类型', value: say.translated(declarator.type) }] : []),
{ label: valueLabel, value: declarator.value ? say.translated(declarator.value) : [hint('无')] }, { label: valueLabel, ...(declarator.value ? literalValue(declarator.value, declarator.entries, say) : { value: [hint('无')] }) },
]) ])
} }
/**
* 值:字典 / 对象字面量(语言插件给了 entries)写项数,各项展开成下一层子行;
* 空字面量和别的值是一行译文(空的译成「空字典」「空对象」)。
*/
function literalValue(value: Node, entries: EntryFacts[] | undefined, say: Phrases): { value: Segment[], 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) }
}
}
+1 -1
View File
@@ -30,7 +30,7 @@ export function App(): ReactElement | null {
/** 等着滚过去的位置:新预览第一份文档带的,或已开着的预览收到的 reveal */ /** 等着滚过去的位置:新预览第一份文档带的,或已开着的预览收到的 reveal */
const [reveal, setReveal] = useState<Reveal | null>(null) const [reveal, setReveal] = useState<Reveal | null>(null)
const collapsed = useMemo(() => collapsedIds(doc?.blocks ?? [], view.folds), [doc, view.folds]) const collapsed = useMemo(() => collapsedIds(doc?.blocks ?? [], view.folds), [doc, view.folds])
/** 记下这几块、属性行折叠还是展开,从此不再按默认 */ /** 记下这几块、属性行、子行折叠还是展开,从此不再按默认 */
const setFolded = useCallback((ids: string[], folded: boolean) => setView(prev => ({ const setFolded = useCallback((ids: string[], folded: boolean) => setView(prev => ({
...prev, ...prev,
folds: { ...prev.folds, ...Object.fromEntries(ids.map(id => [id, folded])) }, folds: { ...prev.folds, ...Object.fromEntries(ids.map(id => [id, folded])) },
@@ -29,7 +29,7 @@ const KIND_LABEL: Record<BlockKind, string> = {
export interface BlockTreeState { export interface BlockTreeState {
version: number version: number
editing: ReadonlySet<string> editing: ReadonlySet<string>
/** 眼下折叠着的块 id 和属性行 id(propertyRowId):用户的选择叠在默认上,见 collapsedIds */ /** 眼下折叠着的块 id、属性行 id 和子行 id(propertyRowId、propertyItemId):用户的选择叠在默认上,见 collapsedIds */
collapsed: ReadonlySet<string> collapsed: ReadonlySet<string>
/** 刚点了「编辑」、要把光标放进去的块 */ /** 刚点了「编辑」、要把光标放进去的块 */
focusId: string | null focusId: string | null
@@ -1,12 +1,12 @@
import type { ReactElement } from 'react' import type { ReactElement } from 'react'
import type { PropertyItem, PropertyRow } from './types' import type { PropertyItem, PropertyRow, Segment } from './types'
import { ChevronDown, ChevronRight } from 'lucide-react' import { ChevronDown, ChevronRight } from 'lucide-react'
import { Fragment } from 'react' import { Fragment } from 'react'
import { SegmentText } from './SegmentText' import { SegmentText } from './SegmentText'
import { TextPreview } from './TextPreview' import { TextPreview } from './TextPreview'
import { propertyItemId, propertyRowId } from './viewState' import { propertyItemId, propertyRowId } from './viewState'
/** 子行里除最后一列以外各列的宽度(参数的名称、类型),最后一列占满剩下的;编辑区窄时按比例收,不把最后一列挤没 */ /** 子行里除最后一列以外各列的宽度(参数的名称、类型,字典的键),最后一列占满剩下的;编辑区窄时按比例收,不把最后一列挤没 */
const CELL_WIDTHS = ['min(150px, 28%)', 'min(220px, 38%)'] const CELL_WIDTHS = ['min(150px, 28%)', 'min(220px, 38%)']
function columns(count: number): string { function columns(count: number): string {
@@ -14,32 +14,89 @@ function columns(count: number): string {
return [...fixed, 'minmax(0, 1fr)'].join(' ') return [...fixed, 'minmax(0, 1fr)'].join(' ')
} }
/** 展开在一行下面的子行:左边一条竖线,每个子行几列对齐。 */ /** 眼下折叠着哪些属性行、子行,点折叠箭头时切换哪个。 */
function PropertyItems({ rowId, items }: { rowId: string, items: PropertyItem[] }): ReactElement { interface Folding {
collapsed: ReadonlySet<string>
onToggleFold: (id: string) => void
}
function segmentsText(segments: Segment[]): string {
return segments.map(segment => segment.text).join('')
}
/**
* 子行的第一列(字典的键):带下一层的子行前面是折叠箭头(图标、大小、颜色同属性行的),点箭头或这一列切换;
* 同一层里不带下一层的子行在同样的位置留空,第一列才对得齐。
*/
function ItemHead({ id, cell, foldable, folding }: { id: string, cell: Segment[], foldable: boolean, folding: Folding }): ReactElement {
if (!foldable) {
return (
<div className="flex min-w-0 gap-1">
<span className="w-3.5 shrink-0" />
<p className="cr-code min-w-0">
<SegmentText segments={cell} />
</p>
</div>
)
}
const folded = folding.collapsed.has(id)
const Chevron = folded ? ChevronRight : ChevronDown
return (
<button
type="button"
className="group flex min-w-0 items-start gap-1 rounded-sm text-left"
aria-label={`${folded ? '展开' : '折叠'}${segmentsText(cell)}`}
aria-expanded={!folded}
onClick={() => folding.onToggleFold(id)}
>
<span className="flex h-5 w-3.5 shrink-0 items-center">
<Chevron size={14} strokeWidth={1.8} className="text-[var(--vscode-descriptionForeground)] group-hover:text-[var(--vscode-foreground)]" />
</span>
<span className="cr-code min-w-0">
<SegmentText segments={cell} />
</span>
</button>
)
}
/**
* 展开在一行或一个子行(parentId)下面的一层子行:左边一条竖线,每个子行几列对齐。
* 带下一层的子行(字典 / 对象的值又是字典 / 对象),下一层展开在它下面、再往右缩进一层,默认展开。
*/
function PropertyItems({ parentId, items, folding }: { parentId: string, items: PropertyItem[], folding: Folding }): ReactElement {
const nested = items.some(item => (item.items?.length ?? 0) > 0)
return ( return (
<div className="flex gap-2.5 pl-[5px]"> <div className="flex gap-2.5 pl-[5px]">
<div className="cr-guide w-px shrink-0" /> <div className="cr-guide w-px shrink-0" />
<div className="flex min-w-0 flex-1 flex-col gap-0.5"> <div className="flex min-w-0 flex-1 flex-col gap-0.5">
{items.map((item, index) => ( {items.map((item, index) => {
<div const id = propertyItemId(parentId, index)
key={index} const children = item.items ?? []
className="cr-prop-item grid gap-x-3" return (
data-id={propertyItemId(rowId, index)} <Fragment key={index}>
style={{ gridTemplateColumns: columns(item.cells.length) }} <div
> className="cr-prop-item grid gap-x-3"
{item.cells.map((cell, i) => ( data-id={id}
<p key={i} className="cr-code min-w-0"> style={{ gridTemplateColumns: columns(item.cells.length) }}
<SegmentText segments={cell} /> >
</p> {item.cells.map((cell, i) => i === 0 && nested
))} ? <ItemHead key={i} id={id} cell={cell} foldable={children.length > 0} folding={folding} />
</div> : (
))} <p key={i} className="cr-code min-w-0">
<SegmentText segments={cell} />
</p>
))}
</div>
{children.length > 0 && !folding.collapsed.has(id) && <PropertyItems parentId={id} items={children} folding={folding} />}
</Fragment>
)
})}
</div> </div>
</div> </div>
) )
} }
/** 函数、类、导入、常量、变量块的预览:属性名一列对齐,值按词性着色;输入参数、导入项下面展开子行。 */ /** 函数、类、导入、常量、变量块的预览:属性名一列对齐,值按词性着色;输入参数、导入项、字典 / 对象的值下面展开子行。 */
export function PropertyGrid({ blockId, rows, collapsed, onToggleFold }: { export function PropertyGrid({ blockId, rows, collapsed, onToggleFold }: {
blockId: string blockId: string
rows: PropertyRow[] rows: PropertyRow[]
@@ -72,7 +129,7 @@ export function PropertyGrid({ blockId, rows, collapsed, onToggleFold }: {
: <span className="cr-prop-label flex h-5 w-[76px] shrink-0 items-center">{row.label}</span>} : <span className="cr-prop-label flex h-5 w-[76px] shrink-0 items-center">{row.label}</span>}
<TextPreview text={row.value} className="min-w-0 flex-1" /> <TextPreview text={row.value} className="min-w-0 flex-1" />
</div> </div>
{items.length > 0 && !folded && <PropertyItems rowId={id} items={items} />} {items.length > 0 && !folded && <PropertyItems parentId={id} items={items} folding={{ collapsed, onToggleFold }} />}
</Fragment> </Fragment>
) )
})} })}
@@ -1,6 +1,6 @@
import type { LanguageAdapter, Translator } from '../../../src/codeReader/contract' import type { LanguageAdapter, Translator } from '../../../src/codeReader/contract'
import type { RevealPlan } from './reveal' import type { RevealPlan } from './reveal'
import type { BlockKind, BlockView, PropertyRow, Segment } from './types' import type { BlockKind, BlockView, PropertyItem, PropertyRow, Segment } from './types'
import { describe, expect, it } from 'vitest' import { describe, expect, it } from 'vitest'
import { toBlockViews } from '../../../src/codeReader/blockView' import { toBlockViews } from '../../../src/codeReader/blockView'
import { createPythonAdapter } from '../../../src/codeReader/languages/python' import { createPythonAdapter } from '../../../src/codeReader/languages/python'
@@ -138,13 +138,25 @@ describe('跳到定义:TS / Python 源码里闪哪一处', () => {
/** 不填 defines 的第三方翻译插件:属性表照旧,行和子行都不标讲的是哪个定义。 */ /** 不填 defines 的第三方翻译插件:属性表照旧,行和子行都不标讲的是哪个定义。 */
function withoutDefines(translator: Translator): Translator { function withoutDefines(translator: Translator): Translator {
const strip = (items: PropertyItem[]): PropertyItem[] => items.map(({ cells, items: nested }) => nested ? { cells, items: strip(nested) } : { cells })
return { return {
...translator, ...translator,
describe: (block, source) => translator.describe(block, source).map(({ label, value, items }) => describe: (block, source) => translator.describe(block, source).map(({ label, value, items }) =>
items ? { label, value, items: items.map(({ cells }) => ({ cells })) } : { label, value }), items ? { label, value, items: strip(items) } : { label, value }),
} }
} }
/** parentId 下面一层层的子行里 id 是 target 的那个。 */
function findItem(items: PropertyItem[] | undefined, parentId: string, target: string): PropertyItem | undefined {
for (const [index, item] of (items ?? []).entries()) {
const id = propertyItemId(parentId, index)
const found = id === target ? item : findItem(item.items, id, target)
if (found)
return found
}
return undefined
}
function flatten(list: BlockView[]): BlockView[] { function flatten(list: BlockView[]): BlockView[] {
return list.flatMap(view => [view, ...flatten(view.children)]) return list.flatMap(view => [view, ...flatten(view.children)])
} }
@@ -163,7 +175,7 @@ describe('跳到定义:TS / Python 源码里闪哪一处', () => {
const rowId = propertyRowId(block.id, index) const rowId = propertyRowId(block.id, index)
if (plan.row === rowId) if (plan.row === rowId)
return `${row.label}: ${text(row.value)}` return `${row.label}: ${text(row.value)}`
const item = row.items?.find((_, i) => plan.row === propertyItemId(rowId, i)) const item = findItem(row.items, rowId, plan.row)
if (item) if (item)
return item.cells.map(text).join(' | ') return item.cells.map(text).join(' | ')
} }
@@ -267,6 +279,27 @@ describe('跳到定义:TS / Python 源码里闪哪一处', () => {
expect(await atPy('run')).toEqual({ plan: { id: '0.0', expand: ['0'] }, flashed: 'def run(self):' }) expect(await atPy('run')).toEqual({ plan: { id: '0.0', expand: ['0'] }, flashed: 'def run(self):' })
}) })
it('字典 / 对象:定义落在对象的属性名上只闪那一项,嵌套的连外层各层一起展开;值里只是显示着的名字闪整块,展开到它那一层', async () => {
const source = [
'export const routes = {',
' home: { path: \'/\', page: Home },',
' about,',
'}',
'use(routes.home.path)',
].join('\n')
const at = (needle: string): ReturnType<typeof jump> => jump(ts, 'typescript', typescriptZhTranslator, source, needle)
expect(await at('home:')).toEqual({ plan: { id: '0', row: '0/p2/0', expand: ['0/p2'] }, flashed: 'home | 对象 2 项' })
expect(await at('path:')).toEqual({ plan: { id: '0', row: '0/p2/0/0', expand: ['0/p2', '0/p2/0'] }, flashed: 'path | \'/\'' })
expect(await at('about,')).toEqual({ plan: { id: '0', row: '0/p2/1', expand: ['0/p2'] }, flashed: 'about | about' })
expect(await at('Home')).toEqual({ plan: { id: '0', expand: ['0/p2', '0/p2/0'] }, flashed: 'export const routes = {' })
const plain = (needle: string): ReturnType<typeof jump> => jump(ts, 'typescript', withoutDefines(typescriptZhTranslator), source, needle)
expect(await plain('path:')).toEqual({ plan: { id: '0', expand: ['0/p2', '0/p2/0'] }, flashed: 'export const routes = {' })
const pySource = 'CONFIG = {"db": {"host": host}}'
const atPy = (needle: string): ReturnType<typeof jump> => jump(py, 'python', pythonZhTranslator, pySource, needle)
expect(await atPy('host}')).toEqual({ plan: { id: '0', expand: ['0/p2', '0/p2/0'] }, flashed: 'CONFIG = {"db": {"host": host}}' })
})
it('typeScript:翻译插件没标 defines 时闪整块', async () => { it('typeScript:翻译插件没标 defines 时闪整块', async () => {
const at = (needle: string): ReturnType<typeof jump> => jump(ts, 'typescript', withoutDefines(typescriptZhTranslator), TS, needle) const at = (needle: string): ReturnType<typeof jump> => jump(ts, 'typescript', withoutDefines(typescriptZhTranslator), TS, needle)
expect(await at('path: string')).toEqual({ plan: { id: '1', expand: ['1/p3'] }, flashed: 'export function load(path: string, { encoding }: Options): string {' }) expect(await at('path: string')).toEqual({ plan: { id: '1', expand: ['1/p3'] }, flashed: 'export function load(path: string, { encoding }: Options): string {' })
+36 -9
View File
@@ -1,7 +1,7 @@
import type { BlockView, PreviewText, SourceRange } from './types' import type { BlockView, PreviewText, PropertyItem, SourceRange } from './types'
import { propertyItemId, propertyRowId } from './viewState' import { propertyItemId, propertyRowId } from './viewState'
/** 跳到定义时要滚到的块,就是这处定义的属性行 / 子行(只闪它),以及要展开的块和属性行:它们折叠着,目标就不在页面上。 */ /** 跳到定义时要滚到的块,就是这处定义的属性行 / 子行(只闪它),以及要展开的块、属性行和子行:它们折叠着,目标就不在页面上。 */
export interface RevealPlan { export interface RevealPlan {
id: string id: string
/** 属性行 id(propertyRowId)或子行 id(propertyItemId);没有时闪整块 */ /** 属性行 id(propertyRowId)或子行 id(propertyItemId);没有时闪整块 */
@@ -38,8 +38,9 @@ function shows(text: PreviewText, offset: number): boolean {
/** /**
* 块里哪儿是 offset 处的定义,从具体到笼统: * 块里哪儿是 offset 处的定义,从具体到笼统:
* ① 块自己的名字(函数名、类名、只声明一个变量时的变量名)→ 整块; * ① 块自己的名字(函数名、类名、只声明一个变量时的变量名)→ 整块;
* ② 属性表里讲这个定义的一行 / 子行(defines 盖住 offset:参数、导入项、多变量声明里的一个)→ 只闪它,子行连它所在的行一起展开; * ② 属性表里讲这个定义的一行 / 子行(defines 盖住 offset:参数、导入项、多变量声明里的一个、对象一项的键)→ 只闪它,
* ③ 块里只是显示着这个名字(一行译文里的循环变量、没填 defines 的翻译插件)→ 整块,名字在子行里时展开那一行。 * 子行连它上面的各层(所在的行、嵌套时外层的子行)一起展开;
* ③ 块里只是显示着这个名字(一行译文里的循环变量、没填 defines 的翻译插件)→ 整块,名字在子行里时展开它上面的各层。
* 不在这块里是 undefined。 * 不在这块里是 undefined。
*/ */
function locate(block: BlockView, offset: number): Spot | undefined { function locate(block: BlockView, offset: number): Spot | undefined {
@@ -51,15 +52,41 @@ function locate(block: BlockView, offset: number): Spot | undefined {
for (const { row, id } of rows) { for (const { row, id } of rows) {
if (covers(row.defines, offset)) if (covers(row.defines, offset))
return { row: id, expand: [] } return { row: id, expand: [] }
const item = row.items?.findIndex(entry => covers(entry.defines, offset)) ?? -1 const item = definingItem(row.items, id, offset)
if (item >= 0) if (item)
return { row: propertyItemId(id, item), expand: [id] } return item
} }
for (const { row, id } of rows) { for (const { row, id } of rows) {
if (shows(row.value, offset)) if (shows(row.value, offset))
return { expand: [] } return { expand: [] }
if (row.items?.some(item => item.cells.some(cell => shows(cell, offset)))) const expand = showingItem(row.items, id, offset)
return { expand: [id] } if (expand)
return { expand }
}
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 (covers(item.defines, offset))
return { row: id, expand: [parentId] }
const inner = definingItem(item.items, id, offset)
if (inner)
return { ...inner, expand: [parentId, ...inner.expand] }
}
return undefined
}
/** parentId 下面一层层的子行里,哪个显示着 offset 处的名字:要展开的各层(parentId 到那个子行所在的那一层)。 */
function showingItem(items: PropertyItem[] | undefined, parentId: string, offset: number): string[] | undefined {
for (const [index, item] of (items ?? []).entries()) {
if (item.cells.some(cell => shows(cell, offset)))
return [parentId]
const inner = showingItem(item.items, propertyItemId(parentId, index), offset)
if (inner)
return [parentId, ...inner]
} }
return undefined return undefined
} }
+8 -4
View File
@@ -27,15 +27,19 @@ export type PreviewText = Segment[] | Markdown
export interface SourceRange { startIndex: number, endIndex: number } export interface SourceRange { startIndex: number, endIndex: number }
/** /**
* 属性表里的一行或一个子行讲的是哪个定义:定义出来的那个名字(参数名、导入项、声明的变量名)在源码里的范围。 * 属性表里的一行或一个子行讲的是哪个定义:定义出来的那个名字(参数名、导入项、声明的变量名、字典 / 对象一项的键)在源码里的范围。
* 跳到的定义落在里面就只闪这一行;翻译插件没填就闪整块 * 跳到的定义落在里面就只闪这一行;翻译插件没填就闪整块
*/ */
export interface Defines { defines?: SourceRange } export interface Defines { defines?: SourceRange }
/** 属性表里展开的一个子行:几列对齐的格子,例如参数的名称 / 类型 / 默认值 */ /** 属性表里展开的一个子行:几列对齐的格子,例如参数的名称 / 类型 / 默认值、字典一项的键 / 值 */
export interface PropertyItem extends Defines { cells: Segment[][] } export interface PropertyItem extends Defines {
cells: Segment[][]
/** 展开在这个子行下面的下一层子行(字典 / 对象的值又是字典 / 对象时,它的各项),一层层可以一直往下套 */
items?: PropertyItem[]
}
/** 属性表的一行:左列属性名,右列着色的值或 Markdown 说明;items 是展开在这一行下面的子行(输入参数、导入项) */ /** 属性表的一行:左列属性名,右列着色的值或 Markdown 说明;items 是展开在这一行下面的子行(输入参数、导入项、字典 / 对象的各项) */
export interface PropertyRow extends Defines { export interface PropertyRow extends Defines {
label: string label: string
value: PreviewText value: PreviewText
@@ -25,7 +25,7 @@ function flash(el: HTMLElement): void {
/** /**
* 滚到 reveal 指的位置,处理完调 done: * 滚到 reveal 指的位置,处理完调 done:
* ① 从文本编辑器切过来(没有 definition):包含那一行的块顶到视口顶部,折叠着的不展开; * ① 从文本编辑器切过来(没有 definition):包含那一行的块顶到视口顶部,折叠着的不展开;
* ② 跳到定义:先展开把目标藏起来的块和属性行(expand 之后重新渲染,再进来一次),再把目标(整块,或就是这处定义的属性行 / 子行)滚进视口、闪几下。 * ② 跳到定义:先展开把目标藏起来的块、属性行和子行(expand 之后重新渲染,再进来一次),再把目标(整块,或就是这处定义的属性行 / 子行)滚进视口、闪几下。
*/ */
export function useReveal( export function useReveal(
doc: DocumentMessage | null, doc: DocumentMessage | null,
@@ -1,7 +1,7 @@
import type { BlockKind, BlockView } from './types' import type { BlockKind, BlockView } from './types'
import type { ViewStateStorage } from './viewState' import type { ViewStateStorage } from './viewState'
import { describe, expect, it } from 'vitest' import { describe, expect, it } from 'vitest'
import { collapsedIds, createViewStateStore, propertyRowId } from './viewState' import { collapsedIds, createViewStateStore, propertyItemId, propertyRowId } from './viewState'
/** 模拟 VS Code 的 webview 状态:存进去的要能序列化,页面重新加载后原样取回;也能塞进旧版本存的东西 */ /** 模拟 VS Code 的 webview 状态:存进去的要能序列化,页面重新加载后原样取回;也能塞进旧版本存的东西 */
function memoryStorage(initial?: unknown): ViewStateStorage { function memoryStorage(initial?: unknown): ViewStateStorage {
@@ -73,6 +73,32 @@ describe('webview 状态', () => {
createViewStateStore(storage).update({ folds: { 6.1: false, [propertyRowId('6.1.0', 3)]: true } }) createViewStateStore(storage).update({ folds: { 6.1: false, [propertyRowId('6.1.0', 3)]: true } })
expect(createViewStateStore(storage).get().folds).toEqual({ '6.1': false, '6.1.0/p3': true }) expect(createViewStateStore(storage).get().folds).toEqual({ '6.1': false, '6.1.0/p3': true })
}) })
it('子行 id 是上一层的 id 加子行序号,一层层往后接;不同位置的不会撞上,也不会和块 id、属性行 id 撞上', () => {
const row = propertyRowId('6.1.0', 3)
expect(propertyItemId(row, 1)).toBe('6.1.0/p3/1')
expect(propertyItemId(propertyItemId(row, 1), 0)).toBe('6.1.0/p3/1/0')
const ids = [
'6',
'6.1',
propertyRowId('6', 1),
propertyRowId('6', 11),
propertyItemId(propertyRowId('6', 1), 1),
propertyItemId(propertyRowId('6', 11), 1),
propertyItemId(propertyItemId(propertyRowId('6', 1), 1), 1),
propertyItemId(propertyItemId(propertyRowId('6', 1), 11), 1),
propertyItemId(propertyItemId(propertyRowId('6', 1), 1), 11),
propertyItemId(propertyRowId('6.1', 1), 1),
]
expect(new Set(ids).size).toBe(ids.length)
})
it('嵌套子行的折叠选择也存在 folds 里,页面重新加载后还在', () => {
const storage = memoryStorage()
const nested = propertyItemId(propertyItemId(propertyRowId('2', 2), 0), 1)
createViewStateStore(storage).update({ folds: { [nested]: true, [propertyItemId(propertyRowId('2', 2), 0)]: false } })
expect(createViewStateStore(storage).get().folds).toEqual({ '2/p2/0/1': true, '2/p2/0': false })
})
}) })
describe('眼下折叠着哪些', () => { describe('眼下折叠着哪些', () => {
@@ -84,4 +110,11 @@ describe('眼下折叠着哪些', () => {
const folds = { '1': false, '2': false, '1.0': true, '1.0.0': false, [propertyRowId('0', 2)]: true, [propertyRowId('2.0', 1)]: false } const folds = { '1': false, '2': false, '1.0': true, '1.0.0': false, [propertyRowId('0', 2)]: true, [propertyRowId('2.0', 1)]: false }
expect([...collapsedIds(BLOCKS, folds)].sort()).toEqual(['0/p2', '1.0', '2.0']) expect([...collapsedIds(BLOCKS, folds)].sort()).toEqual(['0/p2', '1.0', '2.0'])
}) })
it('带下一层的子行默认展开,用户折叠过的按他的', () => {
const outer = propertyItemId(propertyRowId('0', 2), 0)
const inner = propertyItemId(outer, 3)
expect(collapsedIds(BLOCKS, {}).has(outer)).toBe(false)
expect([...collapsedIds(BLOCKS, { [outer]: false, [inner]: true })].sort()).toEqual(['0/p2/0/3', '1', '1.0.0', '2', '2.0'])
})
}) })
+13 -8
View File
@@ -3,11 +3,11 @@ import type { BlockKind, BlockView } from './types'
/** /**
* 标签页切走时 VS Code 会销毁 webview、切回来重新加载,靠它恢复:哪些块停在「编辑」、 * 标签页切走时 VS Code 会销毁 webview、切回来重新加载,靠它恢复:哪些块停在「编辑」、
* 用户把哪些块和属性行折叠 / 展开了(块 id 是它在树里的位置,文档刷新后照旧对得上;属性行 id 见 propertyRowId),以及滚到了哪。 * 用户把哪些块、属性行和子行折叠 / 展开了(块 id 是它在树里的位置,文档刷新后照旧对得上;属性行、子行 id 见 propertyRowId、propertyItemId),以及滚到了哪。
*/ */
export interface ViewState { export interface ViewState {
editing: string[] editing: string[]
/** 用户自己点过折叠箭头的块和属性行:id → 折叠着没有。没点过的按默认,见 collapsedIds */ /** 用户自己点过折叠箭头的块、属性行和子行:id → 折叠着没有。没点过的按默认,见 collapsedIds */
folds: Record<string, boolean> folds: Record<string, boolean>
scroll?: ScrollAnchor scroll?: ScrollAnchor
} }
@@ -54,12 +54,12 @@ function readViewState(saved: unknown): ViewState {
return isScrollAnchor(state.scroll) ? { editing, folds, scroll: state.scroll } : { editing, folds } return isScrollAnchor(state.scroll) ? { editing, folds, scroll: state.scroll } : { editing, folds }
} }
/** 块默认折叠吗:函数、类块收起主体(属性表照常显示),其余块展开。属性行默认都展开。 */ /** 块默认折叠吗:函数、类块收起主体(属性表照常显示),其余块展开。属性行和带下一层的子行默认都展开。 */
export function foldedByDefault(kind: BlockKind): boolean { export function foldedByDefault(kind: BlockKind): boolean {
return kind === 'function' || kind === 'class' return kind === 'function' || kind === 'class'
} }
/** 眼下折叠着的块和属性行:用户点过的按他点的,没点过的按 foldedByDefault。 */ /** 眼下折叠着的块、属性行和子行:用户点过的按他点的,没点过的按 foldedByDefault。 */
export function collapsedIds(blocks: readonly BlockView[], folds: Readonly<Record<string, boolean>>): Set<string> { export function collapsedIds(blocks: readonly BlockView[], folds: Readonly<Record<string, boolean>>): Set<string> {
const collapsed = new Set(Object.keys(folds).filter(id => folds[id])) const collapsed = new Set(Object.keys(folds).filter(id => folds[id]))
const visit = (list: readonly BlockView[]): void => { const visit = (list: readonly BlockView[]): void => {
@@ -74,14 +74,19 @@ export function collapsedIds(blocks: readonly BlockView[], folds: Readonly<Recor
} }
/** /**
* 属性表里带子行的那一行(输入参数、导入项)的折叠 id,和块 id 放在同一份 folds 里: * 属性表里带子行的那一行(输入参数、导入项、字典 / 对象的值)的折叠 id,和块 id 放在同一份 folds 里:
* `<块 id>/p<行序号>`,例如 '6.1.0/p3'。块 id 只由数字和点组成,带 '/' 的不会和任何块 id 撞上。 * `<块 id>/p<行序号>`,例如 '6.1.0/p3'。块 id 只由数字和点组成,带 '/' 的不会和任何块 id 撞上。
*/ */
export function propertyRowId(blockId: string, rowIndex: number): string { export function propertyRowId(blockId: string, rowIndex: number): string {
return `${blockId}/p${rowIndex}` return `${blockId}/p${rowIndex}`
} }
/** 属性行展开的第几个子行,例如 '6.1.0/p3/1'。只用来在页面上找到它(跳到定义时闪它),子行不折叠、不进 folds。 */ /**
export function propertyItemId(rowId: string, itemIndex: number): string { * 一行或一个子行(parentId)下面展开的第几个子行:`<上一层的 id>/<子行序号>`,一层套一层往后接,
return `${rowId}/${itemIndex}` * 例如 '6.1.0/p3/1' 是第 3 行的第 1 个子行,'6.1.0/p3/1/0' 是它下面的第 0 个。跳到定义时拿它在页面上找到要闪的子行;
* 带下一层子行的子行(字典 / 对象的值又是字典 / 对象)也拿它当折叠 id,和属性行一样记进 folds、默认展开。
* 只有行那一节带 'p',后面各节都是纯数字,不同位置的 id 不会撞上,也不会和块 id、属性行 id 撞上。
*/
export function propertyItemId(parentId: string, itemIndex: number): string {
return `${parentId}/${itemIndex}`
} }