✨ feat(vscode): 代码阅读类块改成属性表并默认折叠,类的说明进「说明」行
This commit is contained in:
1 parent
3329aca2e1
commit
43323659e1
22 files changed
+580
-121
No files matched your search
@@ -19,13 +19,13 @@
|
||||
| 多种翻译怎么选 | 预览顶部下拉框,整个文件一起换,按语言记住选择;只有一种时不显示 |
|
||||
| 侧栏点文件 | 支持的语言默认打开成「预览」;不支持的语言用普通编辑器打开 |
|
||||
| 块内修改 | 改的就是文件本身:未保存标记、⌘S 保存、⌘Z 撤销都照常,切回「预览」按新代码重新翻译 |
|
||||
| 折叠 | 有子块的块、属性表里带子行的行能折叠;函数块(含方法、嵌套函数)默认收起主体、属性表照常显示,其余块和属性行默认展开;用户点过的按他点的记住 |
|
||||
| 函数的说明 | TypeScript 紧贴在函数前面的 JSDoc、Python 函数主体开头的 docstring 是函数自己的说明:进属性表「说明」一行,不再单独成一块注释;它的源码行算在函数块的头部里,「编辑」时和签名一起改 |
|
||||
| 注释按 Markdown 显示 | 注释块和函数的「说明」按 Markdown 渲染,像 Kotlin 的 KDoc:去掉注释符号、引号和共同缩进后的原文原样交给 Markdown,不按语言转换(reStructuredText 的 `:func:`、JSDoc 的 `@param` 都照抄,注释以后慢慢改成 Markdown 写法);段内单个换行是软换行,空行分段 |
|
||||
| 折叠 | 有子块的块、属性表里带子行的行能折叠;函数块(含方法、嵌套函数)和类块默认收起主体、属性表照常显示,其余块和属性行默认展开;用户点过的按他点的记住 |
|
||||
| 函数、类的说明 | TypeScript 紧贴在函数、类前面的 JSDoc,Python 函数、类主体开头的 docstring 是它自己的说明:进属性表「说明」一行,不再单独成一块注释;它的源码行算在函数、类块的头部里,「编辑」时和签名一起改 |
|
||||
| 注释按 Markdown 显示 | 注释块和函数、类的「说明」按 Markdown 渲染,像 Kotlin 的 KDoc:去掉注释符号、引号和共同缩进后的原文原样交给 Markdown,不按语言转换(reStructuredText 的 `:func:`、JSDoc 的 `@param` 都照抄,注释以后慢慢改成 Markdown 写法);段内单个换行是软换行,空行分段 |
|
||||
| 跳到定义 | 和普通编辑器一样 ⌘ / Ctrl + 点击名字跳到定义,定义由 VS Code 装的语言扩展(Pylance、TypeScript 语言服务……)查;目标语言有代码阅读就在预览里打开 |
|
||||
| 侧栏文件树 | 只看和打开:展开目录、打开文件、刷新、全部折叠;不做新建 / 重命名 / 删除 |
|
||||
| 译文用词 | 结构照抄原语言,只翻关键字、内置类型、运算符;用专业术语(`export` → 公共,`catch` → 捕获,`===` → 全等) |
|
||||
| 函数、导入、常量、变量块 | 预览不显示一行译文,显示树形属性表;属性值里的类型、表达式照常直译 |
|
||||
| 函数、类、导入、常量、变量块 | 预览不显示一行译文,显示树形属性表;属性值里的类型、表达式照常直译 |
|
||||
|
||||
## 用户可见行为
|
||||
|
||||
@@ -50,34 +50,35 @@
|
||||
- 块的组成(从左到右):折叠箭头(只有有子块的块才显示,其余占位对齐)、种类标签、内容、右侧切换按钮。
|
||||
- 种类标签和底色按种类区分(颜色见下表);注释块虚线框。
|
||||
- 内容两种状态,每块单独切:
|
||||
- 预览:函数、导入、常量、变量块是属性表(见下节),注释块是 Markdown 说明(见「注释的 Markdown 排版」),其余块是一行译文;按词着色,等宽字体(中文部分用系统中文字体回退)。
|
||||
- 预览:函数、类、导入、常量、变量块是属性表(见下节),注释块是 Markdown 说明(见「注释的 Markdown 排版」),其余块是一行译文;按词着色,等宽字体(中文部分用系统中文字体回退)。
|
||||
- 编辑:这一块的源码(按语法树着色),可以直接改;蓝色边框。按钮文字「预览」;Esc 也回到预览。
|
||||
- 复合语句(函数、条件、循环……)块里只放它自己的头部那几行(主体交给子块);收尾括号、空行不成块。函数的说明(JSDoc / docstring)也算在函数块的头部里。
|
||||
- 折叠:函数块打开时默认收起子块(属性表照常显示),其余块默认展开;用户点过折叠箭头的块按他点的,记在视图状态里(见「预览数据与消息」)。
|
||||
- 复合语句(函数、条件、循环……)块里只放它自己的头部那几行(主体交给子块);收尾括号、空行不成块。函数、类的说明(JSDoc / docstring)也算在函数、类块的头部里。
|
||||
- 折叠:函数、类块打开时默认收起子块(属性表照常显示),其余块默认展开;用户点过折叠箭头的块按他点的,记在视图状态里(见「预览数据与消息」)。
|
||||
- 顶部工具栏:翻译下拉框(多于一种翻译时才显示)。
|
||||
- 解析失败:显示错误原因,并提示用「编辑」查看源码。
|
||||
|
||||
### 属性表(函数、导入、常量、变量块)
|
||||
### 属性表(函数、类、导入、常量、变量块)
|
||||
|
||||
卡片外观(折叠箭头、种类标签、底色、悬停高亮、「编辑 / 预览」切换)和其他块一样,只是内容换成一张属性表:左列属性名(界面字体、`descriptionForeground` 色,宽 76px),右列是值(等宽字体,按词性着色,长了换行),两列间距 12px,行高 20px。「输入参数」「导入项」下面展开子行,属性名前带折叠箭头(图标、大小、颜色同块的折叠箭头;点箭头或属性名切换,默认展开,折叠后值「N 个」照常显示,只收起子行);子行往右缩进 5px,左边一条 1px 竖线(`editorIndentGuide-background1`)。输入参数的子行分名称 / 类型 / 默认值三列对齐,前两列宽 150px / 220px,编辑区窄时按 28% / 38% 收,默认值列占剩下的。
|
||||
|
||||
| 种类 | 行 |
|
||||
| --- | --- |
|
||||
| 函数 | 名称(函数色;匿名函数写「匿名」)/ 说明(有说明才显示: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` 后面每个类型的译文,逗号隔开) |
|
||||
| 导入 | 来源(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`)就重复几组 |
|
||||
|
||||
访问级别(公共 / 私有 / 受保护):
|
||||
|
||||
- TypeScript 模块里的声明:`export` 了是公共,顶层没 `export` 是私有;嵌套在函数体、条件等语句块里的没有这一行。
|
||||
- TypeScript 类成员:写了 `public` / `private` / `protected` 按写的,`#name` 是私有,都没写是公共。
|
||||
- Python:只有模块顶层和类主体里的声明有;名字单下划线开头(`_x`、`__x`)是私有,前后双下划线(`__init__`)是公共,其余公共;解构赋值里的名字全是私有才算私有;赋值目标是属性、下标(`self.x = …`)时没有这一行。
|
||||
- TypeScript 模块里的声明(函数、类、变量):`export` 了是公共(含 `export default`),顶层没 `export` 是私有;嵌套在函数体、条件等语句块里的没有这一行。
|
||||
- TypeScript 类成员(方法、函数值字段、类表达式字段):写了 `public` / `private` / `protected` 按写的,`#name` 是私有,都没写是公共。
|
||||
- Python:只有模块顶层和类主体里的声明(函数、类、变量)有;名字单下划线开头(`_x`、`__x`)是私有,前后双下划线(`__init__`)是公共,其余公共;解构赋值里的名字全是私有才算私有;赋值目标是属性、下标(`self.x = …`)时没有这一行。
|
||||
|
||||
值里的表达式只翻落在块头部里的那一段:`const v = useMemo(() => { … })` 这种值带语句块主体回调的块,回调主体是子块,值只翻到 `useMemo(() =>`,和块头部一致。
|
||||
|
||||
### 注释的 Markdown 排版
|
||||
|
||||
- 用在:注释块(`#`、`//`、`/* */`、不挂在函数上的 JSDoc、Python 模块 / 类主体开头的 docstring)的预览,函数属性表的「说明」。视图数据里它们是 `{ markdown }`(见「通用契约」的 `Markdown`),webview 按数据决定怎么渲染,不猜。
|
||||
- 用在:注释块(`#`、`//`、`/* */`、不挂在函数、类上的 JSDoc、Python 模块开头的 docstring)的预览,函数、类属性表的「说明」。视图数据里它们是 `{ markdown }`(见「通用契约」的 `Markdown`),webview 按数据决定怎么渲染,不猜。
|
||||
- 原文:翻译插件去掉注释符号(`#`、`//`、`/*` `*/` 和每行开头的 `*`)和引号,再去掉行尾空白、首尾空行和共同缩进,保留换行,不做别的转换。一个注释节点(块注释、docstring)的第一行紧跟在注释符号 / 引号后面,不参与算共同缩进(同 PEP 257);几条行注释连成的一块每行地位相同,第一行也算,缩进四格的代码块才留得住。
|
||||
- 渲染:webview 用 markdown-it(`html: false`、`linkify: true`),CommonMark 加 GFM 的表格、删除线:段落、强调、行内代码(含双反引号)、代码块(围栏、缩进四格)、列表、链接、标题、引用、表格。段内单个换行是软换行,空行分段。同一段文字只渲染一次(缓存最近 500 段)。
|
||||
- 安全:源码里的 HTML 当文字显示;链接只认 http / https / mailto,带协议的网址和邮箱自动成链接(`sample.py` 这种不带协议的不算),别的写法(`javascript:`、`command:`、相对路径)照原文显示。点链接由 VS Code 的 webview 自己拦下、交给宿主在外部打开,不用另发消息。图片不渲染(webview 的 CSP 不放行外部图片),`` 显示成 `!` 加链接。
|
||||
@@ -86,7 +87,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 刚启动那几秒会答「没有」;只认最近一次询问的回答,迟到的丢掉。
|
||||
- ⌘ / Ctrl + 点击:
|
||||
- 一处定义,在当前文件:原地滚过去;
|
||||
@@ -96,8 +97,8 @@
|
||||
- 没有定义:不动。
|
||||
- 和普通编辑器里 ⌘ + 点击一样:目标是否按「预览标签」打开听 `workbench.editor.enablePreview`;`workbench.editor.enablePreviewFromCodeNavigation` 为假(默认)时先把当前预览标签固定住,免得被目标替掉。
|
||||
- 闪哪一处:页面上**就是这处定义**的最具体的那一处,只按源码位置判断(定义提供者给的定义处下标,对上块和属性行带的源码范围),不看属性名文字:
|
||||
1. 定义是块自己的名字(函数名;只声明一个变量时的变量名,解构模式整个算)→ 整块。块的这段范围由扩展端从 facts 取(`BlockView.defines`),和翻译插件无关。
|
||||
2. 定义是属性表里某一行 / 子行讲的东西(参数、导入项、一条语句声明几个变量时的某一个)→ 只闪这一行 / 子行。范围是翻译插件从 facts 取的节点(`PropertyRow.defines`、`PropertyItem.defines`),定义处落在范围里就算(`*args` 的定义在 `args` 上、解构参数里的名字都对得上)。
|
||||
1. 定义是块自己的名字(函数名、类名;只声明一个变量时的变量名,解构模式整个算)→ 整块。块的这段范围由扩展端从 facts 取(`BlockView.defines`),和翻译插件无关。
|
||||
2. 定义是属性表里某一行 / 子行讲的东西(参数、导入项、一条语句声明几个变量时的某一个、类的泛型参数)→ 只闪这一行 / 子行。范围是翻译插件从 facts 取的节点(`PropertyRow.defines`、`PropertyItem.defines`),定义处落在范围里就算(`*args` 的定义在 `args` 上、解构参数里的名字都对得上)。
|
||||
3. 其余(定义在一行译文里,例如循环变量;翻译插件没填 `defines`)→ 显示着定义处那个名字的块,名字在属性行的子行里时展开那一行;页面上找不到这个名字就取包含定义那一行的块。
|
||||
- 同一文件原地跳、跳进另一个文件的预览,规则一样。
|
||||
- 滚过去:目标所在块的祖先块折叠着就展开,目标在折叠的属性行子行里(参数、导入项)就展开那一行。目标已在视口里不滚,否则滚到视口正中(比视口高就顶部对齐)。然后目标的边框和底色往聚焦色(`--vscode-focusBorder`)闪三下,共约 1.5 秒后复原;同一处再跳一次从头闪;系统开了「减少动态效果」时改成静止高亮 1.5 秒。目标是属性行 / 子行但块正在编辑(页面上没有属性表)时闪整块。
|
||||
@@ -139,6 +140,20 @@ export interface FunctionFacts {
|
||||
returnType?: Node
|
||||
}
|
||||
|
||||
/** 父类的一项:TS `extends Base<T>` 的 Base(表达式)和它后面的泛型实参 <T>;Python 类参数列表的一项(含 metaclass=…、*bases)。 */
|
||||
export interface BaseFacts { value: Node, typeArguments?: Node }
|
||||
|
||||
export interface ClassFacts {
|
||||
name?: Node // 匿名类没有;赋给变量、类字段的类表达式取变量名、字段名
|
||||
doc?: Node // 同 FunctionFacts.doc
|
||||
access?: Access // 嵌套在语句块里的类没有
|
||||
abstract?: boolean // 只有 TypeScript 填
|
||||
decorators: Node[]
|
||||
typeParameters: Node[] // 每个泛型参数一项
|
||||
bases: BaseFacts[]
|
||||
implements: Node[] // TypeScript 的 implements,每个接口一项
|
||||
}
|
||||
|
||||
export type ImportItem =
|
||||
| { kind: 'named', name: Node, alias?: Node } // import { a as b }、from m import a as b
|
||||
| { kind: 'default', alias: Node } // import x from 'm'
|
||||
@@ -161,16 +176,19 @@ export interface VariableFacts {
|
||||
export interface BlockShape {
|
||||
/** 这一块对应的语法树节点(整条语句)。 */
|
||||
node: Node
|
||||
/** 块自己显示 / 编辑的那一段:复合语句只到主体开始之前,其余是整条语句;函数的说明(doc)也算在里面。 */
|
||||
/** 块自己显示 / 编辑的那一段:复合语句只到主体开始之前,其余是整条语句;函数、类的说明(doc)也算在里面。 */
|
||||
header: SourceSpan
|
||||
children: CodeBlock[]
|
||||
}
|
||||
|
||||
/** 预览成属性表的块:种类和 facts 绑在一起,语言插件漏给 facts 编译不过。 */
|
||||
export interface FunctionBlock extends BlockShape { kind: 'function', facts: FunctionFacts }
|
||||
export interface ClassBlock extends BlockShape { kind: 'class', facts: ClassFacts }
|
||||
export interface ImportBlock extends BlockShape { kind: 'import', facts: ImportFacts }
|
||||
export interface VariableBlock extends BlockShape { kind: 'const' | 'variable', facts: VariableFacts }
|
||||
export type FactBlock = FunctionBlock | ImportBlock | VariableBlock
|
||||
export type FactBlock = FunctionBlock | ClassBlock | ImportBlock | VariableBlock
|
||||
/** 带说明(facts.doc)的块。 */
|
||||
export type DocumentedBlock = FunctionBlock | ClassBlock
|
||||
|
||||
export type TextKind = Exclude<BlockKind, FactBlock['kind']>
|
||||
/** 预览成一行译文的块(注释块是 Markdown 说明)。 */
|
||||
@@ -199,12 +217,12 @@ export interface Segment { text: string, role: TokenRole, offset?: number }
|
||||
*/
|
||||
export interface Markdown { markdown: string }
|
||||
|
||||
/** 预览里显示的一段文字:着色的词(一行译文、属性值),或整段 Markdown 说明(注释块、函数的说明)。 */
|
||||
/** 预览里显示的一段文字:着色的词(一行译文、属性值),或整段 Markdown 说明(注释块、函数和类的说明)。 */
|
||||
export type PreviewText = Segment[] | Markdown
|
||||
|
||||
/**
|
||||
* 属性表里的一行或一个子行讲的是哪个定义:定义出来的那个名字的节点(取自 facts)在源码里的范围——
|
||||
* 参数名(连 `*` 前缀、整个解构模式)、导入项(名称到别名)、声明的变量名。跳到的定义落在里面就只闪这一行;
|
||||
* 参数名(连 `*` 前缀、整个解构模式)、导入项(名称到别名)、声明的变量名、类的泛型参数(整串)。跳到的定义落在里面就只闪这一行;
|
||||
* 第三方翻译插件可以不填,不填就闪整块。
|
||||
*/
|
||||
export interface Defines { defines?: SourceRange }
|
||||
@@ -212,7 +230,7 @@ export interface Defines { defines?: SourceRange }
|
||||
/** 属性表里展开的一个子行:几列对齐的格子。 */
|
||||
export interface PropertyItem extends Defines { cells: Segment[][] }
|
||||
|
||||
/** 属性表的一行:属性名、着色的值或 Markdown 说明(函数的「说明」)、展开在下面的子行(输入参数、导入项)。 */
|
||||
/** 属性表的一行:属性名、着色的值或 Markdown 说明(函数、类的「说明」)、展开在下面的子行(输入参数、导入项)。 */
|
||||
export interface PropertyRow extends Defines { label: string, value: PreviewText, items?: PropertyItem[] }
|
||||
|
||||
/** 语言插件:语法树 → 代码块树。 */
|
||||
@@ -233,7 +251,7 @@ export interface Translator {
|
||||
label: string // '中文直译'
|
||||
languageIds: string[]
|
||||
translate: (block: TextBlock, source: string) => PreviewText // 只翻 header 那一段;注释块给 Markdown 原文
|
||||
describe: (block: FactBlock, source: string) => PropertyRow[] // 函数、导入、常量、变量块 → 属性表
|
||||
describe: (block: FactBlock, source: string) => PropertyRow[] // 函数、类、导入、常量、变量块 → 属性表
|
||||
}
|
||||
|
||||
export interface CodeReaderApi {
|
||||
@@ -245,7 +263,7 @@ export interface CodeReaderApi {
|
||||
- `activate()` 返回 `{ codeReader: CodeReaderApi }`;其他扩展声明 `extensionDependencies` 后用 `extensions.getExtension('clurdra.superpowers-vscode-clurdra')!.exports.codeReader` 注册。
|
||||
- 内置的 TypeScript、Python 语言插件和两个中文直译翻译插件在 `activate()` 里通过同一个 API 注册,没有旁路。
|
||||
- 注册表变化时更新 `superpowers.codeReader.languageIds` 上下文,并让已打开的预览刷新。
|
||||
- 分工:语言插件只从语法树读出和语言无关的事实(facts,引用源码节点,不做正则匹配文字);翻译插件决定属性名、枚举词(公共 / 是 / 默认导出)和值怎么译。第三方语言插件给函数、导入、常量、变量块填 facts 就能用上任何翻译插件的属性表。
|
||||
- 分工:语言插件只从语法树读出和语言无关的事实(facts,引用源码节点,不做正则匹配文字);翻译插件决定属性名、枚举词(公共 / 是 / 默认导出)和值怎么译。第三方语言插件给函数、类、导入、常量、变量块填 facts 就能用上任何翻译插件的属性表。
|
||||
|
||||
## 解析运行时
|
||||
|
||||
@@ -259,13 +277,13 @@ export interface CodeReaderApi {
|
||||
### TypeScript / JavaScript
|
||||
|
||||
- 顶层与语句块内的每条语句一块;连续的行注释合成一块注释。
|
||||
- JSDoc(以 `/**` 开头的块注释,`/**/` 不算)和下一条语句之间只有空白(空行也算,中间隔着装饰器也算)、那条语句是函数块时,它是这个函数的说明:进 `facts.doc`,头部往上延到注释开头,不单独成块。其余情况(后面不是函数块、中间隔着别的注释、在上一条语句的行尾)照旧是注释块。`//` 行注释和普通 `/* */` 块注释不变。
|
||||
- JSDoc(以 `/**` 开头的块注释,`/**/` 不算)和下一条语句之间只有空白(空行也算,中间隔着装饰器也算)、那条语句是函数或类块时,它是这个函数、类的说明:进 `facts.doc`,头部往上延到注释开头,不单独成块。其余情况(后面不是函数、类块,中间隔着别的注释,在上一条语句的行尾)照旧是注释块。`//` 行注释和普通 `/* */` 块注释不变。
|
||||
- `import_statement` → import。
|
||||
- `export_statement` 包着声明 → 按声明定种类,节点取整个 `export_statement`(头部包含 `export`)。
|
||||
- `function_declaration` / `generator_function_declaration` / `method_definition` → function,子块来自主体语句。
|
||||
- 变量声明的唯一声明值是带语句块主体的箭头函数 / 函数表达式 → function(React 组件、回调常见写法),子块来自主体。
|
||||
- 变量声明的唯一声明值是带语句块主体的箭头函数 / 函数表达式 → function(React 组件、回调常见写法),子块来自主体;是类表达式(`const A = class {}`)→ class。
|
||||
- `lexical_declaration` / `variable_declaration`:模块顶层的 `const` → const,其余 → variable。
|
||||
- `class_declaration` → class,子块:方法 → function,字段 → field,注释 → comment。
|
||||
- `class_declaration` / `abstract_class_declaration`、`export default class {}` → class,子块:方法 → function,字段 → field(值是带语句块主体的函数 → function,值是类表达式 → class),注释 → comment。
|
||||
- `interface_declaration` → type,子块:属性 / 方法签名 → field;`type_alias_declaration` → type;`enum_declaration` → type,成员 → field。
|
||||
- `if_statement` → if,子块来自 consequence;`else` / `else if` 展平成紧跟其后的兄弟块 else(`else if` 的头部是 `else if (...)`,子块来自它的 consequence)。
|
||||
- `for` / `for…in` / `for…of` / `while` / `do` → loop。
|
||||
@@ -279,8 +297,8 @@ export interface CodeReaderApi {
|
||||
- `import_statement` / `import_from_statement` / `future_import_statement` → import。
|
||||
- `function_definition` → function;`decorated_definition` 按内部定义定种类,头部包含装饰器。
|
||||
- `class_definition` → class;类体里的带注解赋值 → field。
|
||||
- 模块 / 类主体开头的字符串表达式(docstring)和 `comment` → comment。
|
||||
- 函数主体的第一条语句(注释不算)是单独一个字符串表达式时,它是函数的 docstring:进 `facts.doc`,头部往下延到字符串结尾(它和冒号之间的注释也在头部里),子块从它后面开始,不单独成块。
|
||||
- 模块开头的字符串表达式(docstring)和 `comment` → comment。
|
||||
- 函数、类主体的第一条语句(注释不算)是单独一个字符串表达式时,它是这个函数、类的 docstring:进 `facts.doc`,头部往下延到字符串结尾(它和冒号之间的注释也在头部里),子块从它后面开始,不单独成块。
|
||||
- 赋值:模块顶层且变量名全大写 → const,其余 → variable。
|
||||
- `if_statement` → if;`elif_clause` / `else_clause` → 兄弟块 else;`for` / `while` → loop(循环的 `else` → 兄弟块 else)。
|
||||
- `try_statement` → try;`except_clause` → catch;`finally_clause` → finally。
|
||||
@@ -291,9 +309,11 @@ export interface CodeReaderApi {
|
||||
### 属性表信息(facts)从哪来
|
||||
|
||||
- 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 导入:`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。
|
||||
- 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 导入:`import_statement` 每个 name 一个来源、导入项是整个模块;`import_from_statement` 的 module_name 和各 name(`aliased_import` 拆出 alias),`wildcard_import` 是全部;`future_import_statement` 的来源是 `__future__`。
|
||||
- Python 变量:`assignment` 的 left / type / right。
|
||||
|
||||
@@ -354,6 +374,10 @@ Python 内置类型(仅注解里):str 字符串、int 整数、float 浮
|
||||
| `from .schemas import (⏎ UserAvatarUploadResponse, … )`(routes.py 19–23 行) | 来源 `.schemas` / 导入项 3 个:UserAvatarUploadResponse、UserDetailResponse、UserUpdateRequest |
|
||||
| `payload = await file.read()`(routes.py,函数体里) | 名称 payload / 初始值 `等待 file.read()` |
|
||||
| `_default_port = 5680 if _role == "beat" else 5679` | 名称 _default_port / 访问级别 私有 / 初始值 `如果 _role 等于 "beat" 则 5680 否则 5679` |
|
||||
| `export abstract class Repository<K, T extends Entity = Entity> extends Base<string> implements Loader<T>, Disposable {`(classes.ts 3–9 行,连同前面的 JSDoc 和 `@Injectable()`) | 名称 Repository / 说明 `仓库基类。…` / 访问级别 公共 / 抽象 是 / 装饰器 `@Injectable()` / 类型参数 `K, T 限定为 Entity = Entity` / 父类 `Base<字符串>` / 实现的接口 `Loader<T>, Disposable` |
|
||||
| `export default class {` | 名称 匿名 / 访问级别 公共 / 抽象 否 |
|
||||
| `class Meta(Base, metaclass=ABCMeta):`(classes.py,带 `@dataclass(frozen=True)`) | 名称 Meta / 访问级别 公共 / 装饰器 `@dataclass(frozen=True)` / 父类 `Base, metaclass=ABCMeta` |
|
||||
| `class NotFound(AppException):`(classes.py,连同 docstring) | 名称 NotFound / 说明 `找不到资源。` / 访问级别 公共 / 父类 `AppException` |
|
||||
|
||||
## 预览数据与消息
|
||||
|
||||
@@ -367,7 +391,7 @@ interface BlockViewBase {
|
||||
endLine: number
|
||||
code: string // header 源码,去掉共同缩进
|
||||
codeTokens: Segment[] // 源码着色(拼起来等于 code)
|
||||
defines?: SourceRange // 块自己的名字(函数名、只声明一个变量时的变量名)在源码里的范围,扩展端从 facts 取;跳到的定义是它时闪整块
|
||||
defines?: SourceRange // 块自己的名字(函数名、类名、只声明一个变量时的变量名)在源码里的范围,扩展端从 facts 取;跳到的定义是它时闪整块
|
||||
children: BlockView[]
|
||||
}
|
||||
|
||||
@@ -385,7 +409,7 @@ type BlockView = BlockViewBase & ({ translation: PreviewText } | { properties: P
|
||||
- `{ type: 'ready' }`
|
||||
- `{ type: 'edit', startLine, endLine, text }`:用 `WorkspaceEdit` 把这几行整行替换成 `text`(每行补回原来第一行的缩进)。
|
||||
- `{ 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`);没记的按默认:函数、类块折叠,其余块和属性行展开。眼下折叠着哪些由「默认 + folds」算出来,界面、跳到定义的展开都只看这一份。跳到定义展开祖先块、属性行时也记进 `folds`(记成展开)。读回来的状态逐项核对,旧版本存的、形状不对的项当没存过。文档刷新后保留;正在编辑且有焦点的块,刷新时不覆盖输入框内容。
|
||||
|
||||
## 颜色
|
||||
|
||||
@@ -404,7 +428,7 @@ type BlockView = BlockViewBase & ({ translation: PreviewText } | { properties: P
|
||||
| hint | `--vscode-descriptionForeground` | 同左 |
|
||||
| plain | `--vscode-editor-foreground` | 同左 |
|
||||
|
||||
属性表里:说明按 comment,访问级别(公共 / 私有 / 受保护)和「作为」按 keyword,是 / 否按 storage,函数名称按 function,未标注、无、N 个、默认、默认导出、全部、整个模块、匿名按 hint。
|
||||
属性表里:说明按 comment,访问级别(公共 / 私有 / 受保护)和「作为」按 keyword,是 / 否按 storage,函数名称按 function,类名称按 type,未标注、无、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`。
|
||||
|
||||
@@ -432,15 +456,15 @@ 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 连同它前面的注释进头部。
|
||||
- 块头部:各样例里块的头部行互不重叠;函数说明所在的行在函数块里,「编辑」写回原样不变。
|
||||
- 折叠:默认函数块折叠、其余展开;用户的选择盖过默认、重新加载后还在;旧版本存的状态不报错;跳到定义时默认折叠的函数也会展开。
|
||||
- 语言插件:`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`、`sample.py`、`routes.py`(原型里的 FastAPI 路由)加内联片段,覆盖各种导入写法、形参写法、类成员访问修饰符、解构和多变量声明、Python 下划线规则。
|
||||
- 翻译:上面两张样例表逐字一致;属性名不被翻;每个被翻的词单独成段;节点模板嵌套时正确、槽位里照样翻、没命中模板的节点照抄;属性表里的值和一行译文用同一套表和模板。
|
||||
- `blockView`:`codeTokens` 拼起来等于 `code`;去缩进正确;函数、导入、常量、变量块只带属性表,其余块只带译文。
|
||||
- 注释的 Markdown:注释块、函数「说明」的视图数据是 `{ markdown }`,原文照抄(reST 角色、双反引号、JSDoc 标签、HTML 都不转换),行注释连成的块留得住缩进四格的代码块;渲染时 HTML 当文字、带协议的网址和邮箱成链接、不带协议的文件名不成链接,`javascript:` / `command:` / 相对路径的链接和图片不渲染;跳到定义不认 Markdown 里的名字。
|
||||
- `blockView`:`codeTokens` 拼起来等于 `code`;去缩进正确;函数、类、导入、常量、变量块只带属性表,其余块只带译文。
|
||||
- 注释的 Markdown:注释块、函数和类「说明」的视图数据是 `{ markdown }`,原文照抄(reST 角色、双反引号、JSDoc 标签、HTML 都不转换),行注释连成的块留得住缩进四格的代码块;渲染时 HTML 当文字、带协议的网址和邮箱成链接、不带协议的文件名不成链接,`javascript:` / `command:` / 相对路径的链接和图片不渲染;跳到定义不认 Markdown 里的名字。
|
||||
- 行替换:多行块替换后缩进正确。
|
||||
- 跳到定义:TS / Python 译文和属性表里的名字、点分模块名每一节、模块字符串带下标且指回源码原文,关键字、运算符、说明词不带(前面有中文注释也对得上);查到的定义 → 不动 / 原地 / 预览 / 普通编辑器 / 挑选框;webview 里按下标找目标块、要展开的祖先块和属性行,滚动位置。闪哪一处(TS、Python 源码走完整的语言插件 + 翻译插件 + 视图数据):参数 → 它那个子行,导入项 → 它那一项,多变量声明里的一个 → 它的名称行,函数名 / 单个变量名(含解构模式)→ 整块,循环变量 → 所在的块,翻译插件没填 `defines` → 整块(照样展开属性行);翻译插件给参数、导入项、声明的变量标的范围指回源码原文;块自己的名字只有函数和单变量声明有。
|
||||
- 跳到定义:TS / Python 译文和属性表里的名字、点分模块名每一节、模块字符串带下标且指回源码原文,关键字、运算符、说明词不带(前面有中文注释也对得上);查到的定义 → 不动 / 原地 / 预览 / 普通编辑器 / 挑选框;webview 里按下标找目标块、要展开的祖先块和属性行,滚动位置。闪哪一处(TS、Python 源码走完整的语言插件 + 翻译插件 + 视图数据):参数 → 它那个子行,导入项 → 它那一项,多变量声明里的一个 → 它的名称行,类的泛型参数 → 「类型参数」那一行,函数名 / 类名 / 单个变量名(含解构模式)→ 整块,循环变量 → 所在的块,翻译插件没填 `defines` → 整块(照样展开属性行);翻译插件给参数、导入项、声明的变量、泛型参数标的范围指回源码原文;块自己的名字只有函数、类和单变量声明有。
|
||||
- 注册表:注册 / 注销后 `languageIds` 与可选翻译正确;同一语言多个翻译时按 `globalState` 选中。
|
||||
|
||||
## 收尾
|
||||
|
||||
@@ -29,11 +29,13 @@ function byId(list: BlockView[], id: string): BlockView {
|
||||
const SAMPLES: [LanguageAdapter, string, Translator, string][] = [
|
||||
[ts, 'typescript', typescriptZhTranslator, 'envLock.ts.txt'],
|
||||
[ts, 'typescript', typescriptZhTranslator, 'issueLock.ts.txt'],
|
||||
[ts, 'typescript', typescriptZhTranslator, 'classes.ts.txt'],
|
||||
[py, 'python', pythonZhTranslator, 'sample.py'],
|
||||
[py, 'python', pythonZhTranslator, 'routes.py'],
|
||||
[py, 'python', pythonZhTranslator, 'classes.py'],
|
||||
]
|
||||
|
||||
const PROPERTY_KINDS = new Set(['function', 'import', 'const', 'variable'])
|
||||
const PROPERTY_KINDS = new Set(['function', 'class', 'import', 'const', 'variable'])
|
||||
|
||||
function translationText(view: BlockView): string {
|
||||
if (!('translation' in view))
|
||||
@@ -57,7 +59,7 @@ describe('blockView', () => {
|
||||
}
|
||||
})
|
||||
|
||||
it.each(SAMPLES)('%#: 函数、导入、常量、变量块只带属性表,其余块只带一行译文', async (adapter, languageId, translator, fixture) => {
|
||||
it.each(SAMPLES)('%#: 函数、类、导入、常量、变量块只带属性表,其余块只带一行译文', async (adapter, languageId, translator, fixture) => {
|
||||
for (const view of flatten(await views(adapter, languageId, translator, readFixture(fixture)))) {
|
||||
expect('properties' in view, view.id).toBe(PROPERTY_KINDS.has(view.kind))
|
||||
expect('translation' in view, view.id).toBe(!PROPERTY_KINDS.has(view.kind))
|
||||
@@ -100,6 +102,26 @@ describe('blockView', () => {
|
||||
expect(getUser?.children[0]).toMatchObject({ kind: 'comment', startLine: 38, endLine: 38 })
|
||||
})
|
||||
|
||||
it('类的说明同样算在类块的头部里:JSDoc 连着中间的装饰器,docstring 连着它前面的注释', async () => {
|
||||
const repo = byId(await views(ts, 'typescript', typescriptZhTranslator, readFixture('classes.ts.txt')), '1')
|
||||
expect(repo).toMatchObject({ kind: 'class', startLine: 2, endLine: 8 })
|
||||
expect(repo.code.split('\n').slice(-3)).toEqual([
|
||||
' */',
|
||||
'@Injectable()',
|
||||
'export abstract class Repository<K, T extends Entity = Entity> extends Base<string> implements Loader<T>, Disposable {',
|
||||
])
|
||||
expect('properties' in repo && repo.properties[1]).toEqual({ label: '说明', value: { markdown: '仓库基类。\n\n子类实现 `load`。' } })
|
||||
|
||||
const notFound = byId(await views(py, 'python', pythonZhTranslator, readFixture('classes.py')), '3')
|
||||
expect(notFound).toMatchObject({ kind: 'class', startLine: 17, endLine: 19 })
|
||||
expect(notFound.code.split('\n')).toEqual([
|
||||
'class NotFound(AppException): # 行尾',
|
||||
' # 说明前面的注释',
|
||||
' """找不到资源。"""',
|
||||
])
|
||||
expect(notFound.children[0]).toMatchObject({ kind: 'variable', startLine: 20, endLine: 20 })
|
||||
})
|
||||
|
||||
it('注释块的译文、函数的「说明」是 Markdown 原文,webview 按它渲染', async () => {
|
||||
const env = await views(ts, 'typescript', typescriptZhTranslator, readFixture('envLock.ts.txt'))
|
||||
expect(byId(env, '0')).toMatchObject({ kind: 'comment', translation: { markdown: expect.stringMatching(/^\.env file lock \/ unlock helpers\.\n\nRecursively/) } })
|
||||
@@ -157,13 +179,13 @@ describe('blockView', () => {
|
||||
expect(signature.filter(token => token.text === 'str')).toEqual([{ text: 'str', role: 'type' }, { text: 'str', role: 'type' }])
|
||||
})
|
||||
|
||||
it('块自己的名字(defines):函数名、只声明一个变量时的变量名(解构模式整个算);导入、一条语句几个变量、一行译文的块没有', async () => {
|
||||
it('块自己的名字(defines):函数名、类名(赋给变量的类表达式取变量名)、只声明一个变量时的变量名(解构模式整个算);导入、一条语句几个变量、一行译文的块、匿名类没有', async () => {
|
||||
const own = (source: string, list: BlockView[]): Record<string, string | undefined> =>
|
||||
Object.fromEntries(list.map(view => [view.id, view.defines && source.slice(view.defines.startIndex, view.defines.endIndex)]))
|
||||
const pySource = ['import os', 'def f(x): pass', 'a, b = 1, 2', 'for i in x: pass'].join('\n')
|
||||
expect(own(pySource, await views(py, 'python', pythonZhTranslator, pySource))).toEqual({ 0: undefined, 1: 'f', 2: 'a, b', 3: undefined })
|
||||
const tsSource = ['let i = 0, j', 'const { u, v } = o', 'export const h = () => 1'].join('\n')
|
||||
expect(own(tsSource, await views(ts, 'typescript', typescriptZhTranslator, tsSource))).toEqual({ 0: undefined, 1: '{ u, v }', 2: 'h' })
|
||||
const pySource = ['import os', 'def f(x): pass', 'a, b = 1, 2', 'for i in x: pass', 'class K: pass'].join('\n')
|
||||
expect(own(pySource, await views(py, 'python', pythonZhTranslator, pySource))).toEqual({ 0: undefined, 1: 'f', 2: 'a, b', 3: undefined, 4: 'K' })
|
||||
const tsSource = ['let i = 0, j', 'const { u, v } = o', 'export const h = () => 1', 'class A {}', 'const B = class C {}', 'export default class {}'].join('\n')
|
||||
expect(own(tsSource, await views(ts, 'typescript', typescriptZhTranslator, tsSource))).toEqual({ 0: undefined, 1: '{ u, v }', 2: 'h', 3: 'A', 4: 'B', 5: undefined })
|
||||
})
|
||||
|
||||
it('\\r\\n 和制表符缩进:code 用 \\n 连接,去掉共同的制表符', async () => {
|
||||
|
||||
@@ -18,7 +18,7 @@ interface BlockViewBase {
|
||||
children: BlockView[]
|
||||
}
|
||||
|
||||
/** 发给 webview 的一块(可序列化):函数、导入、常量、变量块预览时是属性表,注释块是 Markdown 说明,其余是一行译文。 */
|
||||
/** 发给 webview 的一块(可序列化):函数、类、导入、常量、变量块预览时是属性表,注释块是 Markdown 说明,其余是一行译文。 */
|
||||
export type BlockView = BlockViewBase & ({ translation: PreviewText } | { properties: PropertyRow[] })
|
||||
|
||||
interface Line { start: number, end: number }
|
||||
@@ -83,12 +83,13 @@ function colorLines(root: Node, source: string, lines: Line[], startRow: number,
|
||||
}
|
||||
|
||||
/**
|
||||
* 这块本身就是哪个名字的定义:函数名,只声明一个变量(或一个解构模式)时的变量名。
|
||||
* 这块本身就是哪个名字的定义:函数名、类名,只声明一个变量(或一个解构模式)时的变量名。
|
||||
* 导入、一条语句声明几个变量时没有,它们的名字各是属性表里的一行(PropertyRow.defines)。
|
||||
*/
|
||||
function ownName(block: CodeBlock): Node | undefined {
|
||||
switch (block.kind) {
|
||||
case 'function':
|
||||
case 'class':
|
||||
return block.facts.name
|
||||
case 'const':
|
||||
case 'variable':
|
||||
|
||||
@@ -42,6 +42,33 @@ export interface FunctionFacts {
|
||||
returnType?: Node
|
||||
}
|
||||
|
||||
/**
|
||||
* 父类的一项:TypeScript `extends Base<T>` 里的 Base(一个表达式)和跟在它后面的泛型实参 `<T>`(语法里是两个节点);
|
||||
* Python 类参数列表里的一项,`metaclass=ABCMeta` 这类关键字参数、`*bases` 也算。
|
||||
*/
|
||||
export interface BaseFacts {
|
||||
value: Node
|
||||
typeArguments?: Node
|
||||
}
|
||||
|
||||
export interface ClassFacts {
|
||||
/** 匿名类(`export default class {}`)没有;赋给变量、类字段的类表达式取变量名、字段名。 */
|
||||
name?: Node
|
||||
/** 类自己的说明,同 FunctionFacts.doc:TypeScript 是紧贴在前面的 JSDoc,Python 是主体开头的 docstring。 */
|
||||
doc?: Node
|
||||
/** 同函数:嵌套在函数体、条件等语句块里的类没有。 */
|
||||
access?: Access
|
||||
/** 抽象类(TypeScript 的 `abstract class`);没有这个概念的语言不填。 */
|
||||
abstract?: boolean
|
||||
decorators: Node[]
|
||||
/** 泛型参数,每个参数一项(TypeScript `<T extends X = Y>` 里的 `T extends X = Y`)。 */
|
||||
typeParameters: Node[]
|
||||
/** 按源码顺序。 */
|
||||
bases: BaseFacts[]
|
||||
/** 实现的接口(TypeScript 的 `implements A, B<T>`),每个接口一项。 */
|
||||
implements: Node[]
|
||||
}
|
||||
|
||||
/** 导入的一项。 */
|
||||
export type ImportItem
|
||||
= | { kind: 'named', name: Node, alias?: Node } // import { a as b }、from m import a as b
|
||||
@@ -77,7 +104,7 @@ export interface BlockShape {
|
||||
node: Node
|
||||
/**
|
||||
* 块自己显示 / 编辑的那一段:复合语句只到主体开始之前,其余是整条语句。
|
||||
* 函数的说明(FunctionFacts.doc)也算在里面:JSDoc 往上延到注释开头,docstring 往下延到字符串结尾。
|
||||
* 函数、类的说明(facts.doc)也算在里面:JSDoc 往上延到注释开头,docstring 往下延到字符串结尾。
|
||||
*/
|
||||
header: SourceSpan
|
||||
children: CodeBlock[]
|
||||
@@ -85,9 +112,13 @@ export interface BlockShape {
|
||||
|
||||
/** 预览成属性表的块:语言插件从语法树读出结构化信息(facts),翻译插件把它排成属性表。 */
|
||||
export interface FunctionBlock extends BlockShape { kind: 'function', facts: FunctionFacts }
|
||||
export interface ClassBlock extends BlockShape { kind: 'class', facts: ClassFacts }
|
||||
export interface ImportBlock extends BlockShape { kind: 'import', facts: ImportFacts }
|
||||
export interface VariableBlock extends BlockShape { kind: 'const' | 'variable', facts: VariableFacts }
|
||||
export type FactBlock = FunctionBlock | ImportBlock | VariableBlock
|
||||
export type FactBlock = FunctionBlock | ClassBlock | ImportBlock | VariableBlock
|
||||
|
||||
/** 带说明(facts.doc)的块。 */
|
||||
export type DocumentedBlock = FunctionBlock | ClassBlock
|
||||
|
||||
export type TextKind = Exclude<BlockKind, FactBlock['kind']>
|
||||
|
||||
@@ -116,12 +147,12 @@ export interface Segment { text: string, role: TokenRole, offset?: number }
|
||||
*/
|
||||
export interface Markdown { markdown: string }
|
||||
|
||||
/** 预览里显示的一段文字:着色的词(一行译文、属性值),或整段 Markdown 说明(注释块、函数的说明)。 */
|
||||
/** 预览里显示的一段文字:着色的词(一行译文、属性值),或整段 Markdown 说明(注释块、函数和类的说明)。 */
|
||||
export type PreviewText = Segment[] | Markdown
|
||||
|
||||
/**
|
||||
* 属性表里的一行或一个子行讲的是哪个定义:定义出来的那个名字的节点(取自 facts,例如参数名、导入项的名称和别名、
|
||||
* 声明的变量名)在源码里的范围。跳到定义时,定义处落在这个范围里就只闪这一行;不填(第三方翻译插件可以不填)就闪整块。
|
||||
* 声明的变量名、类的泛型参数)在源码里的范围。跳到定义时,定义处落在这个范围里就只闪这一行;不填(第三方翻译插件可以不填)就闪整块。
|
||||
*/
|
||||
export interface Defines { defines?: SourceRange }
|
||||
|
||||
@@ -154,7 +185,7 @@ export interface Translator {
|
||||
languageIds: string[]
|
||||
/** 只翻 header 那一段;注释块给去掉注释符号的 Markdown 原文。 */
|
||||
translate: (block: TextBlock, source: string) => PreviewText
|
||||
/** 函数、导入、常量、变量块 → 属性表。 */
|
||||
/** 函数、类、导入、常量、变量块 → 属性表。 */
|
||||
describe: (block: FactBlock, source: string) => PropertyRow[]
|
||||
}
|
||||
|
||||
|
||||
@@ -37,9 +37,11 @@ describe('行替换', () => {
|
||||
it.each([
|
||||
[createTypeScriptAdapter(WASM_DIR), 'typescript', 'envLock.ts.txt'],
|
||||
[createTypeScriptAdapter(WASM_DIR), 'typescript', 'issueLock.ts.txt'],
|
||||
[createTypeScriptAdapter(WASM_DIR), 'typescript', 'classes.ts.txt'],
|
||||
[createPythonAdapter(WASM_DIR), 'python', 'sample.py'],
|
||||
[createPythonAdapter(WASM_DIR), 'python', 'routes.py'],
|
||||
] as const)('%#: 把预览里的 code 原样写回,文件不变(去缩进与补缩进互逆,函数头部连着说明也一样)', async (adapter, languageId, fixture) => {
|
||||
[createPythonAdapter(WASM_DIR), 'python', 'classes.py'],
|
||||
] as const)('%#: 把预览里的 code 原样写回,文件不变(去缩进与补缩进互逆,函数、类头部连着说明也一样)', async (adapter, languageId, fixture) => {
|
||||
const source = readFixture(fixture)
|
||||
const views = await withBlocks(adapter, languageId, source, blocks => toBlockViews(blocks, source, adapter, undefined))
|
||||
const all = views.flatMap(function flat(view): typeof views {
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
"""领域异常。"""
|
||||
|
||||
from abc import ABCMeta
|
||||
|
||||
|
||||
class AppException(Exception):
|
||||
"""应用异常的基类。
|
||||
|
||||
带上 HTTP 状态码。
|
||||
"""
|
||||
|
||||
status_code: int = 500
|
||||
|
||||
def __init__(self, message: str) -> None:
|
||||
self.message = message
|
||||
|
||||
|
||||
class NotFound(AppException): # 行尾
|
||||
# 说明前面的注释
|
||||
"""找不到资源。"""
|
||||
status_code = 404
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class Meta(Base, metaclass=ABCMeta):
|
||||
class Config:
|
||||
extra = "forbid"
|
||||
|
||||
class _Hidden:
|
||||
pass
|
||||
|
||||
|
||||
class _Private:
|
||||
pass
|
||||
|
||||
|
||||
def factory():
|
||||
class Local:
|
||||
"""局部类。"""
|
||||
return Local
|
||||
@@ -0,0 +1,32 @@
|
||||
import { Injectable } from './di'
|
||||
|
||||
/**
|
||||
* 仓库基类。
|
||||
*
|
||||
* 子类实现 `load`。
|
||||
*/
|
||||
@Injectable()
|
||||
export abstract class Repository<K, T extends Entity = Entity> extends Base<string> implements Loader<T>, Disposable {
|
||||
/** 缓存 */
|
||||
private readonly cache = new Map<K, T>()
|
||||
|
||||
abstract load(id: K): Promise<T>
|
||||
|
||||
static Query = class extends QueryBase {
|
||||
run() {}
|
||||
}
|
||||
}
|
||||
|
||||
export default class {
|
||||
x = 1
|
||||
}
|
||||
|
||||
/** 类表达式 */
|
||||
export const Mixed = class Inner extends mixin(A, B) {}
|
||||
|
||||
function make() {
|
||||
class Local {}
|
||||
return Local
|
||||
}
|
||||
|
||||
@sealed class Plain {}
|
||||
@@ -90,7 +90,7 @@ describe('python 代码块树', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('python 函数的 docstring', () => {
|
||||
describe('python 函数、类的 docstring', () => {
|
||||
const source = [
|
||||
'class K:',
|
||||
' """类说明"""',
|
||||
@@ -105,10 +105,9 @@ describe('python 函数的 docstring', () => {
|
||||
'def g(): "单行"',
|
||||
].join('\n')
|
||||
|
||||
it('算进函数头部、不单独成块,它前面的注释也在头部里;后面的注释照旧成块;类的 docstring 照旧是注释块', async () => {
|
||||
it('算进函数、类头部、不单独成块,它前面的注释也在头部里;后面的注释照旧成块', async () => {
|
||||
expect(await tree(source)).toEqual([
|
||||
{ at: 1, kind: 'class', kids: [
|
||||
{ at: 2, kind: 'comment' },
|
||||
{ at: [1, 2], kind: 'class', kids: [
|
||||
{ at: [3, 8], kind: 'function', kids: [
|
||||
{ at: 9, kind: 'comment' },
|
||||
{ at: 10, kind: 'return' },
|
||||
@@ -118,9 +117,11 @@ describe('python 函数的 docstring', () => {
|
||||
])
|
||||
})
|
||||
|
||||
it('进 facts.doc:是那个字符串节点;没有 docstring 的函数没有', async () => {
|
||||
it('进 facts.doc:是那个字符串节点;没有 docstring 的函数没有;模块开头的 docstring 照旧是注释块', async () => {
|
||||
const found = await facts(source)
|
||||
expect(found[1]).toMatchObject({ name: 'K', doc: '"""类说明"""' })
|
||||
expect(found[3]).toMatchObject({ name: 'run', doc: '"""跑一遍。\n\n 细节。\n """' })
|
||||
expect(await tree('"""模块说明"""\nclass K:\n pass')).toEqual([{ at: 1, kind: 'comment' }, { at: 2, kind: 'class', kids: [{ at: 3, kind: 'statement' }] }])
|
||||
expect(found[11]).toMatchObject({ name: 'g', doc: '"单行"' })
|
||||
expect(await facts('def f():\n x = 1\n """not a docstring"""')).not.toHaveProperty('1.doc')
|
||||
})
|
||||
@@ -236,3 +237,63 @@ describe('python 属性表信息(facts)', () => {
|
||||
expect(found[10]).not.toHaveProperty('access')
|
||||
})
|
||||
})
|
||||
|
||||
describe('python 类', () => {
|
||||
it('classes.py:docstring(连同它前面的注释)算进类头部,类里的类、函数里的类照常成块', async () => {
|
||||
expect(await tree(readFixture('classes.py'))).toEqual([
|
||||
{ at: 1, kind: 'comment' },
|
||||
{ at: 3, kind: 'import' },
|
||||
{ at: [6, 10], kind: 'class', kids: [
|
||||
{ at: 12, kind: 'field' },
|
||||
{ at: 14, kind: 'function', kids: [{ at: 15, kind: 'variable' }] },
|
||||
] },
|
||||
{ at: [18, 20], kind: 'class', kids: [{ at: 21, kind: 'variable' }] },
|
||||
{ at: [24, 25], kind: 'class', kids: [
|
||||
{ at: 26, kind: 'class', kids: [{ at: 27, kind: 'variable' }] },
|
||||
{ at: 29, kind: 'class', kids: [{ at: 30, kind: 'statement' }] },
|
||||
] },
|
||||
{ at: 33, kind: 'class', kids: [{ at: 34, kind: 'statement' }] },
|
||||
{ at: 37, kind: 'function', kids: [
|
||||
{ at: [38, 39], kind: 'class' },
|
||||
{ at: 40, kind: 'return' },
|
||||
] },
|
||||
])
|
||||
})
|
||||
|
||||
it('classes.py:名称、说明、访问级别(同函数的下划线规则,类里的类也按它,函数里的没有)、装饰器、父类(参数列表照写的顺序,关键字参数也算)', async () => {
|
||||
const found = await facts(readFixture('classes.py'))
|
||||
expect(found[6]).toEqual({
|
||||
name: 'AppException',
|
||||
doc: '"""应用异常的基类。\n\n 带上 HTTP 状态码。\n """',
|
||||
access: 'public',
|
||||
decorators: [],
|
||||
typeParameters: [],
|
||||
bases: [{ value: 'Exception' }],
|
||||
implements: [],
|
||||
})
|
||||
expect(found[18]).toMatchObject({ name: 'NotFound', doc: '"""找不到资源。"""', bases: [{ value: 'AppException' }] })
|
||||
expect(found[24]).toEqual({
|
||||
name: 'Meta',
|
||||
access: 'public',
|
||||
decorators: ['@dataclass(frozen=True)'],
|
||||
typeParameters: [],
|
||||
bases: [{ value: 'Base' }, { value: 'metaclass=ABCMeta' }],
|
||||
implements: [],
|
||||
})
|
||||
expect(found[26]).toMatchObject({ name: 'Config', access: 'public', bases: [] })
|
||||
expect(found[29]).toMatchObject({ name: '_Hidden', access: 'private' })
|
||||
expect(found[33]).toMatchObject({ name: '_Private', access: 'private' })
|
||||
expect(found[38]).toEqual({ name: 'Local', doc: '"""局部类。"""', decorators: [], typeParameters: [], bases: [], implements: [] })
|
||||
})
|
||||
|
||||
it('没有「抽象」这个概念;Python 3.12 的泛型参数进 typeParameters;*bases、**kw 也是父类里的一项', async () => {
|
||||
expect((await facts('class G[T](Generic[T], *mixins, **kw):\n pass'))[1]).toEqual({
|
||||
name: 'G',
|
||||
access: 'public',
|
||||
decorators: [],
|
||||
typeParameters: ['T'],
|
||||
bases: [{ value: 'Generic[T]' }, { value: '*mixins' }, { value: '**kw' }],
|
||||
implements: [],
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { Access, BlockShape, CodeBlock, FunctionFacts, ImportFacts, ImportItem, LanguageAdapter, Node, ParameterFacts, TextKind, TokenRole } from '../contract'
|
||||
import type { Access, BlockShape, ClassFacts, CodeBlock, FunctionFacts, ImportFacts, ImportItem, LanguageAdapter, Node, ParameterFacts, TextKind, TokenRole } from '../contract'
|
||||
import type { CommentRules } from './shared'
|
||||
import * as path from 'node:path'
|
||||
import { children, compoundShape, leafBlock, leafShape, listBlocks, named, sameNode, syntaxChildren } from './shared'
|
||||
@@ -9,7 +9,7 @@ type Scope = 'module' | 'class' | 'other'
|
||||
const CONSTANT_NAME = /^[A-Z][A-Z0-9_]*$/
|
||||
const PARAMETER_SEPARATORS = new Set(['keyword_separator', 'positional_separator'])
|
||||
const PATTERNS = new Set(['pattern_list', 'tuple_pattern', 'list_pattern'])
|
||||
// 只有 # 行注释;函数的说明是主体开头的 docstring,不是写在前面的注释
|
||||
// 只有 # 行注释;函数、类的说明是主体开头的 docstring,不是写在前面的注释
|
||||
const COMMENTS: CommentRules = { isLine: () => true, isDoc: () => false }
|
||||
|
||||
function statements(items: Node[], scope: Scope, afterRow: number, docstring: boolean): CodeBlock[] {
|
||||
@@ -73,14 +73,12 @@ function statementBlocks(node: Node, scope: Scope, docstring: boolean): CodeBloc
|
||||
/** function_definition / class_definition;outer 是带装饰器的整体时头部从装饰器开始。 */
|
||||
function definitionBlock(definition: Node, outer: Node, scope: Scope): CodeBlock {
|
||||
const body = definition.childForFieldName('body')
|
||||
if (definition.type === 'class_definition')
|
||||
return { kind: 'class', ...bodyShape(outer, body, 'class', true) }
|
||||
const doc = docstringOf(body)
|
||||
return {
|
||||
kind: 'function',
|
||||
...(body && doc ? documentedShape(outer, body, doc) : bodyShape(outer, body, 'other')),
|
||||
facts: functionFacts(definition, outer, scope, doc),
|
||||
}
|
||||
const inner: Scope = definition.type === 'class_definition' ? 'class' : 'other'
|
||||
const shape = body && doc ? documentedShape(outer, body, doc, inner) : bodyShape(outer, body, inner)
|
||||
if (definition.type === 'class_definition')
|
||||
return { kind: 'class', ...shape, facts: classFacts(definition, outer, scope, doc) }
|
||||
return { kind: 'function', ...shape, facts: functionFacts(definition, outer, scope, doc) }
|
||||
}
|
||||
|
||||
/** 主体开头的 docstring:第一条语句(注释不算)是单独一个字符串表达式时,返回那个字符串节点。 */
|
||||
@@ -97,10 +95,10 @@ function docstringIn(statement: Node): Node | undefined {
|
||||
return rest.length === 0 && (expression?.type === 'string' || expression?.type === 'concatenated_string') ? expression : undefined
|
||||
}
|
||||
|
||||
/** 函数的 docstring 是它自己的说明:算进头部(连同它前面的注释),子块从它后面开始。 */
|
||||
function documentedShape(outer: Node, body: Node, doc: Node): BlockShape {
|
||||
/** 函数、类的 docstring 是它自己的说明:算进头部(连同它前面的注释),子块从它后面开始。 */
|
||||
function documentedShape(outer: Node, body: Node, doc: Node, scope: Scope): BlockShape {
|
||||
const rest = named(body).filter(item => item.startIndex >= doc.endIndex)
|
||||
return compoundShape(outer, doc.endIndex, rest, (list, afterRow) => statements(list, 'other', afterRow, false))
|
||||
return compoundShape(outer, doc.endIndex, rest, (list, afterRow) => statements(list, scope, afterRow, false))
|
||||
}
|
||||
|
||||
/** 单下划线开头的名字是私有(`_x`、`__x`),前后双下划线的特殊方法(`__init__`)是公共。 */
|
||||
@@ -140,7 +138,7 @@ function functionFacts(definition: Node, outer: Node, scope: Scope, doc: Node |
|
||||
doc,
|
||||
access: name ? scopeAccess(name, scope) : undefined,
|
||||
async: children(definition).some(child => child.type === 'async'),
|
||||
decorators: named(outer).filter(child => child.type === 'decorator'),
|
||||
decorators: decoratorsOf(outer),
|
||||
parameters: syntaxChildren(definition.childForFieldName('parameters'))
|
||||
.filter(param => !PARAMETER_SEPARATORS.has(param.type))
|
||||
.map(parameterFacts),
|
||||
@@ -148,6 +146,26 @@ function functionFacts(definition: Node, outer: Node, scope: Scope, doc: Node |
|
||||
}
|
||||
}
|
||||
|
||||
/** 父类是类名后面括号里的参数列表,照写的顺序一项一项取,`metaclass=…` 这类关键字参数也在里面。 */
|
||||
function classFacts(definition: Node, outer: Node, scope: Scope, doc: Node | undefined): ClassFacts {
|
||||
const name = definition.childForFieldName('name')
|
||||
return {
|
||||
name: name ?? undefined,
|
||||
doc,
|
||||
access: name ? scopeAccess(name, scope) : undefined,
|
||||
decorators: decoratorsOf(outer),
|
||||
// Python 3.12 的 `class G[T]:`
|
||||
typeParameters: syntaxChildren(definition.childForFieldName('type_parameters')),
|
||||
bases: syntaxChildren(definition.childForFieldName('superclasses')).map(value => ({ value })),
|
||||
implements: [],
|
||||
}
|
||||
}
|
||||
|
||||
/** 装饰器在 decorated_definition 里,和被装饰的定义是兄弟节点。 */
|
||||
function decoratorsOf(outer: Node): Node[] {
|
||||
return named(outer).filter(child => child.type === 'decorator')
|
||||
}
|
||||
|
||||
/** 带注解的参数(typed_parameter)语法没给名字起字段名,名字是第一个具名子节点(可能是 `*args`)。 */
|
||||
function parameterFacts(param: Node): ParameterFacts {
|
||||
switch (param.type) {
|
||||
@@ -301,6 +319,9 @@ function identifierRole(leaf: Node): TokenRole {
|
||||
return 'function'
|
||||
if (parent.type === 'class_definition' && sameNode(parent.childForFieldName('name'), leaf))
|
||||
return 'type'
|
||||
// 父类:类名后面括号里直接写的名字
|
||||
if (parent.type === 'argument_list' && parent.parent?.type === 'class_definition')
|
||||
return 'type'
|
||||
if (parent.type === 'decorator' || isCallee(leaf))
|
||||
return 'function'
|
||||
if (parent.type === 'attribute' && sameNode(parent.childForFieldName('attribute'), leaf) && isCallee(parent))
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { BlockShape, CodeBlock, FunctionBlock, Node, SourceSpan, TextKind } from '../contract'
|
||||
import type { BlockShape, CodeBlock, DocumentedBlock, Node, SourceSpan, TextKind } from '../contract'
|
||||
|
||||
/** 语言插件共用的建树工具:取子节点、算头部、把一串语句切成块。 */
|
||||
|
||||
@@ -89,19 +89,19 @@ export function compoundShape(outer: Node, bodyStart: number, items: Node[], lis
|
||||
export interface CommentRules {
|
||||
/** 行注释:相邻几行合成一块。 */
|
||||
isLine: (comment: Node) => boolean
|
||||
/** 文档注释(JSDoc):紧贴在函数块前面时是这个函数的说明,见 FunctionFacts.doc。 */
|
||||
/** 文档注释(JSDoc):紧贴在函数、类块前面时是它的说明,见 FunctionFacts.doc、ClassFacts.doc。 */
|
||||
isDoc: (comment: Node) => boolean
|
||||
}
|
||||
|
||||
/** 函数块带上说明:说明进 facts.doc,头部往上延到它开头。 */
|
||||
function documented(block: FunctionBlock, doc: Node): FunctionBlock {
|
||||
/** 函数、类块带上说明:说明进 facts.doc,头部往上延到它开头。 */
|
||||
function documented<B extends DocumentedBlock>(block: B, doc: Node): B {
|
||||
return { ...extendHeader(block, doc), facts: { ...block.facts, doc } }
|
||||
}
|
||||
|
||||
/**
|
||||
* 一串语句 → 块。注释单独成块,相邻行的行注释合成一块;
|
||||
* 和前一条语句(或头部)同一行的注释是行尾注释,跟着那一行显示,不单独成块。
|
||||
* 文档注释和下一条语句之间只有空白、下一条语句是函数块时,它是这个函数的说明,不单独成块;
|
||||
* 文档注释和下一条语句之间只有空白、下一条语句是函数或类块时,它是这个函数、类的说明,不单独成块;
|
||||
* 中间隔着的装饰器(不出块的语句)不算隔开。
|
||||
* afterRow 是这串语句之前的最后一行(头部结尾行),模块顶层传 -1;
|
||||
* toBlocks 的 first 表示它是这串里第一条非注释语句(Python 认 docstring 用)。
|
||||
@@ -145,7 +145,7 @@ export function listBlocks(
|
||||
else {
|
||||
const blocks = toBlocks(item, first)
|
||||
const [head] = blocks
|
||||
if (head?.kind === 'function' && doc) {
|
||||
if ((head?.kind === 'function' || head?.kind === 'class') && doc) {
|
||||
blocks[0] = documented(head, doc)
|
||||
doc = undefined
|
||||
}
|
||||
|
||||
@@ -424,3 +424,61 @@ describe('typeScript 属性表信息(facts)', () => {
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('typeScript 类', () => {
|
||||
it('classes.ts:紧贴的 JSDoc(隔着装饰器也算)算进类头部;类表达式赋给变量、类字段也是类块;函数里的类照常成块', async () => {
|
||||
expect(await tree(readFixture('classes.ts.txt'))).toEqual([
|
||||
{ at: 1, kind: 'import' },
|
||||
{ at: [3, 9], kind: 'class', kids: [
|
||||
{ at: 10, kind: 'comment' },
|
||||
{ at: 11, kind: 'field' },
|
||||
{ at: 13, kind: 'function' },
|
||||
{ at: 15, kind: 'class', kids: [{ at: 16, kind: 'function' }] },
|
||||
] },
|
||||
{ at: 20, kind: 'class', kids: [{ at: 21, kind: 'field' }] },
|
||||
{ at: [24, 25], kind: 'class' },
|
||||
{ at: 27, kind: 'function', kids: [
|
||||
{ at: 28, kind: 'class' },
|
||||
{ at: 29, kind: 'return' },
|
||||
] },
|
||||
{ at: 32, kind: 'class' },
|
||||
])
|
||||
})
|
||||
|
||||
it('classes.ts:名称、说明、访问级别、抽象、装饰器、泛型参数、父类(泛型实参归它)、实现的接口', async () => {
|
||||
const found = await facts(readFixture('classes.ts.txt'))
|
||||
expect(found[3]).toEqual({
|
||||
name: 'Repository',
|
||||
doc: '/**\n * 仓库基类。\n *\n * 子类实现 `load`。\n */',
|
||||
access: 'public',
|
||||
abstract: true,
|
||||
decorators: ['@Injectable()'],
|
||||
typeParameters: ['K', 'T extends Entity = Entity'],
|
||||
bases: [{ value: 'Base', typeArguments: '<string>' }],
|
||||
implements: ['Loader<T>', 'Disposable'],
|
||||
})
|
||||
// 类字段的值是类表达式:名称取字段名,访问级别同类成员
|
||||
expect(found[15]).toEqual({ name: 'Query', access: 'public', abstract: false, decorators: [], typeParameters: [], bases: [{ value: 'QueryBase' }], implements: [] })
|
||||
expect(found[20]).toEqual({ access: 'public', abstract: false, decorators: [], typeParameters: [], bases: [], implements: [] })
|
||||
// 赋给变量的类表达式:名称取变量名
|
||||
expect(found[24]).toMatchObject({ name: 'Mixed', doc: '/** 类表达式 */', access: 'public', bases: [{ value: 'mixin(A, B)' }] })
|
||||
expect(found[28]).toEqual({ name: 'Local', abstract: false, decorators: [], typeParameters: [], bases: [], implements: [] })
|
||||
expect(found[32]).toMatchObject({ name: 'Plain', access: 'private', decorators: ['@sealed'] })
|
||||
})
|
||||
|
||||
it('装饰器写在 export 后面也算;export default class Foo 是公共;JavaScript 的父类直接跟在 extends 后面', async () => {
|
||||
const source = [
|
||||
'export @dec class A {}',
|
||||
'export default class B extends C {}',
|
||||
'const D = class {}',
|
||||
].join('\n')
|
||||
expect(await facts(source)).toEqual({
|
||||
1: { name: 'A', access: 'public', abstract: false, decorators: ['@dec'], typeParameters: [], bases: [], implements: [] },
|
||||
2: { name: 'B', access: 'public', abstract: false, decorators: [], typeParameters: [], bases: [{ value: 'C' }], implements: [] },
|
||||
3: { name: 'D', access: 'private', abstract: false, decorators: [], typeParameters: [], bases: [], implements: [] },
|
||||
})
|
||||
const js = await facts('class A extends mixin(B) {}\nexport default class extends C {}', 'javascript')
|
||||
expect(js[1]).toMatchObject({ name: 'A', bases: [{ value: 'mixin(B)' }], implements: [] })
|
||||
expect(js[2]).toMatchObject({ access: 'public', bases: [{ value: 'C' }] })
|
||||
})
|
||||
})
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { Access, BlockShape, CodeBlock, DeclaratorFacts, FunctionFacts, ImportFacts, ImportItem, LanguageAdapter, Node, ParameterFacts, TextKind, TokenRole } from '../contract'
|
||||
import type { Access, BaseFacts, BlockShape, ClassFacts, CodeBlock, DeclaratorFacts, FunctionFacts, ImportFacts, ImportItem, LanguageAdapter, Node, ParameterFacts, TextKind, TokenRole } from '../contract'
|
||||
import type { CommentRules } from './shared'
|
||||
import * as path from 'node:path'
|
||||
import { children, compoundShape, extendHeader, leafBlock, leafShape, listBlocks, named, sameNode, syntaxChildren } from './shared'
|
||||
@@ -98,7 +98,7 @@ function exportBlocks(node: Node, top: boolean): CodeBlock[] {
|
||||
return declarationBlocks(declaration, node, top)
|
||||
const value = node.childForFieldName('value')
|
||||
if (value && CLASSES.has(value.type))
|
||||
return [classBlock(value, node)]
|
||||
return [classBlock(node, value, value.childForFieldName('name'), 'public')]
|
||||
if (value && functionBody(value))
|
||||
return [functionBlock(node, value, value.childForFieldName('name'), 'public')]
|
||||
return [leafBlock('statement', node)]
|
||||
@@ -120,18 +120,20 @@ function declarationBlocks(declaration: Node, outer: Node, top: boolean): CodeBl
|
||||
return [leafBlock('type', outer)]
|
||||
default: {
|
||||
if (CLASSES.has(declaration.type))
|
||||
return [classBlock(declaration, outer)]
|
||||
return [classBlock(outer, declaration, declaration.childForFieldName('name'), declarationAccess(outer, top))]
|
||||
const body = declaration.childForFieldName('body')
|
||||
return [body?.type === 'statement_block' ? bodyBlock('statement', outer, body) : leafBlock('statement', outer)]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** 唯一声明值是带语句块主体的函数 → function;否则模块顶层的 const → const,其余 → variable。 */
|
||||
/** 唯一声明值是带语句块主体的函数 → function,是类表达式 → class;否则模块顶层的 const → const,其余 → variable。 */
|
||||
function variableBlock(declaration: Node, outer: Node, top: boolean): CodeBlock {
|
||||
const declarators = named(declaration).filter(child => child.type === 'variable_declarator')
|
||||
const access = declarationAccess(outer, top)
|
||||
const value = declarators.length === 1 ? declarators[0].childForFieldName('value') : null
|
||||
if (value && CLASSES.has(value.type))
|
||||
return classBlock(outer, value, declarators[0].childForFieldName('name'), access)
|
||||
if (value && functionBody(value))
|
||||
return functionBlock(outer, value, declarators[0].childForFieldName('name'), access)
|
||||
const isConst = declaration.type === 'lexical_declaration' && declaration.childForFieldName('kind')?.type === 'const'
|
||||
@@ -276,29 +278,68 @@ function caseBlock(node: Node): CodeBlock {
|
||||
return { kind: 'if', ...compoundShape(node, bodyStart, items, (list, afterRow) => statements(list, false, afterRow)) }
|
||||
}
|
||||
|
||||
/** 类:成员前面的装饰器是类主体里的兄弟节点,算进成员头部,也交给成员当装饰器。 */
|
||||
function classBlock(declaration: Node, outer: Node): CodeBlock {
|
||||
/**
|
||||
* 类块:declaration 是类声明或类表达式,name 是它在源码里的名字(类名;赋给变量、类字段的类表达式取变量名、字段名),
|
||||
* decorators 是类主体里排在它前面的装饰器(类字段的值是类表达式时)。
|
||||
*/
|
||||
function classBlock(outer: Node, declaration: Node, name: Node | null, access: Access | undefined, decorators: Node[] = []): CodeBlock {
|
||||
const body = declaration.childForFieldName('body')
|
||||
const facts = classFacts(outer, declaration, name, access, decorators)
|
||||
if (!body)
|
||||
return leafBlock('class', outer)
|
||||
return { kind: 'class', ...leafShape(outer), facts }
|
||||
return { kind: 'class', ...compoundShape(outer, body.startIndex, named(body), classMembers), facts }
|
||||
}
|
||||
|
||||
/** 类成员:成员前面的装饰器是类主体里的兄弟节点,算进成员头部,也交给成员当装饰器。 */
|
||||
function classMembers(list: Node[], afterRow: number): CodeBlock[] {
|
||||
let decorators: Node[] = []
|
||||
return listBlocks(list, afterRow, (member) => {
|
||||
if (member.type === 'decorator') {
|
||||
decorators.push(member)
|
||||
return []
|
||||
}
|
||||
const blocks = memberBlocks(member, decorators)
|
||||
const [from] = decorators
|
||||
decorators = []
|
||||
return from ? blocks.map((block, i) => i === 0 ? extendHeader(block, from) : block) : blocks
|
||||
}, COMMENTS)
|
||||
}
|
||||
|
||||
/**
|
||||
* 类的装饰器写在 export 前面时挂在 export 语句上,写在 class 前面时挂在类声明上;
|
||||
* 父类、实现的接口在 class_heritage 里:TypeScript 分 extends_clause / implements_clause,JavaScript 直接是 extends 后面的表达式。
|
||||
*/
|
||||
function classFacts(outer: Node, declaration: Node, name: Node | null, access: Access | undefined, decorators: Node[]): ClassFacts {
|
||||
const holders = sameNode(outer, declaration) ? [declaration] : [outer, declaration]
|
||||
const heritage = syntaxChildren(named(declaration).find(child => child.type === 'class_heritage'))
|
||||
return {
|
||||
kind: 'class',
|
||||
...compoundShape(outer, body.startIndex, named(body), (list, afterRow) => {
|
||||
let decorators: Node[] = []
|
||||
return listBlocks(list, afterRow, (member) => {
|
||||
if (member.type === 'decorator') {
|
||||
decorators.push(member)
|
||||
return []
|
||||
}
|
||||
const blocks = memberBlocks(member, decorators)
|
||||
const [from] = decorators
|
||||
decorators = []
|
||||
return from ? blocks.map((block, i) => i === 0 ? extendHeader(block, from) : block) : blocks
|
||||
}, COMMENTS)
|
||||
name: name ?? undefined,
|
||||
access,
|
||||
abstract: declaration.type === 'abstract_class_declaration',
|
||||
decorators: [...decorators, ...holders.flatMap(holder => named(holder).filter(child => child.type === 'decorator'))],
|
||||
typeParameters: syntaxChildren(declaration.childForFieldName('type_parameters')),
|
||||
bases: heritage.flatMap((part) => {
|
||||
if (part.type === 'extends_clause')
|
||||
return extendsBases(part)
|
||||
return part.type === 'implements_clause' ? [] : [{ value: part }]
|
||||
}),
|
||||
implements: heritage.filter(part => part.type === 'implements_clause').flatMap(syntaxChildren),
|
||||
}
|
||||
}
|
||||
|
||||
/** extends 后面的一串:每个表达式是一项,跟在它后面的泛型实参 `<T>` 归它。 */
|
||||
function extendsBases(clause: Node): BaseFacts[] {
|
||||
const bases: BaseFacts[] = []
|
||||
for (const part of syntaxChildren(clause)) {
|
||||
const last = bases.at(-1)
|
||||
if (part.type === 'type_arguments' && last)
|
||||
last.typeArguments = part
|
||||
else
|
||||
bases.push({ value: part })
|
||||
}
|
||||
return bases
|
||||
}
|
||||
|
||||
function memberBlocks(member: Node, decorators: Node[]): CodeBlock[] {
|
||||
switch (member.type) {
|
||||
case 'method_definition':
|
||||
@@ -309,6 +350,8 @@ function memberBlocks(member: Node, decorators: Node[]): CodeBlock[] {
|
||||
return [bodyBlock('statement', member, member.childForFieldName('body'))]
|
||||
default: {
|
||||
const value = member.childForFieldName('value')
|
||||
if (value && CLASSES.has(value.type))
|
||||
return [classBlock(member, value, member.childForFieldName('name'), memberAccess(member), decorators)]
|
||||
if (value && functionBody(value))
|
||||
return [functionBlock(member, value, member.childForFieldName('name'), memberAccess(member), decorators)]
|
||||
return [leafBlock('field', member)]
|
||||
@@ -385,7 +428,8 @@ function nameRole(leaf: Node): TokenRole {
|
||||
return 'function'
|
||||
if (parent.type === 'member_expression' && sameNode(parent.childForFieldName('property'), leaf) && isCallee(parent))
|
||||
return 'function'
|
||||
if (parent.type === 'new_expression')
|
||||
// 父类 `extends Base`:TypeScript 在 extends_clause 里,JavaScript 直接在 class_heritage 里
|
||||
if (parent.type === 'new_expression' || parent.type === 'extends_clause' || parent.type === 'class_heritage')
|
||||
return 'type'
|
||||
if (parent.type === 'variable_declarator' && sameNode(parent.childForFieldName('name'), leaf) && functionBody(parent.childForFieldName('value')))
|
||||
return 'function'
|
||||
|
||||
@@ -174,7 +174,7 @@ describe('typeScript 中文直译', () => {
|
||||
'k = [\'a\'] as const',
|
||||
'for (const key in obj) {}',
|
||||
'has = \'k\' in obj',
|
||||
'class A extends B implements C {}',
|
||||
'use(class A extends B implements C {})',
|
||||
'interface I extends J, K {}',
|
||||
'type F<T extends X> = (t: T) => void',
|
||||
'type R<T> = T extends string ? 1 : 2',
|
||||
@@ -191,7 +191,7 @@ describe('typeScript 中文直译', () => {
|
||||
expect(map[4]).toBe('k = [\'a\'] 断言为 常量')
|
||||
expect(map[5]).toBe('遍历 (常量 key 取自 obj)')
|
||||
expect(map[6]).toBe('has = \'k\' 在 obj')
|
||||
expect(map[7]).toBe('类 A 继承 B 实现 C')
|
||||
expect(map[7]).toBe('use(类 A 继承 B 实现 C {})')
|
||||
expect(map[8]).toBe('接口 I 继承 J, K')
|
||||
expect(map[9]).toBe('类型 F<T 限定为 X> = (t: T) => 无')
|
||||
expect(map[10]).toBe('类型 R<T> = T 符合 字符串 ? 1 : 2')
|
||||
@@ -244,7 +244,6 @@ describe('python 中文直译', () => {
|
||||
28: '最终',
|
||||
29: 'skipped.clear()',
|
||||
30: '返回 found',
|
||||
33: '类 EnvLockResult',
|
||||
34: 'total: 整数',
|
||||
35: 'ok: 列表[字符串] = 空列表',
|
||||
36: 'failed: 字典[字符串, 字符串] = 空字典',
|
||||
@@ -390,6 +389,7 @@ describe('属性表', () => {
|
||||
|
||||
const sample = rowTexts(await describeAll(py, 'python', pythonZhTranslator, readFixture('sample.py')))
|
||||
expect(sample[10]).toEqual(['名称: find_env_files', '说明: Return every .env file under root.', '访问级别: 公共', '异步: 否', '输入参数: 1 个', ' root | 字符串 | ', '输出参数: 列表[字符串]'])
|
||||
expect(sample[33]).toEqual(['名称: EnvLockResult', '访问级别: 公共'])
|
||||
const port = rowTexts(await describeAll(py, 'python', pythonZhTranslator, '_default_port = 5680 if _role == "beat" else 5679'))
|
||||
expect(port[1]).toEqual(['名称: _default_port', '访问级别: 私有', '初始值: 如果 _role 等于 "beat" 则 5680 否则 5679'])
|
||||
})
|
||||
@@ -555,10 +555,58 @@ describe('属性表', () => {
|
||||
1: ['名称: 匿名', '访问级别: 公共', '异步: 是', '输入参数: 无', '输出参数: 未标注'],
|
||||
2: ['名称: f', '访问级别: 私有', '异步: 否', '输入参数: 2 个', ' b? | 数值 | ', ' c | 未标注 | 默认 1', '输出参数: 无'],
|
||||
3: ['名称: inner', '异步: 否', '输入参数: 无', '输出参数: 未标注'],
|
||||
5: ['名称: K', '访问级别: 私有', '抽象: 否'],
|
||||
6: ['名称: load', '访问级别: 受保护', '异步: 否', '装饰器: @memo()', '输入参数: 无', '输出参数: x is Foo'],
|
||||
})
|
||||
})
|
||||
|
||||
it('类:说明、抽象(只有 TypeScript 有)、装饰器、类型参数、父类、实现的接口有才显示;父类的泛型实参、类型照常翻', async () => {
|
||||
const tsRows = rowTexts(await describeAll(ts, 'typescript', typescriptZhTranslator, readFixture('classes.ts.txt')))
|
||||
expect(tsRows[3]).toEqual([
|
||||
'名称: Repository',
|
||||
'说明: 仓库基类。\n\n子类实现 `load`。',
|
||||
'访问级别: 公共',
|
||||
'抽象: 是',
|
||||
'装饰器: @Injectable()',
|
||||
'类型参数: K, T 限定为 Entity = Entity',
|
||||
'父类: Base<字符串>',
|
||||
'实现的接口: Loader<T>, Disposable',
|
||||
])
|
||||
expect(tsRows[15]).toEqual(['名称: Query', '访问级别: 公共', '抽象: 否', '父类: QueryBase'])
|
||||
expect(tsRows[20]).toEqual(['名称: 匿名', '访问级别: 公共', '抽象: 否'])
|
||||
expect(tsRows[24]).toEqual(['名称: Mixed', '说明: 类表达式', '访问级别: 公共', '抽象: 否', '父类: mixin(A, B)'])
|
||||
expect(tsRows[28]).toEqual(['名称: Local', '抽象: 否'])
|
||||
expect(tsRows[32]).toEqual(['名称: Plain', '访问级别: 私有', '抽象: 否', '装饰器: @sealed'])
|
||||
|
||||
const pyRows = rowTexts(await describeAll(py, 'python', pythonZhTranslator, readFixture('classes.py')))
|
||||
expect(pyRows[6]).toEqual(['名称: AppException', '说明: 应用异常的基类。\n\n带上 HTTP 状态码。', '访问级别: 公共', '父类: Exception'])
|
||||
expect(pyRows[18]).toEqual(['名称: NotFound', '说明: 找不到资源。', '访问级别: 公共', '父类: AppException'])
|
||||
expect(pyRows[24]).toEqual(['名称: Meta', '访问级别: 公共', '装饰器: @dataclass(frozen=True)', '父类: Base, metaclass=ABCMeta'])
|
||||
expect(pyRows[29]).toEqual(['名称: _Hidden', '访问级别: 私有'])
|
||||
expect(pyRows[38]).toEqual(['名称: Local', '说明: 局部类。'])
|
||||
})
|
||||
|
||||
it('类的着色:名称、父类名按类型,说明是 Markdown,抽象按 storage', async () => {
|
||||
const tsRows = colors((await describeAll(ts, 'typescript', typescriptZhTranslator, readFixture('classes.ts.txt'))).get(3) ?? [])
|
||||
expect(tsRows[0].value).toEqual([{ text: 'Repository', role: 'type' }])
|
||||
expect(tsRows[1].value).toEqual({ markdown: '仓库基类。\n\n子类实现 `load`。' })
|
||||
expect(tsRows[3].value).toEqual([{ text: '是', role: 'storage' }])
|
||||
expect(valueOf(tsRows, '父类')).toEqual([
|
||||
{ text: 'Base', role: 'type' },
|
||||
{ text: '<', role: 'plain' },
|
||||
{ text: '字符串', role: 'type' },
|
||||
{ text: '>', role: 'plain' },
|
||||
])
|
||||
const pyRows = colors((await describeAll(py, 'python', pythonZhTranslator, readFixture('classes.py'))).get(24) ?? [])
|
||||
expect(valueOf(pyRows, '父类')).toEqual([
|
||||
{ text: 'Base', role: 'type' },
|
||||
{ text: ', ', role: 'plain' },
|
||||
{ text: 'metaclass', role: 'variable' },
|
||||
{ text: '=', role: 'plain' },
|
||||
{ text: 'ABCMeta', role: 'variable' },
|
||||
])
|
||||
})
|
||||
|
||||
it('变量:一条语句多个变量各占一组,没初始值写无,带语句块主体的回调只翻到主体开始之前', async () => {
|
||||
const source = [
|
||||
'let i = 0, n: number',
|
||||
@@ -675,12 +723,35 @@ describe('源码下标(跳到定义用)', () => {
|
||||
expect(marked(tsSource, tsRows.get(3))).toEqual(['名称: i', '名称: j'])
|
||||
})
|
||||
|
||||
it('类:名称、类型参数、父类、实现的接口里的名字带下标;只有「类型参数」一行标出它讲的定义(这几个泛型参数),名称是块自己的名字、不标', async () => {
|
||||
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 rows = await describeAll(ts, 'typescript', typescriptZhTranslator, source)
|
||||
const found = rows.get(1) ?? []
|
||||
expect(linked(valueOf(found, '名称'))).toEqual([['Repo', at('Repo')]])
|
||||
expect(linked(valueOf(found, '类型参数'))).toEqual([['K', at('K,')], ['T', at('T extends')], ['Entity', at('Entity')]])
|
||||
expect(linked(valueOf(found, '父类'))).toEqual([['Base', at('Base')], ['T', at('T> implements')]])
|
||||
expect(linked(valueOf(found, '实现的接口'))).toEqual([['Loader', at('Loader')], ['K', at('K>,')], ['Disposable', at('Disposable')]])
|
||||
expect(found.filter(row => row.defines).map(row => [row.label, source.slice(row.defines?.startIndex, row.defines?.endIndex)])).toEqual([['类型参数', 'K, T extends Entity']])
|
||||
|
||||
const pySource = 'class NotFound(errors.AppException, metaclass=ABCMeta):\n pass'
|
||||
const pyRows = await describeAll(py, 'python', pythonZhTranslator, pySource)
|
||||
expect(linked(valueOf(pyRows.get(1), '父类'))).toEqual([
|
||||
['errors', pySource.indexOf('errors')],
|
||||
['AppException', pySource.indexOf('AppException')],
|
||||
['metaclass', pySource.indexOf('metaclass')],
|
||||
['ABCMeta', pySource.indexOf('ABCMeta')],
|
||||
])
|
||||
})
|
||||
|
||||
it('样例全文:带下标的只有照抄的名字和字符串,下标处的源码就是这段;变量名、函数名都带,译词、说明词都不带', async () => {
|
||||
const samples: [LanguageAdapter, string, Translator, string][] = [
|
||||
[ts, 'typescript', typescriptZhTranslator, 'envLock.ts.txt'],
|
||||
[ts, 'typescript', typescriptZhTranslator, 'issueLock.ts.txt'],
|
||||
[ts, 'typescript', typescriptZhTranslator, 'classes.ts.txt'],
|
||||
[py, 'python', pythonZhTranslator, 'sample.py'],
|
||||
[py, 'python', pythonZhTranslator, 'routes.py'],
|
||||
[py, 'python', pythonZhTranslator, 'classes.py'],
|
||||
]
|
||||
for (const [adapter, languageId, translator, name] of samples) {
|
||||
const source = readFixture(name)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { Access, Defines, FactBlock, FunctionFacts, ImportFacts, ImportItem, Markdown, Node, PreviewText, PropertyRow, Segment, SourceSpan, TextBlock, TokenRole, Translator, VariableBlock } from '../contract'
|
||||
import type { Access, ClassFacts, Defines, FactBlock, FunctionFacts, ImportFacts, ImportItem, Markdown, Node, PreviewText, PropertyRow, Segment, SourceSpan, TextBlock, TokenRole, Translator, VariableBlock } from '../contract'
|
||||
import { commonIndent, splitLines } from '../editText'
|
||||
|
||||
/** 按所在语法节点区分译法的关键字,例如 Python 的 else:if 后「否则」,for / while 后「未中断时」。 */
|
||||
@@ -307,6 +307,8 @@ export function createZhTemplateTranslator(table: ZhTable): Translator {
|
||||
switch (block.kind) {
|
||||
case 'function':
|
||||
return functionRows(block.facts, phrases)
|
||||
case 'class':
|
||||
return classRows(block.facts, phrases)
|
||||
case 'import':
|
||||
return importRows(block.facts, phrases)
|
||||
default:
|
||||
@@ -361,9 +363,9 @@ interface Phrases {
|
||||
verbatim: (node: Node) => Segment[]
|
||||
/** 导入来源:字符串字面量按字符串着色,模块名按类型着色。 */
|
||||
module: (node: Node) => Segment[]
|
||||
/** 源码里的一个名字原样照抄、按 role 着色(函数名称)。 */
|
||||
/** 源码里的一个名字原样照抄、按 role 着色(函数、类的名称)。 */
|
||||
name: (node: Node, role: TokenRole) => Segment[]
|
||||
/** 函数的说明(JSDoc、docstring):去掉注释符号、引号和共同缩进的 Markdown 原文;没有文字时没有。 */
|
||||
/** 函数、类的说明(JSDoc、docstring):去掉注释符号、引号和共同缩进的 Markdown 原文;没有文字时没有。 */
|
||||
doc: (node: Node) => Markdown | undefined
|
||||
}
|
||||
|
||||
@@ -414,16 +416,14 @@ function aliased(item: Segment[], alias: Node | undefined, say: Phrases): Segmen
|
||||
return [...item, { text: ' ', role: 'plain' }, { text: '作为', role: 'keyword' }, { text: ' ', role: 'plain' }, ...say.translated(alias)]
|
||||
}
|
||||
|
||||
/** 函数:名称 / 访问级别 / 异步 / 装饰器 / 输入参数(每个参数一行:名称、类型、默认值)/ 输出参数。 */
|
||||
/** 函数:名称 / 说明 / 访问级别 / 异步 / 装饰器 / 输入参数(每个参数一行:名称、类型、默认值)/ 输出参数。 */
|
||||
function functionRows(facts: FunctionFacts, say: Phrases): PropertyRow[] {
|
||||
const decorators = facts.decorators.map(say.verbatim)
|
||||
const doc = facts.doc && say.doc(facts.doc)
|
||||
return [
|
||||
{ label: '名称', value: facts.name ? say.name(facts.name, 'function') : [hint('匿名')] },
|
||||
...(doc ? [{ label: '说明', value: doc }] : []),
|
||||
...docRows(facts.doc, say),
|
||||
...accessRows(facts.access),
|
||||
{ label: '异步', value: yesNo(facts.async) },
|
||||
...(decorators.length > 0 ? [{ label: '装饰器', value: joined(decorators, '\n') }] : []),
|
||||
...decoratorRows(facts.decorators, say),
|
||||
{
|
||||
label: '输入参数',
|
||||
value: count(facts.parameters.length),
|
||||
@@ -440,6 +440,36 @@ function functionRows(facts: FunctionFacts, say: Phrases): PropertyRow[] {
|
||||
]
|
||||
}
|
||||
|
||||
/**
|
||||
* 类:名称 / 说明 / 访问级别 / 抽象(只有分这个的语言才有)/ 装饰器 / 类型参数 / 父类 / 实现的接口,
|
||||
* 说明及以后各行有才显示;类型参数、父类、实现的接口各是一行,多项用逗号隔开。类型参数那一行讲的是这几个泛型参数的定义。
|
||||
*/
|
||||
function classRows(facts: ClassFacts, say: Phrases): PropertyRow[] {
|
||||
const listRow = (label: string, parts: Segment[][], defines: Defines = {}): PropertyRow[] =>
|
||||
parts.length > 0 ? [{ label, value: joined(parts, ', '), ...defines }] : []
|
||||
return [
|
||||
{ label: '名称', value: facts.name ? say.name(facts.name, 'type') : [hint('匿名')] },
|
||||
...docRows(facts.doc, say),
|
||||
...accessRows(facts.access),
|
||||
...(facts.abstract === undefined ? [] : [{ label: '抽象', value: yesNo(facts.abstract) }]),
|
||||
...decoratorRows(facts.decorators, say),
|
||||
...listRow('类型参数', facts.typeParameters.map(say.translated), defining(...facts.typeParameters)),
|
||||
...listRow('父类', facts.bases.map(base => base.typeArguments ? [...say.translated(base.value), ...say.translated(base.typeArguments)] : say.translated(base.value))),
|
||||
...listRow('实现的接口', facts.implements.map(say.translated)),
|
||||
]
|
||||
}
|
||||
|
||||
/** 函数、类的说明:有文字才有这一行。 */
|
||||
function docRows(doc: Node | undefined, say: Phrases): PropertyRow[] {
|
||||
const markdown = doc && say.doc(doc)
|
||||
return markdown ? [{ label: '说明', value: markdown }] : []
|
||||
}
|
||||
|
||||
/** 装饰器:源码原样照抄,多个换行。 */
|
||||
function decoratorRows(decorators: Node[], say: Phrases): PropertyRow[] {
|
||||
return decorators.length > 0 ? [{ label: '装饰器', value: joined(decorators.map(say.verbatim), '\n') }] : []
|
||||
}
|
||||
|
||||
/** 导入:来源 / 仅类型(只有分这个的语言才有)/ 导入项(每项一行,有别名写成「名称 作为 别名」)。 */
|
||||
function importRows(facts: ImportFacts, say: Phrases): PropertyRow[] {
|
||||
const itemText = (item: ImportItem): Segment[] => {
|
||||
|
||||
@@ -62,7 +62,7 @@ export function BlockLevel({ blocks, kind, tree }: { blocks: BlockView[], kind?:
|
||||
)
|
||||
}
|
||||
|
||||
/** 预览状态的内容:函数、导入、常量、变量块是属性表,其余是一行译文。 */
|
||||
/** 预览状态的内容:函数、类、导入、常量、变量块是属性表,其余是一行译文。 */
|
||||
function BlockPreview({ block, tree }: { block: BlockView, tree: BlockTreeState }): ReactElement {
|
||||
if ('properties' in block)
|
||||
return <PropertyGrid blockId={block.id} rows={block.properties} collapsed={tree.collapsed} onToggleFold={tree.onToggleFold} />
|
||||
|
||||
@@ -39,7 +39,7 @@ function PropertyItems({ rowId, items }: { rowId: string, items: PropertyItem[]
|
||||
)
|
||||
}
|
||||
|
||||
/** 函数、导入、常量、变量块的预览:属性名一列对齐,值按词性着色;输入参数、导入项下面展开子行。 */
|
||||
/** 函数、类、导入、常量、变量块的预览:属性名一列对齐,值按词性着色;输入参数、导入项下面展开子行。 */
|
||||
export function PropertyGrid({ blockId, rows, collapsed, onToggleFold }: {
|
||||
blockId: string
|
||||
rows: PropertyRow[]
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import MarkdownIt from 'markdown-it'
|
||||
|
||||
/**
|
||||
* 注释、函数说明的 Markdown 排版:CommonMark 加 GFM 的表格、删除线,带 http(s):// 的网址和邮箱自动成链接。
|
||||
* 注释、函数和类说明的 Markdown 排版:CommonMark 加 GFM 的表格、删除线,带 http(s):// 的网址和邮箱自动成链接。
|
||||
* 源码里的 HTML 当文字显示;链接只认 http / https / mailto,别的写法照原文显示——
|
||||
* webview 会拦下 <a> 的点击交给 VS Code 在外部打开。图片不渲染:webview 的 CSP 不放行外部图片。
|
||||
*/
|
||||
|
||||
@@ -245,6 +245,28 @@ describe('跳到定义:TS / Python 源码里闪哪一处', () => {
|
||||
expect(await at('line of')).toEqual({ plan: { id: '1.0', expand: ['1'] }, flashed: 'for (const line of lines) {' })
|
||||
})
|
||||
|
||||
it('类:类名是块自己的名字闪整块,泛型参数只闪「类型参数」一行;类默认折叠,跳到类里的方法、参数要先展开它', async () => {
|
||||
const source = [
|
||||
'export class Repo<T> extends Base {',
|
||||
' load(id: T) {',
|
||||
' return id',
|
||||
' }',
|
||||
'}',
|
||||
].join('\n')
|
||||
const at = (needle: string): ReturnType<typeof jump> => jump(ts, 'typescript', typescriptZhTranslator, source, needle)
|
||||
expect(await at('Repo')).toEqual({ plan: { id: '0', expand: [] }, flashed: 'export class Repo<T> extends Base {' })
|
||||
expect(await at('T>')).toEqual({ plan: { id: '0', row: '0/p3', expand: [] }, flashed: '类型参数: T' })
|
||||
expect(await at('load')).toEqual({ plan: { id: '0.0', expand: ['0'] }, flashed: 'load(id: T) {' })
|
||||
expect(await at('id: T')).toEqual({ plan: { id: '0.0', row: '0.0/p3/0', expand: ['0', '0.0/p3'] }, flashed: 'id | T | ' })
|
||||
const views = await withBlocks(ts, 'typescript', source, blocks => toBlockViews(blocks, source, ts, typescriptZhTranslator))
|
||||
expect([...collapsedIds(views, {})].sort()).toEqual(['0', '0.0'])
|
||||
|
||||
const pySource = ['class NotFound(AppException):', ' """找不到。"""', ' def run(self):', ' pass'].join('\n')
|
||||
const atPy = (needle: string): ReturnType<typeof jump> => jump(py, 'python', pythonZhTranslator, pySource, needle)
|
||||
expect(await atPy('NotFound')).toEqual({ plan: { id: '0', expand: [] }, flashed: 'class NotFound(AppException):' })
|
||||
expect(await atPy('run')).toEqual({ plan: { id: '0.0', expand: ['0'] }, flashed: 'def run(self):' })
|
||||
})
|
||||
|
||||
it('typeScript:翻译插件没标 defines 时闪整块', async () => {
|
||||
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 {' })
|
||||
|
||||
@@ -37,7 +37,7 @@ function shows(text: PreviewText, offset: number): boolean {
|
||||
|
||||
/**
|
||||
* 块里哪儿是 offset 处的定义,从具体到笼统:
|
||||
* ① 块自己的名字(函数名、只声明一个变量时的变量名)→ 整块;
|
||||
* ① 块自己的名字(函数名、类名、只声明一个变量时的变量名)→ 整块;
|
||||
* ② 属性表里讲这个定义的一行 / 子行(defines 盖住 offset:参数、导入项、多变量声明里的一个)→ 只闪它,子行连它所在的行一起展开;
|
||||
* ③ 块里只是显示着这个名字(一行译文里的循环变量、没填 defines 的翻译插件)→ 整块,名字在子行里时展开那一行。
|
||||
* 不在这块里是 undefined。
|
||||
|
||||
@@ -20,7 +20,7 @@ export interface Segment { text: string, role: TokenRole, offset?: number }
|
||||
/** 一段 Markdown 说明文字:注释、JSDoc、docstring 去掉注释符号、引号和共同缩进后的原文,按 Markdown 渲染成注释色的段落、列表、代码块 */
|
||||
export interface Markdown { markdown: string }
|
||||
|
||||
/** 预览里显示的一段文字:着色的词(一行译文、属性值),或整段 Markdown 说明(注释块、函数的说明) */
|
||||
/** 预览里显示的一段文字:着色的词(一行译文、属性值),或整段 Markdown 说明(注释块、函数和类的说明) */
|
||||
export type PreviewText = Segment[] | Markdown
|
||||
|
||||
/** 源码里一段的下标,end 为开区间 */
|
||||
@@ -53,12 +53,12 @@ interface BlockViewBase {
|
||||
code: string
|
||||
/** 源码着色(拼起来等于 code) */
|
||||
codeTokens: Segment[]
|
||||
/** 块自己的名字(函数名、只声明一个变量时的变量名)在源码里的范围:跳到的定义就是它时闪整块 */
|
||||
/** 块自己的名字(函数名、类名、只声明一个变量时的变量名)在源码里的范围:跳到的定义就是它时闪整块 */
|
||||
defines?: SourceRange
|
||||
children: BlockView[]
|
||||
}
|
||||
|
||||
/** 函数、导入、常量、变量块预览时是属性表,注释块是 Markdown 说明,其余是一行译文 */
|
||||
/** 函数、类、导入、常量、变量块预览时是属性表,注释块是 Markdown 说明,其余是一行译文 */
|
||||
export type BlockView = BlockViewBase & ({ translation: PreviewText } | { properties: PropertyRow[] })
|
||||
|
||||
export interface TranslatorOption { id: string, label: string }
|
||||
|
||||
@@ -76,12 +76,12 @@ describe('webview 状态', () => {
|
||||
})
|
||||
|
||||
describe('眼下折叠着哪些', () => {
|
||||
it('没点过:函数块(含方法、嵌套函数)折叠,其余块和属性行展开', () => {
|
||||
expect([...collapsedIds(BLOCKS, {})].sort()).toEqual(['1', '1.0.0', '2.0'])
|
||||
it('没点过:函数块(含方法、嵌套函数)、类块折叠,其余块和属性行展开', () => {
|
||||
expect([...collapsedIds(BLOCKS, {})].sort()).toEqual(['1', '1.0.0', '2', '2.0'])
|
||||
})
|
||||
|
||||
it('点过的按用户的:展开的函数保持展开,折叠的其他块、属性行保持折叠', () => {
|
||||
const folds = { '1': false, '2': true, '1.0.0': false, [propertyRowId('0', 2)]: true, [propertyRowId('2.0', 1)]: false }
|
||||
expect([...collapsedIds(BLOCKS, folds)].sort()).toEqual(['0/p2', '2', '2.0'])
|
||||
it('点过的按用户的:展开的函数、类保持展开,折叠的其他块、属性行保持折叠', () => {
|
||||
const folds = { '1': false, '2': false, '1.0': true, '1.0.0': false, [propertyRowId('0', 2)]: true, [propertyRowId('2.0', 1)]: false }
|
||||
expect([...collapsedIds(BLOCKS, folds)].sort()).toEqual(['0/p2', '1.0', '2.0'])
|
||||
})
|
||||
})
|
||||
@@ -54,9 +54,9 @@ function readViewState(saved: unknown): ViewState {
|
||||
return isScrollAnchor(state.scroll) ? { editing, folds, scroll: state.scroll } : { editing, folds }
|
||||
}
|
||||
|
||||
/** 块默认折叠吗:函数块收起主体(属性表照常显示),其余块展开。属性行默认都展开。 */
|
||||
/** 块默认折叠吗:函数、类块收起主体(属性表照常显示),其余块展开。属性行默认都展开。 */
|
||||
export function foldedByDefault(kind: BlockKind): boolean {
|
||||
return kind === 'function'
|
||||
return kind === 'function' || kind === 'class'
|
||||
}
|
||||
|
||||
/** 眼下折叠着的块和属性行:用户点过的按他点的,没点过的按 foldedByDefault。 */
|
||||
|
||||
Reference in new issue
Block a user