✨ feat(vscode): 代码阅读的注释和函数说明按 Markdown 渲染
This commit is contained in:
1 parent
aae9012f14
commit
a53c2a6aac
18 files changed
+394
-66
No files matched your search
@@ -21,6 +21,7 @@
|
|||||||
| 块内修改 | 改的就是文件本身:未保存标记、⌘S 保存、⌘Z 撤销都照常,切回「预览」按新代码重新翻译 |
|
| 块内修改 | 改的就是文件本身:未保存标记、⌘S 保存、⌘Z 撤销都照常,切回「预览」按新代码重新翻译 |
|
||||||
| 折叠 | 有子块的块、属性表里带子行的行能折叠;函数块(含方法、嵌套函数)默认收起主体、属性表照常显示,其余块和属性行默认展开;用户点过的按他点的记住 |
|
| 折叠 | 有子块的块、属性表里带子行的行能折叠;函数块(含方法、嵌套函数)默认收起主体、属性表照常显示,其余块和属性行默认展开;用户点过的按他点的记住 |
|
||||||
| 函数的说明 | TypeScript 紧贴在函数前面的 JSDoc、Python 函数主体开头的 docstring 是函数自己的说明:进属性表「说明」一行,不再单独成一块注释;它的源码行算在函数块的头部里,「编辑」时和签名一起改 |
|
| 函数的说明 | TypeScript 紧贴在函数前面的 JSDoc、Python 函数主体开头的 docstring 是函数自己的说明:进属性表「说明」一行,不再单独成一块注释;它的源码行算在函数块的头部里,「编辑」时和签名一起改 |
|
||||||
|
| 注释按 Markdown 显示 | 注释块和函数的「说明」按 Markdown 渲染,像 Kotlin 的 KDoc:去掉注释符号、引号和共同缩进后的原文原样交给 Markdown,不按语言转换(reStructuredText 的 `:func:`、JSDoc 的 `@param` 都照抄,注释以后慢慢改成 Markdown 写法);段内单个换行是软换行,空行分段 |
|
||||||
| 跳到定义 | 和普通编辑器一样 ⌘ / Ctrl + 点击名字跳到定义,定义由 VS Code 装的语言扩展(Pylance、TypeScript 语言服务……)查;目标语言有代码阅读就在预览里打开 |
|
| 跳到定义 | 和普通编辑器一样 ⌘ / Ctrl + 点击名字跳到定义,定义由 VS Code 装的语言扩展(Pylance、TypeScript 语言服务……)查;目标语言有代码阅读就在预览里打开 |
|
||||||
| 侧栏文件树 | 只看和打开:展开目录、打开文件、刷新、全部折叠;不做新建 / 重命名 / 删除 |
|
| 侧栏文件树 | 只看和打开:展开目录、打开文件、刷新、全部折叠;不做新建 / 重命名 / 删除 |
|
||||||
| 译文用词 | 结构照抄原语言,只翻关键字、内置类型、运算符;用专业术语(`export` → 公共,`catch` → 捕获,`===` → 全等) |
|
| 译文用词 | 结构照抄原语言,只翻关键字、内置类型、运算符;用专业术语(`export` → 公共,`catch` → 捕获,`===` → 全等) |
|
||||||
@@ -49,7 +50,7 @@
|
|||||||
- 块的组成(从左到右):折叠箭头(只有有子块的块才显示,其余占位对齐)、种类标签、内容、右侧切换按钮。
|
- 块的组成(从左到右):折叠箭头(只有有子块的块才显示,其余占位对齐)、种类标签、内容、右侧切换按钮。
|
||||||
- 种类标签和底色按种类区分(颜色见下表);注释块虚线框。
|
- 种类标签和底色按种类区分(颜色见下表);注释块虚线框。
|
||||||
- 内容两种状态,每块单独切:
|
- 内容两种状态,每块单独切:
|
||||||
- 预览:函数、导入、常量、变量块是属性表(见下节),其余块是一行译文;按词着色,等宽字体(中文部分用系统中文字体回退)。
|
- 预览:函数、导入、常量、变量块是属性表(见下节),注释块是 Markdown 说明(见「注释的 Markdown 排版」),其余块是一行译文;按词着色,等宽字体(中文部分用系统中文字体回退)。
|
||||||
- 编辑:这一块的源码(按语法树着色),可以直接改;蓝色边框。按钮文字「预览」;Esc 也回到预览。
|
- 编辑:这一块的源码(按语法树着色),可以直接改;蓝色边框。按钮文字「预览」;Esc 也回到预览。
|
||||||
- 复合语句(函数、条件、循环……)块里只放它自己的头部那几行(主体交给子块);收尾括号、空行不成块。函数的说明(JSDoc / docstring)也算在函数块的头部里。
|
- 复合语句(函数、条件、循环……)块里只放它自己的头部那几行(主体交给子块);收尾括号、空行不成块。函数的说明(JSDoc / docstring)也算在函数块的头部里。
|
||||||
- 折叠:函数块打开时默认收起子块(属性表照常显示),其余块默认展开;用户点过折叠箭头的块按他点的,记在视图状态里(见「预览数据与消息」)。
|
- 折叠:函数块打开时默认收起子块(属性表照常显示),其余块默认展开;用户点过折叠箭头的块按他点的,记在视图状态里(见「预览数据与消息」)。
|
||||||
@@ -62,7 +63,7 @@
|
|||||||
|
|
||||||
| 种类 | 行 |
|
| 种类 | 行 |
|
||||||
| --- | --- |
|
| --- | --- |
|
||||||
| 函数 | 名称(函数色;匿名函数写「匿名」)/ 说明(有说明才显示:JSDoc 去掉 `/**`、`*/` 和每行开头的 `*`,docstring 去掉引号;再去掉共同缩进和首尾空行,保留换行和 `@param` 这类标签行;按 comment 着色,多行照原样换行、长了折行;里面的名字不能点)/ 访问级别 / 异步(是 / 否)/ 装饰器(有才显示,源码原样照抄、按源码着色,多个换行)/ 输入参数(`N 个` 或「无」,每个参数一行:名称 \| 类型(没标注写「未标注」)\| 默认值(`默认 ` + 译文,没有留空))/ 输出参数(返回类型的译文,没标注写「未标注」) |
|
| 函数 | 名称(函数色;匿名函数写「匿名」)/ 说明(有说明才显示:JSDoc 去掉 `/**`、`*/` 和每行开头的 `*`,docstring 去掉引号;再去掉共同缩进和首尾空行,`@param` 这类标签行照抄;得到的原文按 Markdown 渲染,见「注释的 Markdown 排版」;里面的名字不能点)/ 访问级别 / 异步(是 / 否)/ 装饰器(有才显示,源码原样照抄、按源码着色,多个换行)/ 输入参数(`N 个` 或「无」,每个参数一行:名称 \| 类型(没标注写「未标注」)\| 默认值(`默认 ` + 译文,没有留空))/ 输出参数(返回类型的译文,没标注写「未标注」) |
|
||||||
| 导入 | 来源(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`)就重复几组 |
|
||||||
|
|
||||||
@@ -74,6 +75,15 @@
|
|||||||
|
|
||||||
值里的表达式只翻落在块头部里的那一段:`const v = useMemo(() => { … })` 这种值带语句块主体回调的块,回调主体是子块,值只翻到 `useMemo(() =>`,和块头部一致。
|
值里的表达式只翻落在块头部里的那一段:`const v = useMemo(() => { … })` 这种值带语句块主体回调的块,回调主体是子块,值只翻到 `useMemo(() =>`,和块头部一致。
|
||||||
|
|
||||||
|
### 注释的 Markdown 排版
|
||||||
|
|
||||||
|
- 用在:注释块(`#`、`//`、`/* */`、不挂在函数上的 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 不放行外部图片),`` 显示成 `!` 加链接。
|
||||||
|
- 样子:正文用界面字体(`--vscode-font-family`、`--vscode-font-size`)、comment 色、行高 20px,一行注释和一行译文一样高;行内代码用编辑器字体、`--vscode-textCodeBlock-background` 底;围栏代码块用编辑器字体和字号、同样的底,长行折行;段落、列表、代码块、表格之间 6px;标题只比正文稍大(h1 1.15em、h2 1.08em,其余同正文,加粗);链接 `--vscode-textLink-foreground`;引用左边 3px `--vscode-textBlockQuote-border` 竖线、`--vscode-textBlockQuote-background` 底;表格 1px 细线。
|
||||||
|
- 「编辑」状态照旧是源码,编辑写回不变;Markdown 里的名字不能点,不参与跳到定义。
|
||||||
|
|
||||||
### 跳到定义
|
### 跳到定义
|
||||||
|
|
||||||
- 能点的:预览里(属性表的名称、类型、来源、导入项、默认值、值,以及一行译文)照抄自源码的名字——标识符、属性名、类型名、点分模块名的每一节(`luffy_agent.core.db` 三节各自能点,和 VS Code 一样)、字符串(TypeScript 的模块路径 `'./x'` 就是字符串)。翻出来的关键字、运算符、说明词不能点。
|
- 能点的:预览里(属性表的名称、类型、来源、导入项、默认值、值,以及一行译文)照抄自源码的名字——标识符、属性名、类型名、点分模块名的每一节(`luffy_agent.core.db` 三节各自能点,和 VS Code 一样)、字符串(TypeScript 的模块路径 `'./x'` 就是字符串)。翻出来的关键字、运算符、说明词不能点。
|
||||||
@@ -163,7 +173,7 @@ export interface VariableBlock extends BlockShape { kind: 'const' | 'variable',
|
|||||||
export type FactBlock = FunctionBlock | ImportBlock | VariableBlock
|
export type FactBlock = FunctionBlock | ImportBlock | VariableBlock
|
||||||
|
|
||||||
export type TextKind = Exclude<BlockKind, FactBlock['kind']>
|
export type TextKind = Exclude<BlockKind, FactBlock['kind']>
|
||||||
/** 预览成一行译文的块。 */
|
/** 预览成一行译文的块(注释块是 Markdown 说明)。 */
|
||||||
export interface TextBlock extends BlockShape { kind: TextKind }
|
export interface TextBlock extends BlockShape { kind: TextKind }
|
||||||
|
|
||||||
export type CodeBlock = TextBlock | FactBlock
|
export type CodeBlock = TextBlock | FactBlock
|
||||||
@@ -183,6 +193,15 @@ export type TokenRole =
|
|||||||
*/
|
*/
|
||||||
export interface Segment { text: string, role: TokenRole, offset?: number }
|
export interface Segment { text: string, role: TokenRole, offset?: number }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 一段 Markdown 说明文字:注释、JSDoc、docstring 去掉注释符号、引号和共同缩进后的原文,不做别的转换。
|
||||||
|
* 预览按 Markdown 渲染成注释色的段落、列表、代码块(见「注释的 Markdown 排版」);第三方翻译插件给注释也填它。
|
||||||
|
*/
|
||||||
|
export interface Markdown { markdown: string }
|
||||||
|
|
||||||
|
/** 预览里显示的一段文字:着色的词(一行译文、属性值),或整段 Markdown 说明(注释块、函数的说明)。 */
|
||||||
|
export type PreviewText = Segment[] | Markdown
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 属性表里的一行或一个子行讲的是哪个定义:定义出来的那个名字的节点(取自 facts)在源码里的范围——
|
* 属性表里的一行或一个子行讲的是哪个定义:定义出来的那个名字的节点(取自 facts)在源码里的范围——
|
||||||
* 参数名(连 `*` 前缀、整个解构模式)、导入项(名称到别名)、声明的变量名。跳到的定义落在里面就只闪这一行;
|
* 参数名(连 `*` 前缀、整个解构模式)、导入项(名称到别名)、声明的变量名。跳到的定义落在里面就只闪这一行;
|
||||||
@@ -193,8 +212,8 @@ export interface Defines { defines?: SourceRange }
|
|||||||
/** 属性表里展开的一个子行:几列对齐的格子。 */
|
/** 属性表里展开的一个子行:几列对齐的格子。 */
|
||||||
export interface PropertyItem extends Defines { cells: Segment[][] }
|
export interface PropertyItem extends Defines { cells: Segment[][] }
|
||||||
|
|
||||||
/** 属性表的一行:属性名、着色的值、展开在下面的子行(输入参数、导入项)。 */
|
/** 属性表的一行:属性名、着色的值或 Markdown 说明(函数的「说明」)、展开在下面的子行(输入参数、导入项)。 */
|
||||||
export interface PropertyRow extends Defines { label: string, value: Segment[], items?: PropertyItem[] }
|
export interface PropertyRow extends Defines { label: string, value: PreviewText, items?: PropertyItem[] }
|
||||||
|
|
||||||
/** 语言插件:语法树 → 代码块树。 */
|
/** 语言插件:语法树 → 代码块树。 */
|
||||||
export interface LanguageAdapter {
|
export interface LanguageAdapter {
|
||||||
@@ -213,7 +232,7 @@ export interface Translator {
|
|||||||
id: string // 'typescript.zh-template'
|
id: string // 'typescript.zh-template'
|
||||||
label: string // '中文直译'
|
label: string // '中文直译'
|
||||||
languageIds: string[]
|
languageIds: string[]
|
||||||
translate: (block: TextBlock, source: string) => Segment[] // 只翻 header 那一段
|
translate: (block: TextBlock, source: string) => PreviewText // 只翻 header 那一段;注释块给 Markdown 原文
|
||||||
describe: (block: FactBlock, source: string) => PropertyRow[] // 函数、导入、常量、变量块 → 属性表
|
describe: (block: FactBlock, source: string) => PropertyRow[] // 函数、导入、常量、变量块 → 属性表
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -288,7 +307,7 @@ export interface CodeReaderApi {
|
|||||||
|
|
||||||
同一套表和节点模板也用来翻属性表里的值:引擎能翻任意一个节点(只取它落在块头部里的那一段),参数类型、默认值、返回类型、变量类型和初始值都走这条路。
|
同一套表和节点模板也用来翻属性表里的值:引擎能翻任意一个节点(只取它落在块头部里的那一段),参数类型、默认值、返回类型、变量类型和初始值都走这条路。
|
||||||
|
|
||||||
其他规则:空数组字面量 → 空数组(Python 空列表 → 空列表、空字典 → 空字典);复合语句头部去掉结尾的 `{` 或 `:`;多行合成一行(连续空白压成一个空格,右括号前的尾逗号去掉);注释去掉注释符号,原文照搬。关键字与前后内容之间保留源码里的空格(`!` 翻成「非」后补一个空格)。
|
其他规则:空数组字面量 → 空数组(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 语法为准。下面两张表里写成「(语境)译法 / (语境)译法」的关键字就是按这条规则区分的,排第一个的是默认译法。
|
||||||
|
|
||||||
@@ -326,7 +345,7 @@ Python 内置类型(仅注解里):str 字符串、int 整数、float 浮
|
|||||||
|
|
||||||
| 源码 | 属性表 |
|
| 源码 | 属性表 |
|
||||||
| --- | --- |
|
| --- | --- |
|
||||||
| `export async function findEnvFiles(workspaceRoot: string): Promise<string[]> {`(envLock.ts 18–23 行,连同前面的 JSDoc) | 名称 findEnvFiles / 说明 `Scan the workspace folder rooted at …`(三行,照原样换行)/ 访问级别 公共 / 异步 是 / 输入参数 1 个:`workspaceRoot \| 字符串 \|` / 输出参数 `Promise<字符串[]>` |
|
| `export async function findEnvFiles(workspaceRoot: string): Promise<string[]> {`(envLock.ts 18–23 行,连同前面的 JSDoc) | 名称 findEnvFiles / 说明 `Scan the workspace folder rooted at …`(三行原文照留,按 Markdown 排成一段)/ 访问级别 公共 / 异步 是 / 输入参数 1 个:`workspaceRoot \| 字符串 \|` / 输出参数 `Promise<字符串[]>` |
|
||||||
| `async function chmodAll(workspaceRoot: string, mode: number): Promise<EnvLockResult> {` | 名称 chmodAll / 访问级别 私有 / 异步 是 / 输入参数 2 个:`workspaceRoot \| 字符串`、`mode \| 数值` / 输出参数 `Promise<EnvLockResult>` |
|
| `async function chmodAll(workspaceRoot: string, mode: number): Promise<EnvLockResult> {` | 名称 chmodAll / 访问级别 私有 / 异步 是 / 输入参数 2 个:`workspaceRoot \| 字符串`、`mode \| 数值` / 输出参数 `Promise<EnvLockResult>` |
|
||||||
| `import { promises as fsp } from 'node:fs'` | 来源 `'node:fs'` / 仅类型 否 / 导入项 1 个:`promises 作为 fsp` |
|
| `import { promises as fsp } from 'node:fs'` | 来源 `'node:fs'` / 仅类型 否 / 导入项 1 个:`promises 作为 fsp` |
|
||||||
| `const ENV_PATTERN = '**/.env*'` | 名称 ENV_PATTERN / 访问级别 私有 / 值 `'**/.env*'` |
|
| `const ENV_PATTERN = '**/.env*'` | 名称 ENV_PATTERN / 访问级别 私有 / 值 `'**/.env*'` |
|
||||||
@@ -352,8 +371,8 @@ interface BlockViewBase {
|
|||||||
children: BlockView[]
|
children: BlockView[]
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 属性表块(有 facts)只带 properties,其余块只带 translation;没有翻译插件时是空数组。 */
|
/** 属性表块(有 facts)只带 properties,其余块只带 translation(注释块是 `{ markdown }`);没有翻译插件时是空数组。 */
|
||||||
type BlockView = BlockViewBase & ({ translation: Segment[] } | { properties: PropertyRow[] })
|
type BlockView = BlockViewBase & ({ translation: PreviewText } | { properties: PropertyRow[] })
|
||||||
```
|
```
|
||||||
|
|
||||||
- 扩展 → webview:
|
- 扩展 → webview:
|
||||||
@@ -408,7 +427,7 @@ vscode/src/codeReader/
|
|||||||
editor/definitions.ts 查定义(vscode.executeDefinitionProvider)、几处定义的挑选框
|
editor/definitions.ts 查定义(vscode.executeDefinitionProvider)、几处定义的挑选框
|
||||||
editor/definitionAction.ts 查到的定义 → 怎么跳(纯函数)
|
editor/definitionAction.ts 查到的定义 → 怎么跳(纯函数)
|
||||||
index.ts registerCodeReader(context): CodeReaderApi
|
index.ts registerCodeReader(context): CodeReaderApi
|
||||||
vscode/webview-ui/code-reader.html + src/codeReader/ 预览界面(React,Vite 第二个入口;属性表在 PropertyGrid.tsx)
|
vscode/webview-ui/code-reader.html + src/codeReader/ 预览界面(React,Vite 第二个入口;属性表在 PropertyGrid.tsx,Markdown 渲染在 markdown.ts / TextPreview.tsx)
|
||||||
```
|
```
|
||||||
|
|
||||||
## 测试
|
## 测试
|
||||||
@@ -419,6 +438,7 @@ vscode/webview-ui/code-reader.html + src/codeReader/ 预览界面(React,Vit
|
|||||||
- 属性表信息:`envLock.ts`、`sample.py`、`routes.py`(原型里的 FastAPI 路由)加内联片段,覆盖各种导入写法、形参写法、类成员访问修饰符、解构和多变量声明、Python 下划线规则。
|
- 属性表信息:`envLock.ts`、`sample.py`、`routes.py`(原型里的 FastAPI 路由)加内联片段,覆盖各种导入写法、形参写法、类成员访问修饰符、解构和多变量声明、Python 下划线规则。
|
||||||
- 翻译:上面两张样例表逐字一致;属性名不被翻;每个被翻的词单独成段;节点模板嵌套时正确、槽位里照样翻、没命中模板的节点照抄;属性表里的值和一行译文用同一套表和模板。
|
- 翻译:上面两张样例表逐字一致;属性名不被翻;每个被翻的词单独成段;节点模板嵌套时正确、槽位里照样翻、没命中模板的节点照抄;属性表里的值和一行译文用同一套表和模板。
|
||||||
- `blockView`:`codeTokens` 拼起来等于 `code`;去缩进正确;函数、导入、常量、变量块只带属性表,其余块只带译文。
|
- `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` 选中。
|
- 注册表:注册 / 注销后 `languageIds` 与可选翻译正确;同一语言多个翻译时按 `globalState` 选中。
|
||||||
|
|||||||
Generated
+44
@@ -89,6 +89,9 @@ importers:
|
|||||||
lucide-react:
|
lucide-react:
|
||||||
specifier: ^1.16.0
|
specifier: ^1.16.0
|
||||||
version: 1.16.0(react@19.2.6)
|
version: 1.16.0(react@19.2.6)
|
||||||
|
markdown-it:
|
||||||
|
specifier: ^15.0.2
|
||||||
|
version: 15.0.2
|
||||||
react:
|
react:
|
||||||
specifier: ^19.0.0
|
specifier: ^19.0.0
|
||||||
version: 19.2.6
|
version: 19.2.6
|
||||||
@@ -1515,6 +1518,9 @@ packages:
|
|||||||
argparse@2.0.1:
|
argparse@2.0.1:
|
||||||
resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==}
|
resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==}
|
||||||
|
|
||||||
|
argparse@3.0.2:
|
||||||
|
resolution: {integrity: sha512-mFdDM6WqWKraGLsVb+C9CahPnzTXOefAOLq3jYcca2YZ8bEWpr++Tzj+zSaKW9+X9L5uSxcm1AZ3Y6aZJ09OhQ==}
|
||||||
|
|
||||||
args-tokenizer@0.3.0:
|
args-tokenizer@0.3.0:
|
||||||
resolution: {integrity: sha512-xXAd7G2Mll5W8uo37GETpQ2VrE84M181Z7ugHFGQnJZ50M2mbOv0osSZ9VsSgPfJQ+LVG0prSi0th+ELMsno7Q==}
|
resolution: {integrity: sha512-xXAd7G2Mll5W8uo37GETpQ2VrE84M181Z7ugHFGQnJZ50M2mbOv0osSZ9VsSgPfJQ+LVG0prSi0th+ELMsno7Q==}
|
||||||
|
|
||||||
@@ -1871,6 +1877,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==}
|
resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==}
|
||||||
engines: {node: '>=0.12'}
|
engines: {node: '>=0.12'}
|
||||||
|
|
||||||
|
entities@8.1.0:
|
||||||
|
resolution: {integrity: sha512-kxL7msIffSuh9aaFAMD7rxAIuTRMAHMeBtgHW2yUdWw732ZNh4MehkF2gdjvtdmikkaIP9bFDDJOPlsvm7avrA==}
|
||||||
|
engines: {node: '>=20.19.0'}
|
||||||
|
|
||||||
environment@1.1.0:
|
environment@1.1.0:
|
||||||
resolution: {integrity: sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==}
|
resolution: {integrity: sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==}
|
||||||
engines: {node: '>=18'}
|
engines: {node: '>=18'}
|
||||||
@@ -2576,6 +2586,9 @@ packages:
|
|||||||
linkify-it@5.0.0:
|
linkify-it@5.0.0:
|
||||||
resolution: {integrity: sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==}
|
resolution: {integrity: sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==}
|
||||||
|
|
||||||
|
linkify-it@6.1.0:
|
||||||
|
resolution: {integrity: sha512-wJ/TwpSDTLepCrQoYWYIExIKg5Zchex2Nn5yk2mFnB+6PtdkHtyLx742md9csRjjOnGkKIS/RrbY7l8D6gT9Vw==}
|
||||||
|
|
||||||
local-pkg@1.1.2:
|
local-pkg@1.1.2:
|
||||||
resolution: {integrity: sha512-arhlxbFRmoQHl33a0Zkle/YWlmNwoyt6QNZEIJcqNbdrsix5Lvc4HyyI3EnwxTYlZYc32EbYrQ8SzEZ7dqgg9A==}
|
resolution: {integrity: sha512-arhlxbFRmoQHl33a0Zkle/YWlmNwoyt6QNZEIJcqNbdrsix5Lvc4HyyI3EnwxTYlZYc32EbYrQ8SzEZ7dqgg9A==}
|
||||||
engines: {node: '>=14'}
|
engines: {node: '>=14'}
|
||||||
@@ -2646,6 +2659,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-a54IwgWPaeBCAAsv13YgmALOF1elABB08FxO9i+r4VFk5Vl4pKokRPeX8u5TCgSsPi6ec1otfLjdOpVcgbpshg==}
|
resolution: {integrity: sha512-a54IwgWPaeBCAAsv13YgmALOF1elABB08FxO9i+r4VFk5Vl4pKokRPeX8u5TCgSsPi6ec1otfLjdOpVcgbpshg==}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
|
markdown-it@15.0.2:
|
||||||
|
resolution: {integrity: sha512-q4IGxMv56jCqT4OCRCADBoDP3LO4MhmTXjFbphHPXs4g3j9Xg5RDnxqN8IF/3vIWEU+VCnUq+7JUg/cfy2E6Qw==}
|
||||||
|
hasBin: true
|
||||||
|
|
||||||
markdown-table@3.0.4:
|
markdown-table@3.0.4:
|
||||||
resolution: {integrity: sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw==}
|
resolution: {integrity: sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw==}
|
||||||
|
|
||||||
@@ -2688,6 +2705,9 @@ packages:
|
|||||||
mdurl@2.0.0:
|
mdurl@2.0.0:
|
||||||
resolution: {integrity: sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==}
|
resolution: {integrity: sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==}
|
||||||
|
|
||||||
|
mdurl@2.1.0:
|
||||||
|
resolution: {integrity: sha512-1+HBaOx0zi/dQWht8rNv9MYf9qqpqL/kxI0hXImU6Y547zM6Sni8BQibt7ifgMcYtQg41ao3Ivd6cnSM86inpg==}
|
||||||
|
|
||||||
merge2@1.4.1:
|
merge2@1.4.1:
|
||||||
resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==}
|
resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==}
|
||||||
engines: {node: '>= 8'}
|
engines: {node: '>= 8'}
|
||||||
@@ -3416,6 +3436,9 @@ packages:
|
|||||||
uc.micro@2.1.0:
|
uc.micro@2.1.0:
|
||||||
resolution: {integrity: sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==}
|
resolution: {integrity: sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==}
|
||||||
|
|
||||||
|
uc.micro@3.0.0:
|
||||||
|
resolution: {integrity: sha512-U3PppEkleoTnIfi8BozMx3yju3qc/L6SwqWo2Sw+54PX+PX0q9I+r1Um5HCmqD7n9VDX5/v3vQH/AjA6deDdtw==}
|
||||||
|
|
||||||
ufo@1.6.1:
|
ufo@1.6.1:
|
||||||
resolution: {integrity: sha512-9a4/uxlTWJ4+a5i0ooc1rU7C7YOw3wT+UGqdeNNHWnOF9qcMBgLRS+4IYUqbczewFx4mLEig6gawh7X6mFlEkA==}
|
resolution: {integrity: sha512-9a4/uxlTWJ4+a5i0ooc1rU7C7YOw3wT+UGqdeNNHWnOF9qcMBgLRS+4IYUqbczewFx4mLEig6gawh7X6mFlEkA==}
|
||||||
|
|
||||||
@@ -5012,6 +5035,8 @@ snapshots:
|
|||||||
|
|
||||||
argparse@2.0.1: {}
|
argparse@2.0.1: {}
|
||||||
|
|
||||||
|
argparse@3.0.2: {}
|
||||||
|
|
||||||
args-tokenizer@0.3.0: {}
|
args-tokenizer@0.3.0: {}
|
||||||
|
|
||||||
assertion-error@2.0.1: {}
|
assertion-error@2.0.1: {}
|
||||||
@@ -5362,6 +5387,8 @@ snapshots:
|
|||||||
|
|
||||||
entities@4.5.0: {}
|
entities@4.5.0: {}
|
||||||
|
|
||||||
|
entities@8.1.0: {}
|
||||||
|
|
||||||
environment@1.1.0: {}
|
environment@1.1.0: {}
|
||||||
|
|
||||||
es-module-lexer@1.7.0: {}
|
es-module-lexer@1.7.0: {}
|
||||||
@@ -6149,6 +6176,10 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
uc.micro: 2.1.0
|
uc.micro: 2.1.0
|
||||||
|
|
||||||
|
linkify-it@6.1.0:
|
||||||
|
dependencies:
|
||||||
|
uc.micro: 3.0.0
|
||||||
|
|
||||||
local-pkg@1.1.2:
|
local-pkg@1.1.2:
|
||||||
dependencies:
|
dependencies:
|
||||||
mlly: 1.7.4
|
mlly: 1.7.4
|
||||||
@@ -6216,6 +6247,15 @@ snapshots:
|
|||||||
punycode.js: 2.3.1
|
punycode.js: 2.3.1
|
||||||
uc.micro: 2.1.0
|
uc.micro: 2.1.0
|
||||||
|
|
||||||
|
markdown-it@15.0.2:
|
||||||
|
dependencies:
|
||||||
|
argparse: 3.0.2
|
||||||
|
entities: 8.1.0
|
||||||
|
linkify-it: 6.1.0
|
||||||
|
mdurl: 2.1.0
|
||||||
|
punycode.js: 2.3.1
|
||||||
|
uc.micro: 3.0.0
|
||||||
|
|
||||||
markdown-table@3.0.4: {}
|
markdown-table@3.0.4: {}
|
||||||
|
|
||||||
mdast-util-find-and-replace@3.0.2:
|
mdast-util-find-and-replace@3.0.2:
|
||||||
@@ -6333,6 +6373,8 @@ snapshots:
|
|||||||
|
|
||||||
mdurl@2.0.0: {}
|
mdurl@2.0.0: {}
|
||||||
|
|
||||||
|
mdurl@2.1.0: {}
|
||||||
|
|
||||||
merge2@1.4.1: {}
|
merge2@1.4.1: {}
|
||||||
|
|
||||||
micromark-core-commonmark@2.0.2:
|
micromark-core-commonmark@2.0.2:
|
||||||
@@ -7202,6 +7244,8 @@ snapshots:
|
|||||||
|
|
||||||
uc.micro@2.1.0: {}
|
uc.micro@2.1.0: {}
|
||||||
|
|
||||||
|
uc.micro@3.0.0: {}
|
||||||
|
|
||||||
ufo@1.6.1: {}
|
ufo@1.6.1: {}
|
||||||
|
|
||||||
uglify-js@3.19.3:
|
uglify-js@3.19.3:
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { describe, expect, it } from 'vitest'
|
|||||||
import { readBlockViews, toBlockViews } from './blockView'
|
import { readBlockViews, toBlockViews } from './blockView'
|
||||||
import { createPythonAdapter } from './languages/python'
|
import { createPythonAdapter } from './languages/python'
|
||||||
import { createTypeScriptAdapter } from './languages/typescript'
|
import { createTypeScriptAdapter } from './languages/typescript'
|
||||||
import { propertyLines, readFixture, runtime, segmentText, WASM_DIR, withBlocks } from './testSupport'
|
import { previewText, propertyLines, readFixture, runtime, WASM_DIR, withBlocks } from './testSupport'
|
||||||
import { pythonZhTranslator } from './translators/pythonZh'
|
import { pythonZhTranslator } from './translators/pythonZh'
|
||||||
import { typescriptZhTranslator } from './translators/typescriptZh'
|
import { typescriptZhTranslator } from './translators/typescriptZh'
|
||||||
|
|
||||||
@@ -38,7 +38,7 @@ const PROPERTY_KINDS = new Set(['function', 'import', 'const', 'variable'])
|
|||||||
function translationText(view: BlockView): string {
|
function translationText(view: BlockView): string {
|
||||||
if (!('translation' in view))
|
if (!('translation' in view))
|
||||||
throw new Error(`块 ${view.id} 是属性表`)
|
throw new Error(`块 ${view.id} 是属性表`)
|
||||||
return segmentText(view.translation)
|
return previewText(view.translation)
|
||||||
}
|
}
|
||||||
|
|
||||||
describe('blockView', () => {
|
describe('blockView', () => {
|
||||||
@@ -53,7 +53,7 @@ describe('blockView', () => {
|
|||||||
if ('properties' in view)
|
if ('properties' in view)
|
||||||
expect(view.properties.length, view.id).toBeGreaterThan(0)
|
expect(view.properties.length, view.id).toBeGreaterThan(0)
|
||||||
else
|
else
|
||||||
expect(view.translation.length, view.id).toBeGreaterThan(0)
|
expect(previewText(view.translation), view.id).not.toBe('')
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -100,6 +100,13 @@ describe('blockView', () => {
|
|||||||
expect(getUser?.children[0]).toMatchObject({ kind: 'comment', startLine: 38, endLine: 38 })
|
expect(getUser?.children[0]).toMatchObject({ kind: 'comment', startLine: 38, endLine: 38 })
|
||||||
})
|
})
|
||||||
|
|
||||||
|
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/) } })
|
||||||
|
const findEnvFiles = byId(env, '5')
|
||||||
|
expect('properties' in findEnvFiles && findEnvFiles.properties[1]).toEqual({ label: '说明', value: { markdown: expect.stringMatching(/^Scan the workspace folder/) } })
|
||||||
|
})
|
||||||
|
|
||||||
it('id 是块在树里的位置,行号 0 起,多行块去掉共同缩进', async () => {
|
it('id 是块在树里的位置,行号 0 起,多行块去掉共同缩进', async () => {
|
||||||
const env = await views(ts, 'typescript', typescriptZhTranslator, readFixture('envLock.ts.txt'))
|
const env = await views(ts, 'typescript', typescriptZhTranslator, readFixture('envLock.ts.txt'))
|
||||||
expect(byId(env, '5.1.0')).toMatchObject({ kind: 'return', startLine: 25, endLine: 25, code: 'return []' })
|
expect(byId(env, '5.1.0')).toMatchObject({ kind: 'return', startLine: 25, endLine: 25, code: 'return []' })
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import type { BlockKind, CodeBlock, LanguageAdapter, Node, PropertyRow, Segment, SourceRange, TokenRole, Translator } from './contract'
|
import type { BlockKind, CodeBlock, LanguageAdapter, Node, PreviewText, PropertyRow, Segment, SourceRange, TokenRole, Translator } from './contract'
|
||||||
import type { TreeSitterRuntime } from './runtime'
|
import type { TreeSitterRuntime } from './runtime'
|
||||||
import { commonIndent, splitLines } from './editText'
|
import { commonIndent, splitLines } from './editText'
|
||||||
|
|
||||||
@@ -18,8 +18,8 @@ interface BlockViewBase {
|
|||||||
children: BlockView[]
|
children: BlockView[]
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 发给 webview 的一块(可序列化):函数、导入、常量、变量块预览时是属性表,其余是一行译文。 */
|
/** 发给 webview 的一块(可序列化):函数、导入、常量、变量块预览时是属性表,注释块是 Markdown 说明,其余是一行译文。 */
|
||||||
export type BlockView = BlockViewBase & ({ translation: Segment[] } | { properties: PropertyRow[] })
|
export type BlockView = BlockViewBase & ({ translation: PreviewText } | { properties: PropertyRow[] })
|
||||||
|
|
||||||
interface Line { start: number, end: number }
|
interface Line { start: number, end: number }
|
||||||
|
|
||||||
|
|||||||
@@ -110,6 +110,15 @@ export type TokenRole
|
|||||||
*/
|
*/
|
||||||
export interface Segment { text: string, role: TokenRole, offset?: number }
|
export interface Segment { text: string, role: TokenRole, offset?: number }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 一段 Markdown 说明文字:注释、JSDoc、docstring 去掉注释符号、引号和共同缩进后的原文,不做别的转换
|
||||||
|
* (reStructuredText 的 `:func:`、JSDoc 的 `@param` 都照抄)。预览按 Markdown 渲染成注释色的段落、列表、代码块,源码里的 HTML 当文字显示。
|
||||||
|
*/
|
||||||
|
export interface Markdown { markdown: string }
|
||||||
|
|
||||||
|
/** 预览里显示的一段文字:着色的词(一行译文、属性值),或整段 Markdown 说明(注释块、函数的说明)。 */
|
||||||
|
export type PreviewText = Segment[] | Markdown
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 属性表里的一行或一个子行讲的是哪个定义:定义出来的那个名字的节点(取自 facts,例如参数名、导入项的名称和别名、
|
* 属性表里的一行或一个子行讲的是哪个定义:定义出来的那个名字的节点(取自 facts,例如参数名、导入项的名称和别名、
|
||||||
* 声明的变量名)在源码里的范围。跳到定义时,定义处落在这个范围里就只闪这一行;不填(第三方翻译插件可以不填)就闪整块。
|
* 声明的变量名)在源码里的范围。跳到定义时,定义处落在这个范围里就只闪这一行;不填(第三方翻译插件可以不填)就闪整块。
|
||||||
@@ -119,10 +128,10 @@ export interface Defines { defines?: SourceRange }
|
|||||||
/** 属性表里展开的一个子行:几列对齐的格子,例如参数的名称 / 类型 / 默认值。 */
|
/** 属性表里展开的一个子行:几列对齐的格子,例如参数的名称 / 类型 / 默认值。 */
|
||||||
export interface PropertyItem extends Defines { cells: Segment[][] }
|
export interface PropertyItem extends Defines { cells: Segment[][] }
|
||||||
|
|
||||||
/** 属性表的一行:左列属性名,右列着色的值;items 是展开在这一行下面的子行(输入参数、导入项)。 */
|
/** 属性表的一行:左列属性名,右列着色的值或 Markdown 说明;items 是展开在这一行下面的子行(输入参数、导入项)。 */
|
||||||
export interface PropertyRow extends Defines {
|
export interface PropertyRow extends Defines {
|
||||||
label: string
|
label: string
|
||||||
value: Segment[]
|
value: PreviewText
|
||||||
items?: PropertyItem[]
|
items?: PropertyItem[]
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -143,8 +152,8 @@ export interface Translator {
|
|||||||
id: string // 'typescript.zh-template'
|
id: string // 'typescript.zh-template'
|
||||||
label: string // '中文直译'
|
label: string // '中文直译'
|
||||||
languageIds: string[]
|
languageIds: string[]
|
||||||
/** 只翻 header 那一段。 */
|
/** 只翻 header 那一段;注释块给去掉注释符号的 Markdown 原文。 */
|
||||||
translate: (block: TextBlock, source: string) => Segment[]
|
translate: (block: TextBlock, source: string) => PreviewText
|
||||||
/** 函数、导入、常量、变量块 → 属性表。 */
|
/** 函数、导入、常量、变量块 → 属性表。 */
|
||||||
describe: (block: FactBlock, source: string) => PropertyRow[]
|
describe: (block: FactBlock, source: string) => PropertyRow[]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import type { BlockKind, CodeBlock, LanguageAdapter, PropertyRow, Segment } from './contract'
|
import type { BlockKind, CodeBlock, LanguageAdapter, PreviewText, 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'
|
||||||
@@ -49,10 +49,15 @@ export function segmentText(segments: Segment[]): string {
|
|||||||
return segments.map(segment => segment.text).join('')
|
return segments.map(segment => segment.text).join('')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 预览文字写成文字:着色的词拼起来,Markdown 取原文。 */
|
||||||
|
export function previewText(text: PreviewText): string {
|
||||||
|
return 'markdown' in text ? text.markdown : segmentText(text)
|
||||||
|
}
|
||||||
|
|
||||||
/** 属性表写成文字:每行 `名称: 值`,展开的子行缩进两格、各列用 ` | ` 隔开。 */
|
/** 属性表写成文字:每行 `名称: 值`,展开的子行缩进两格、各列用 ` | ` 隔开。 */
|
||||||
export function propertyLines(rows: PropertyRow[]): string[] {
|
export function propertyLines(rows: PropertyRow[]): string[] {
|
||||||
return rows.flatMap(row => [
|
return rows.flatMap(row => [
|
||||||
`${row.label}: ${segmentText(row.value)}`,
|
`${row.label}: ${previewText(row.value)}`,
|
||||||
...(row.items ?? []).map(item => ` ${item.cells.map(segmentText).join(' | ')}`),
|
...(row.items ?? []).map(item => ` ${item.cells.map(segmentText).join(' | ')}`),
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,16 +11,21 @@ import { createZhTemplateTranslator } from './zhTemplate'
|
|||||||
const ts = createTypeScriptAdapter(WASM_DIR)
|
const ts = createTypeScriptAdapter(WASM_DIR)
|
||||||
const py = createPythonAdapter(WASM_DIR)
|
const py = createPythonAdapter(WASM_DIR)
|
||||||
|
|
||||||
interface Translated { kind: string, segments: Segment[] }
|
/** 一块的译文:一行译文在 segments 里;注释块是 Markdown,原文在 markdown 里,segments 为空。 */
|
||||||
|
interface Translated { kind: string, segments: Segment[], markdown?: string }
|
||||||
|
|
||||||
/** 一行译文块的译文,按 header 起始行(1 起)索引;同一行有多块时取外层那块。属性表块见 describeAll。 */
|
/** 一行译文块、注释块的译文,按 header 起始行(1 起)索引;同一行有多块时取外层那块。属性表块见 describeAll。 */
|
||||||
function translateAll(adapter: LanguageAdapter, languageId: string, translator: Translator, source: string): Promise<Map<number, Translated>> {
|
function translateAll(adapter: LanguageAdapter, languageId: string, translator: Translator, source: string): Promise<Map<number, Translated>> {
|
||||||
return withBlocks(adapter, languageId, source, (blocks) => {
|
return withBlocks(adapter, languageId, source, (blocks) => {
|
||||||
const byLine = new Map<number, Translated>()
|
const byLine = new Map<number, Translated>()
|
||||||
walk(blocks, (block) => {
|
walk(blocks, (block) => {
|
||||||
const line = block.header.startRow + 1
|
const line = block.header.startRow + 1
|
||||||
if (!byLine.has(line) && !('facts' in block))
|
if (byLine.has(line) || 'facts' in block)
|
||||||
byLine.set(line, { kind: block.kind, segments: translator.translate(block, source) })
|
return
|
||||||
|
const translation = translator.translate(block, source)
|
||||||
|
byLine.set(line, 'markdown' in translation
|
||||||
|
? { kind: block.kind, segments: [], markdown: translation.markdown }
|
||||||
|
: { kind: block.kind, segments: translation })
|
||||||
})
|
})
|
||||||
return byLine
|
return byLine
|
||||||
})
|
})
|
||||||
@@ -44,7 +49,10 @@ function rowTexts(map: Map<number, PropertyRow[]>): Record<number, string[]> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function valueOf(rows: PropertyRow[] | undefined, label: string): Segment[] {
|
function valueOf(rows: PropertyRow[] | undefined, label: string): Segment[] {
|
||||||
return rows?.find(row => row.label === label)?.value ?? []
|
const value = rows?.find(row => row.label === label)?.value ?? []
|
||||||
|
if ('markdown' in value)
|
||||||
|
throw new Error(`「${label}」是 Markdown`)
|
||||||
|
return value
|
||||||
}
|
}
|
||||||
|
|
||||||
function text(segments: Segment[] | undefined): string {
|
function text(segments: Segment[] | undefined): string {
|
||||||
@@ -52,7 +60,7 @@ function text(segments: Segment[] | undefined): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function texts(map: Map<number, Translated>): Record<number, string> {
|
function texts(map: Map<number, Translated>): Record<number, string> {
|
||||||
return Object.fromEntries([...map].map(([line, { segments }]) => [line, text(segments)]))
|
return Object.fromEntries([...map].map(([line, { segments, markdown }]) => [line, markdown ?? text(segments)]))
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 只看文字和着色:去掉各段的源码下标、属性行讲的定义(这两样另有用例)。 */
|
/** 只看文字和着色:去掉各段的源码下标、属性行讲的定义(这两样另有用例)。 */
|
||||||
@@ -93,14 +101,23 @@ describe('typeScript 中文直译', () => {
|
|||||||
expect(text(issue.get(20)?.segments)).toBe('返回 { locked: 真, prerequisiteNumber: prereq.number, prerequisiteColumn: prereq.column }')
|
expect(text(issue.get(20)?.segments)).toBe('返回 { locked: 真, prerequisiteNumber: prereq.number, prerequisiteColumn: prereq.column }')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('其余块:结构照抄,复合语句去掉结尾的 {,注释去掉注释符号', async () => {
|
it('其余块:结构照抄,复合语句去掉结尾的 {,注释去掉注释符号、保留换行', async () => {
|
||||||
const env = texts(await translateAll(ts, 'typescript', typescriptZhTranslator, readFixture('envLock.ts.txt')))
|
const env = texts(await translateAll(ts, 'typescript', typescriptZhTranslator, readFixture('envLock.ts.txt')))
|
||||||
const issue = texts(await translateAll(ts, 'typescript', typescriptZhTranslator, readFixture('issueLock.ts.txt')))
|
const issue = texts(await translateAll(ts, 'typescript', typescriptZhTranslator, readFixture('issueLock.ts.txt')))
|
||||||
expect(env[32]).toBe('公共 接口 EnvLockResult')
|
expect(env[32]).toBe('公共 接口 EnvLockResult')
|
||||||
expect(env[42]).toBe('尝试')
|
expect(env[42]).toBe('尝试')
|
||||||
expect(env[46]).toBe('捕获 (err)')
|
expect(env[46]).toBe('捕获 (err)')
|
||||||
expect(env[47]).toBe('result.failed.push({ path: f, reason: 如果 err 属于 Error 则 err.message 否则 String(err) })')
|
expect(env[47]).toBe('result.failed.push({ path: f, reason: 如果 err 属于 Error 则 err.message 否则 String(err) })')
|
||||||
expect(env[1]).toBe('.env file lock / unlock helpers. Recursively scans the main workspace folder for `.env*` files (skipping `node_modules`, `.git`, `.claude`) and chmods them to 0o444 (locked) or 0o644 (unlocked) in bulk. Backs the toolbar Lock toggle in `PanelHeader`. No persistent watcher — re-scan happens on every toggle, so newly created `.env` files are only picked up the next time the user clicks the button.')
|
expect(env[1]).toBe([
|
||||||
|
'.env file lock / unlock helpers.',
|
||||||
|
'',
|
||||||
|
'Recursively scans the main workspace folder for `.env*` files (skipping',
|
||||||
|
'`node_modules`, `.git`, `.claude`) and chmods them to 0o444 (locked) or',
|
||||||
|
'0o644 (unlocked) in bulk. Backs the toolbar Lock toggle in `PanelHeader`.',
|
||||||
|
'',
|
||||||
|
'No persistent watcher — re-scan happens on every toggle, so newly created',
|
||||||
|
'`.env` files are only picked up the next time the user clicks the button.',
|
||||||
|
].join('\n'))
|
||||||
expect(issue[5]).toBe('prerequisiteNumber?: 数值')
|
expect(issue[5]).toBe('prerequisiteNumber?: 数值')
|
||||||
expect(issue[15]).toBe('如果 (非 issue 或者 issue.prerequisite 全等 未定义)')
|
expect(issue[15]).toBe('如果 (非 issue 或者 issue.prerequisite 全等 未定义)')
|
||||||
})
|
})
|
||||||
@@ -382,7 +399,7 @@ describe('属性表', () => {
|
|||||||
const rows = colors(env.get(18) ?? [])
|
const rows = colors(env.get(18) ?? [])
|
||||||
expect(rows.map(row => row.label)).toEqual(['名称', '说明', '访问级别', '异步', '输入参数', '输出参数'])
|
expect(rows.map(row => row.label)).toEqual(['名称', '说明', '访问级别', '异步', '输入参数', '输出参数'])
|
||||||
expect(rows[0].value).toEqual([{ text: 'findEnvFiles', role: 'function' }])
|
expect(rows[0].value).toEqual([{ text: 'findEnvFiles', role: 'function' }])
|
||||||
expect(rows[1].value).toEqual([{ text: valueOf(env.get(18), '说明')[0].text, role: 'comment' }])
|
expect(rows[1].value).toEqual({ markdown: expect.stringMatching(/^Scan the workspace folder/) })
|
||||||
expect(rows[2].value).toEqual([{ text: '公共', role: 'keyword' }])
|
expect(rows[2].value).toEqual([{ text: '公共', role: 'keyword' }])
|
||||||
expect(rows[3].value).toEqual([{ text: '是', role: 'storage' }])
|
expect(rows[3].value).toEqual([{ text: '是', role: 'storage' }])
|
||||||
expect(rows[4].value).toEqual([{ text: '1 个', role: 'hint' }])
|
expect(rows[4].value).toEqual([{ text: '1 个', role: 'hint' }])
|
||||||
@@ -447,6 +464,57 @@ describe('属性表', () => {
|
|||||||
expect(pyRows[8].slice(0, 2)).toEqual(['名称: g', '访问级别: 公共'])
|
expect(pyRows[8].slice(0, 2)).toEqual(['名称: g', '访问级别: 公共'])
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('注释块和函数的说明是 Markdown 原文:只去注释符号、引号和共同缩进,reST 角色、双反引号、JSDoc 标签、HTML 照抄', async () => {
|
||||||
|
const pySource = [
|
||||||
|
'"""手机号规范化 / 脱敏工具.',
|
||||||
|
'',
|
||||||
|
'凡是入库、比对的手机号都先过 :func:`normalize_phone`,日志里打印 ``user`` 时用 <b>mask</b>。',
|
||||||
|
'"""',
|
||||||
|
'# 用法:',
|
||||||
|
'#',
|
||||||
|
'# mask_phone("13800138000")',
|
||||||
|
'def mask_phone(phone: str) -> str:',
|
||||||
|
' """把中间四位换成 ``*``.',
|
||||||
|
'',
|
||||||
|
' - 不足 7 位原样返回',
|
||||||
|
' - 见 https://example.com/phone',
|
||||||
|
' """',
|
||||||
|
].join('\n')
|
||||||
|
const pyText = await translateAll(py, 'python', pythonZhTranslator, pySource)
|
||||||
|
expect(pyText.get(1)).toEqual({
|
||||||
|
kind: 'comment',
|
||||||
|
segments: [],
|
||||||
|
markdown: '手机号规范化 / 脱敏工具.\n\n凡是入库、比对的手机号都先过 :func:`normalize_phone`,日志里打印 ``user`` 时用 <b>mask</b>。',
|
||||||
|
})
|
||||||
|
expect(pyText.get(5)?.markdown).toBe('用法:\n\n mask_phone("13800138000")')
|
||||||
|
const pyRows = await describeAll(py, 'python', pythonZhTranslator, pySource)
|
||||||
|
expect(pyRows.get(8)?.find(row => row.label === '说明')?.value).toEqual({
|
||||||
|
markdown: '把中间四位换成 ``*``.\n\n- 不足 7 位原样返回\n- 见 https://example.com/phone',
|
||||||
|
})
|
||||||
|
|
||||||
|
const tsSource = [
|
||||||
|
'// 第一行',
|
||||||
|
'// - 缩进的列表',
|
||||||
|
'/* 单行块注释 */',
|
||||||
|
'/**',
|
||||||
|
' * 读配置,见 {@link load}。',
|
||||||
|
' *',
|
||||||
|
' * ```ts',
|
||||||
|
' * load(\'a\')',
|
||||||
|
' * ```',
|
||||||
|
' * @param path 文件路径',
|
||||||
|
' */',
|
||||||
|
'function load(path: string) {}',
|
||||||
|
].join('\n')
|
||||||
|
const tsText = texts(await translateAll(ts, 'typescript', typescriptZhTranslator, tsSource))
|
||||||
|
expect(tsText[1]).toBe('第一行\n - 缩进的列表')
|
||||||
|
expect(tsText[3]).toBe('单行块注释')
|
||||||
|
const tsRows = await describeAll(ts, 'typescript', typescriptZhTranslator, tsSource)
|
||||||
|
expect(tsRows.get(4)?.find(row => row.label === '说明')?.value).toEqual({
|
||||||
|
markdown: '读配置,见 {@link load}。\n\n```ts\nload(\'a\')\n```\n@param path 文件路径',
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
it('导入:默认导出、全部、整个模块、副作用导入;Python 没有「仅类型」', async () => {
|
it('导入:默认导出、全部、整个模块、副作用导入;Python 没有「仅类型」', async () => {
|
||||||
const tsSource = [
|
const tsSource = [
|
||||||
'import d, { b as c } from \'y\'',
|
'import d, { b as c } from \'y\'',
|
||||||
@@ -517,7 +585,7 @@ describe('源码下标(跳到定义用)', () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function propertySegments(rows: PropertyRow[]): Segment[] {
|
function propertySegments(rows: PropertyRow[]): Segment[] {
|
||||||
return rows.flatMap(row => [...row.value, ...(row.items ?? []).flatMap(item => item.cells.flat())])
|
return rows.flatMap(row => [...('markdown' in row.value ? [] : row.value), ...(row.items ?? []).flatMap(item => item.cells.flat())])
|
||||||
}
|
}
|
||||||
|
|
||||||
it('python:点分模块名每一节、导入项、别名各自带下标,点号不带;前面有中文注释也对得上', async () => {
|
it('python:点分模块名每一节、导入项、别名各自带下标,点号不带;前面有中文注释也对得上', async () => {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import type { Access, Defines, FactBlock, FunctionFacts, ImportFacts, ImportItem, Node, PropertyRow, Segment, SourceSpan, TextBlock, TokenRole, Translator, VariableBlock } from '../contract'
|
import type { Access, Defines, FactBlock, FunctionFacts, ImportFacts, ImportItem, Markdown, Node, PreviewText, 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 后「未中断时」。 */
|
||||||
@@ -102,13 +102,15 @@ function oneLine(text: string): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 说明文字排版:去掉行尾空白、首尾空行和共同缩进,保留换行(段落之间的空行也留着)。
|
* 注释的 Markdown 原文:各个注释节点去掉注释符号后(texts)拼起来,去掉行尾空白、首尾空行和共同缩进,保留换行(段落之间的空行也留着)。
|
||||||
* 第一行紧跟在引号 / 注释符号后面,本来就没有缩进,不参与算共同缩进(同 PEP 257 的做法)。
|
* 只有一个节点(块注释、docstring)时第一行紧跟在引号 / 注释符号后面,本来就没有缩进,不参与算共同缩进(同 PEP 257 的做法);
|
||||||
|
* 几条行注释连成的一块每行地位相同,第一行也算,缩进四格的代码块才留得住。
|
||||||
*/
|
*/
|
||||||
function docText(raw: string): string {
|
function docText(texts: string[]): string {
|
||||||
const [first = '', ...rest] = splitLines(raw).map(line => line.trimEnd())
|
const lines = splitLines(texts.join('\n')).map(line => line.trimEnd())
|
||||||
const indent = commonIndent(rest).length
|
const quoted = texts.length === 1
|
||||||
return [first.trim(), ...rest.map(line => line.slice(indent))].join('\n').replace(/^\n+|\n+$/g, '')
|
const indent = commonIndent(quoted ? lines.slice(1) : lines).length
|
||||||
|
return lines.map((line, i) => quoted && i === 0 ? line.trim() : line.slice(indent)).join('\n').replace(/^\n+|\n+$/g, '')
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 模板槽位引用的子节点;注释是语法之外的节点,不占具名子节点序号。 */
|
/** 模板槽位引用的子节点;注释是语法之外的节点,不占具名子节点序号。 */
|
||||||
@@ -261,15 +263,14 @@ export function createZhTemplateTranslator(table: ZhTable): Translator {
|
|||||||
return segments
|
return segments
|
||||||
}
|
}
|
||||||
|
|
||||||
function translateComment(block: TextBlock): Segment[] {
|
function translateComment(block: TextBlock): Markdown {
|
||||||
const { startIndex, endIndex } = block.header
|
const { startIndex, endIndex } = block.header
|
||||||
const root = block.node.tree.rootNode
|
const root = block.node.tree.rootNode
|
||||||
const nodes = block.node.type === 'comment' ? commentsWithin(root.descendantForIndex(startIndex, endIndex) ?? root, block.header) : [block.node]
|
const nodes = block.node.type === 'comment' ? commentsWithin(root.descendantForIndex(startIndex, endIndex) ?? root, block.header) : [block.node]
|
||||||
const text = nodes.map(table.commentText).join('\n').split('\n').map(line => line.trim()).filter(Boolean).join(' ')
|
return { markdown: docText(nodes.map(table.commentText)) }
|
||||||
return text ? [{ text, role: 'comment' }] : []
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function translate(block: TextBlock, source: string): Segment[] {
|
function translate(block: TextBlock, source: string): PreviewText {
|
||||||
if (block.kind === 'comment')
|
if (block.kind === 'comment')
|
||||||
return translateComment(block)
|
return translateComment(block)
|
||||||
const tokens = tokensIn(block.node, block.header, source)
|
const tokens = tokensIn(block.node, block.header, source)
|
||||||
@@ -299,8 +300,8 @@ export function createZhTemplateTranslator(table: ZhTable): Translator {
|
|||||||
module: node => stringNodes.has(node.type) ? [segment(node.text, 'string', offsetOf(node))] : verbatim(node, source, 'type'),
|
module: node => stringNodes.has(node.type) ? [segment(node.text, 'string', offsetOf(node))] : verbatim(node, source, 'type'),
|
||||||
name: (node, role) => [segment(node.text, role, offsetOf(node))],
|
name: (node, role) => [segment(node.text, role, offsetOf(node))],
|
||||||
doc: (node) => {
|
doc: (node) => {
|
||||||
const text = docText(table.commentText(node))
|
const markdown = docText([table.commentText(node)])
|
||||||
return text ? [{ text, role: 'comment' }] : []
|
return markdown ? { markdown } : undefined
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
switch (block.kind) {
|
switch (block.kind) {
|
||||||
@@ -362,8 +363,8 @@ interface Phrases {
|
|||||||
module: (node: Node) => Segment[]
|
module: (node: Node) => Segment[]
|
||||||
/** 源码里的一个名字原样照抄、按 role 着色(函数名称)。 */
|
/** 源码里的一个名字原样照抄、按 role 着色(函数名称)。 */
|
||||||
name: (node: Node, role: TokenRole) => Segment[]
|
name: (node: Node, role: TokenRole) => Segment[]
|
||||||
/** 函数的说明(JSDoc、docstring):去掉注释符号、引号和共同缩进,保留换行,按注释着色;没有文字时是空的。 */
|
/** 函数的说明(JSDoc、docstring):去掉注释符号、引号和共同缩进的 Markdown 原文;没有文字时没有。 */
|
||||||
doc: (node: Node) => Segment[]
|
doc: (node: Node) => Markdown | undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
const ACCESS_ZH: Record<Access, string> = { public: '公共', private: '私有', protected: '受保护' }
|
const ACCESS_ZH: Record<Access, string> = { public: '公共', private: '私有', protected: '受保护' }
|
||||||
@@ -416,10 +417,10 @@ function aliased(item: Segment[], alias: Node | undefined, say: Phrases): Segmen
|
|||||||
/** 函数:名称 / 访问级别 / 异步 / 装饰器 / 输入参数(每个参数一行:名称、类型、默认值)/ 输出参数。 */
|
/** 函数:名称 / 访问级别 / 异步 / 装饰器 / 输入参数(每个参数一行:名称、类型、默认值)/ 输出参数。 */
|
||||||
function functionRows(facts: FunctionFacts, say: Phrases): PropertyRow[] {
|
function functionRows(facts: FunctionFacts, say: Phrases): PropertyRow[] {
|
||||||
const decorators = facts.decorators.map(say.verbatim)
|
const decorators = facts.decorators.map(say.verbatim)
|
||||||
const doc = facts.doc ? say.doc(facts.doc) : []
|
const doc = facts.doc && say.doc(facts.doc)
|
||||||
return [
|
return [
|
||||||
{ label: '名称', value: facts.name ? say.name(facts.name, 'function') : [hint('匿名')] },
|
{ label: '名称', value: facts.name ? say.name(facts.name, 'function') : [hint('匿名')] },
|
||||||
...(doc.length > 0 ? [{ label: '说明', value: doc }] : []),
|
...(doc ? [{ label: '说明', value: doc }] : []),
|
||||||
...accessRows(facts.access),
|
...accessRows(facts.access),
|
||||||
{ label: '异步', value: yesNo(facts.async) },
|
{ label: '异步', value: yesNo(facts.async) },
|
||||||
...(decorators.length > 0 ? [{ label: '装饰器', value: joined(decorators, '\n') }] : []),
|
...(decorators.length > 0 ? [{ label: '装饰器', value: joined(decorators, '\n') }] : []),
|
||||||
|
|||||||
@@ -18,6 +18,7 @@
|
|||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"lucide-react": "^1.16.0",
|
"lucide-react": "^1.16.0",
|
||||||
|
"markdown-it": "^15.0.2",
|
||||||
"react": "^19.0.0",
|
"react": "^19.0.0",
|
||||||
"react-dom": "^19.0.0",
|
"react-dom": "^19.0.0",
|
||||||
"tailwind-merge": "^3.6.0"
|
"tailwind-merge": "^3.6.0"
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { Fragment } from 'react'
|
|||||||
import { cn } from '@/lib/utils'
|
import { cn } from '@/lib/utils'
|
||||||
import { CodeEditor } from './CodeEditor'
|
import { CodeEditor } from './CodeEditor'
|
||||||
import { PropertyGrid } from './PropertyGrid'
|
import { PropertyGrid } from './PropertyGrid'
|
||||||
import { SegmentText } from './SegmentText'
|
import { TextPreview } from './TextPreview'
|
||||||
|
|
||||||
const KIND_LABEL: Record<BlockKind, string> = {
|
const KIND_LABEL: Record<BlockKind, string> = {
|
||||||
import: '导入',
|
import: '导入',
|
||||||
@@ -66,11 +66,7 @@ export function BlockLevel({ blocks, kind, tree }: { blocks: BlockView[], kind?:
|
|||||||
function BlockPreview({ block, tree }: { block: BlockView, tree: BlockTreeState }): ReactElement {
|
function BlockPreview({ block, tree }: { block: BlockView, tree: BlockTreeState }): ReactElement {
|
||||||
if ('properties' in block)
|
if ('properties' in block)
|
||||||
return <PropertyGrid blockId={block.id} rows={block.properties} collapsed={tree.collapsed} onToggleFold={tree.onToggleFold} />
|
return <PropertyGrid blockId={block.id} rows={block.properties} collapsed={tree.collapsed} onToggleFold={tree.onToggleFold} />
|
||||||
return (
|
return <TextPreview text={block.translation} />
|
||||||
<p className="cr-code">
|
|
||||||
<SegmentText segments={block.translation} />
|
|
||||||
</p>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 一块:折叠箭头、种类标签、预览内容或源码、右边的「编辑 / 预览」切换。 */
|
/** 一块:折叠箭头、种类标签、预览内容或源码、右边的「编辑 / 预览」切换。 */
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import type { PropertyItem, PropertyRow } 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 { propertyItemId, propertyRowId } from './viewState'
|
import { propertyItemId, propertyRowId } from './viewState'
|
||||||
|
|
||||||
/** 子行里除最后一列以外各列的宽度(参数的名称、类型),最后一列占满剩下的;编辑区窄时按比例收,不把最后一列挤没 */
|
/** 子行里除最后一列以外各列的宽度(参数的名称、类型),最后一列占满剩下的;编辑区窄时按比例收,不把最后一列挤没 */
|
||||||
@@ -69,9 +70,7 @@ export function PropertyGrid({ blockId, rows, collapsed, onToggleFold }: {
|
|||||||
</button>
|
</button>
|
||||||
)
|
)
|
||||||
: <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>}
|
||||||
<p className="cr-code min-w-0 flex-1">
|
<TextPreview text={row.value} className="min-w-0 flex-1" />
|
||||||
<SegmentText segments={row.value} />
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
{items.length > 0 && !folded && <PropertyItems rowId={id} items={items} />}
|
{items.length > 0 && !folded && <PropertyItems rowId={id} items={items} />}
|
||||||
</Fragment>
|
</Fragment>
|
||||||
|
|||||||
@@ -0,0 +1,16 @@
|
|||||||
|
import type { ReactElement } from 'react'
|
||||||
|
import type { PreviewText } from './types'
|
||||||
|
import { cn } from '@/lib/utils'
|
||||||
|
import { renderMarkdown } from './markdown'
|
||||||
|
import { SegmentText } from './SegmentText'
|
||||||
|
|
||||||
|
/** 预览里的一段文字:着色的词按源码排版;Markdown 说明渲染成段落、列表、代码块。 */
|
||||||
|
export function TextPreview({ text, className }: { text: PreviewText, className?: string }): ReactElement {
|
||||||
|
if ('markdown' in text)
|
||||||
|
return <div className={cn('cr-markdown', className)} dangerouslySetInnerHTML={{ __html: renderMarkdown(text.markdown) }} />
|
||||||
|
return (
|
||||||
|
<p className={cn('cr-code', className)}>
|
||||||
|
<SegmentText segments={text} />
|
||||||
|
</p>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -163,6 +163,85 @@ body.vscode-high-contrast-light {
|
|||||||
.cr-role-comment { color: var(--cr-comment); }
|
.cr-role-comment { color: var(--cr-comment); }
|
||||||
.cr-role-hint { color: var(--vscode-descriptionForeground); }
|
.cr-role-hint { color: var(--vscode-descriptionForeground); }
|
||||||
|
|
||||||
|
/* 注释块、函数说明的 Markdown:正文界面字体、注释色,和属性名一样的行高;代码用编辑器字体、垫代码块底色。
|
||||||
|
间距压紧:一行注释和一行译文一样高,标题只比正文稍大 */
|
||||||
|
.cr-markdown {
|
||||||
|
font-family: var(--vscode-font-family, system-ui, sans-serif), "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", "Noto Sans CJK SC", sans-serif;
|
||||||
|
font-size: var(--vscode-font-size, 13px);
|
||||||
|
line-height: 20px;
|
||||||
|
color: var(--cr-comment);
|
||||||
|
overflow-wrap: break-word;
|
||||||
|
}
|
||||||
|
.cr-markdown :is(p, ul, ol, pre, blockquote, table, h1, h2, h3, h4, h5, h6, hr) {
|
||||||
|
margin: 0 0 6px;
|
||||||
|
}
|
||||||
|
.cr-markdown :is(li, blockquote) > :last-child,
|
||||||
|
.cr-markdown > :last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
.cr-markdown :is(ul, ol) {
|
||||||
|
padding-left: 20px;
|
||||||
|
}
|
||||||
|
.cr-markdown ul { list-style: disc; }
|
||||||
|
.cr-markdown ol { list-style: decimal; }
|
||||||
|
.cr-markdown :is(ul, ol) ul { list-style: circle; }
|
||||||
|
.cr-markdown :is(h1, h2, h3, h4, h5, h6) {
|
||||||
|
font-size: 1em;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
.cr-markdown h1 { font-size: 1.15em; }
|
||||||
|
.cr-markdown h2 { font-size: 1.08em; }
|
||||||
|
/* 行内代码换了字体,行高压成 1 才不把这一行撑高 */
|
||||||
|
.cr-markdown code {
|
||||||
|
padding: 1px 4px;
|
||||||
|
border-radius: 3px;
|
||||||
|
font-family: var(--vscode-editor-font-family, monospace), "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", "Noto Sans CJK SC", monospace;
|
||||||
|
font-size: 1em;
|
||||||
|
line-height: 1;
|
||||||
|
background: var(--vscode-textCodeBlock-background);
|
||||||
|
}
|
||||||
|
.cr-markdown pre {
|
||||||
|
padding: 4px 8px;
|
||||||
|
border-radius: 4px;
|
||||||
|
background: var(--vscode-textCodeBlock-background);
|
||||||
|
white-space: pre-wrap;
|
||||||
|
tab-size: 4;
|
||||||
|
}
|
||||||
|
.cr-markdown pre code {
|
||||||
|
padding: 0;
|
||||||
|
font-size: var(--vscode-editor-font-size, 12.5px);
|
||||||
|
line-height: inherit;
|
||||||
|
background: none;
|
||||||
|
}
|
||||||
|
.cr-markdown a {
|
||||||
|
color: var(--vscode-textLink-foreground);
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
.cr-markdown a:hover {
|
||||||
|
color: var(--vscode-textLink-activeForeground);
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
.cr-markdown blockquote {
|
||||||
|
padding: 0 8px;
|
||||||
|
border-left: 3px solid var(--vscode-textBlockQuote-border);
|
||||||
|
background: var(--vscode-textBlockQuote-background);
|
||||||
|
}
|
||||||
|
.cr-markdown table {
|
||||||
|
border-collapse: collapse;
|
||||||
|
}
|
||||||
|
.cr-markdown :is(th, td) {
|
||||||
|
padding: 0 8px;
|
||||||
|
border: 1px solid color-mix(in srgb, currentColor 30%, transparent);
|
||||||
|
}
|
||||||
|
.cr-markdown th {
|
||||||
|
font-weight: 600;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
.cr-markdown hr {
|
||||||
|
border: 0;
|
||||||
|
border-top: 1px solid color-mix(in srgb, currentColor 30%, transparent);
|
||||||
|
}
|
||||||
|
|
||||||
/* 按住 ⌘ / Ctrl 停在查得到定义的名字上:和编辑器里的链接一样 */
|
/* 按住 ⌘ / Ctrl 停在查得到定义的名字上:和编辑器里的链接一样 */
|
||||||
.cr-code [data-link] {
|
.cr-code [data-link] {
|
||||||
color: var(--vscode-editorLink-activeForeground);
|
color: var(--vscode-editorLink-activeForeground);
|
||||||
|
|||||||
@@ -0,0 +1,42 @@
|
|||||||
|
import { describe, expect, it } from 'vitest'
|
||||||
|
import { renderMarkdown } from './markdown'
|
||||||
|
|
||||||
|
describe('renderMarkdown', () => {
|
||||||
|
it('一行文字是一个段落;段内换行是软换行,空行分段', () => {
|
||||||
|
expect(renderMarkdown('读配置。')).toBe('<p>读配置。</p>\n')
|
||||||
|
expect(renderMarkdown('第一行\n接着写\n\n第二段')).toBe('<p>第一行\n接着写</p>\n<p>第二段</p>\n')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('行内代码(含双反引号)、强调、列表、代码块、标题、引用、表格', () => {
|
||||||
|
expect(renderMarkdown('先过 :func:`normalize_phone`,打印 ``user`` 时 **脱敏**')).toBe(
|
||||||
|
'<p>先过 :func:<code>normalize_phone</code>,打印 <code>user</code> 时 <strong>脱敏</strong></p>\n',
|
||||||
|
)
|
||||||
|
expect(renderMarkdown('- 甲\n- 乙')).toBe('<ul>\n<li>甲</li>\n<li>乙</li>\n</ul>\n')
|
||||||
|
expect(renderMarkdown('```ts\nload(\'a\')\n```')).toBe('<pre><code class="language-ts">load(\'a\')\n</code></pre>\n')
|
||||||
|
expect(renderMarkdown('例子:\n\n f(1)')).toBe('<p>例子:</p>\n<pre><code>f(1)\n</code></pre>\n')
|
||||||
|
expect(renderMarkdown('# 标题')).toBe('<h1>标题</h1>\n')
|
||||||
|
expect(renderMarkdown('> 引用')).toBe('<blockquote>\n<p>引用</p>\n</blockquote>\n')
|
||||||
|
expect(renderMarkdown('| a | b |\n| - | - |\n| 1 | 2 |')).toContain('<table>')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('源码里的 HTML 当文字显示', () => {
|
||||||
|
expect(renderMarkdown('用 <b>mask</b> <script>alert(1)</script>')).toBe(
|
||||||
|
'<p>用 <b>mask</b> <script>alert(1)</script></p>\n',
|
||||||
|
)
|
||||||
|
expect(renderMarkdown('<div onclick="x()">块</div>')).toBe('<p><div onclick="x()">块</div></p>\n')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('带协议的网址、邮箱自动成链接;不带协议的文件名不算', () => {
|
||||||
|
expect(renderMarkdown('见 https://example.com/phone')).toBe('<p>见 <a href="https://example.com/phone">https://example.com/phone</a></p>\n')
|
||||||
|
expect(renderMarkdown('[文档](http://example.com)')).toBe('<p><a href="http://example.com">文档</a></p>\n')
|
||||||
|
expect(renderMarkdown('找 dev@example.com')).toContain('<a href="mailto:dev@example.com">')
|
||||||
|
expect(renderMarkdown('改 sample.py 和 README.md')).toBe('<p>改 sample.py 和 README.md</p>\n')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('链接只认 http / https / mailto,别的照原文显示;图片不渲染', () => {
|
||||||
|
expect(renderMarkdown('[点](javascript:alert(1))')).not.toContain('<a')
|
||||||
|
expect(renderMarkdown('[命令](command:workbench.action.reloadWindow)')).not.toContain('<a')
|
||||||
|
expect(renderMarkdown('[相对](./x.md)')).not.toContain('<a')
|
||||||
|
expect(renderMarkdown('')).not.toContain('<img')
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
import MarkdownIt from 'markdown-it'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 注释、函数说明的 Markdown 排版:CommonMark 加 GFM 的表格、删除线,带 http(s):// 的网址和邮箱自动成链接。
|
||||||
|
* 源码里的 HTML 当文字显示;链接只认 http / https / mailto,别的写法照原文显示——
|
||||||
|
* webview 会拦下 <a> 的点击交给 VS Code 在外部打开。图片不渲染:webview 的 CSP 不放行外部图片。
|
||||||
|
*/
|
||||||
|
const md = new MarkdownIt({ html: false, linkify: true }).disable('image')
|
||||||
|
md.validateLink = url => /^(?:https?|mailto):/i.test(url)
|
||||||
|
|
||||||
|
/** 最近渲染过的文字 → HTML,最多留 CACHE_SIZE 段,最久没用的先丢。 */
|
||||||
|
const CACHE_SIZE = 500
|
||||||
|
const cache = new Map<string, string>()
|
||||||
|
|
||||||
|
/** Markdown 原文 → HTML;同一段文字只渲染一次。 */
|
||||||
|
export function renderMarkdown(text: string): string {
|
||||||
|
const html = cache.get(text) ?? md.render(text)
|
||||||
|
cache.delete(text)
|
||||||
|
cache.set(text, html)
|
||||||
|
if (cache.size > CACHE_SIZE)
|
||||||
|
cache.delete(cache.keys().next().value!)
|
||||||
|
return html
|
||||||
|
}
|
||||||
@@ -56,6 +56,17 @@ describe('跳到定义:滚到哪块、展开哪些', () => {
|
|||||||
expect(revealPlan(BLOCKS, 4, 102)).toEqual({ id: '1.0.0', expand: ['1', '1.0'] })
|
expect(revealPlan(BLOCKS, 4, 102)).toEqual({ id: '1.0.0', expand: ['1', '1.0'] })
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('注释块、函数「说明」的 Markdown 里的名字不带下标,不算显示着定义', () => {
|
||||||
|
const blocks: BlockView[] = [
|
||||||
|
{ id: '0', kind: 'comment', startLine: 0, endLine: 0, code: '', codeTokens: [], children: [], translation: { markdown: '见 `get_db`' } },
|
||||||
|
factBlock('1', 'function', 1, [
|
||||||
|
{ label: '说明', value: { markdown: '同 `get_db`' } },
|
||||||
|
{ label: '输入参数', value: [{ text: '1 个', role: 'hint' }], items: [{ cells: [[name('get_db', 30)]] }] },
|
||||||
|
]),
|
||||||
|
]
|
||||||
|
expect(revealPlan(blocks, 0, 30)).toEqual({ id: '1', expand: ['1/p1'] })
|
||||||
|
})
|
||||||
|
|
||||||
it('翻译插件没标 defines、名字在属性行展开的子行里(参数):闪整块,连那一行一起展开', () => {
|
it('翻译插件没标 defines、名字在属性行展开的子行里(参数):闪整块,连那一行一起展开', () => {
|
||||||
expect(revealPlan(BLOCKS, 2, 57)).toEqual({ id: '1', expand: ['1/p2'] })
|
expect(revealPlan(BLOCKS, 2, 57)).toEqual({ id: '1', expand: ['1/p2'] })
|
||||||
expect(revealPlan(BLOCKS, 0, 34)).toEqual({ id: '0', expand: ['0/p1'] })
|
expect(revealPlan(BLOCKS, 0, 34)).toEqual({ id: '0', expand: ['0/p1'] })
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import type { BlockView, Segment, SourceRange } from './types'
|
import type { BlockView, PreviewText, SourceRange } from './types'
|
||||||
import { propertyItemId, propertyRowId } from './viewState'
|
import { propertyItemId, propertyRowId } from './viewState'
|
||||||
|
|
||||||
/** 跳到定义时要滚到的块,就是这处定义的属性行 / 子行(只闪它),以及要展开的块和属性行:它们折叠着,目标就不在页面上。 */
|
/** 跳到定义时要滚到的块,就是这处定义的属性行 / 子行(只闪它),以及要展开的块和属性行:它们折叠着,目标就不在页面上。 */
|
||||||
@@ -30,8 +30,9 @@ function covers(range: SourceRange | undefined, offset: number): boolean {
|
|||||||
return range !== undefined && range.startIndex <= offset && offset < range.endIndex
|
return range !== undefined && range.startIndex <= offset && offset < range.endIndex
|
||||||
}
|
}
|
||||||
|
|
||||||
function shows(segments: Segment[], offset: number): boolean {
|
/** 文字里显示着 offset 处的名字;Markdown 说明里的名字不带下标。 */
|
||||||
return segments.some(segment => segment.offset === offset)
|
function shows(text: PreviewText, offset: number): boolean {
|
||||||
|
return !('markdown' in text) && text.some(segment => segment.offset === offset)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -17,6 +17,12 @@ export type TokenRole
|
|||||||
/** offset:照抄自源码里的名字、字符串时它在源码里的起始下标,⌘ / Ctrl 悬停、点击时拿它查定义;译词、提示词没有 */
|
/** offset:照抄自源码里的名字、字符串时它在源码里的起始下标,⌘ / Ctrl 悬停、点击时拿它查定义;译词、提示词没有 */
|
||||||
export interface Segment { text: string, role: TokenRole, offset?: number }
|
export interface Segment { text: string, role: TokenRole, offset?: number }
|
||||||
|
|
||||||
|
/** 一段 Markdown 说明文字:注释、JSDoc、docstring 去掉注释符号、引号和共同缩进后的原文,按 Markdown 渲染成注释色的段落、列表、代码块 */
|
||||||
|
export interface Markdown { markdown: string }
|
||||||
|
|
||||||
|
/** 预览里显示的一段文字:着色的词(一行译文、属性值),或整段 Markdown 说明(注释块、函数的说明) */
|
||||||
|
export type PreviewText = Segment[] | Markdown
|
||||||
|
|
||||||
/** 源码里一段的下标,end 为开区间 */
|
/** 源码里一段的下标,end 为开区间 */
|
||||||
export interface SourceRange { startIndex: number, endIndex: number }
|
export interface SourceRange { startIndex: number, endIndex: number }
|
||||||
|
|
||||||
@@ -29,10 +35,10 @@ export interface Defines { defines?: SourceRange }
|
|||||||
/** 属性表里展开的一个子行:几列对齐的格子,例如参数的名称 / 类型 / 默认值 */
|
/** 属性表里展开的一个子行:几列对齐的格子,例如参数的名称 / 类型 / 默认值 */
|
||||||
export interface PropertyItem extends Defines { cells: Segment[][] }
|
export interface PropertyItem extends Defines { cells: Segment[][] }
|
||||||
|
|
||||||
/** 属性表的一行:左列属性名,右列着色的值;items 是展开在这一行下面的子行(输入参数、导入项) */
|
/** 属性表的一行:左列属性名,右列着色的值或 Markdown 说明;items 是展开在这一行下面的子行(输入参数、导入项) */
|
||||||
export interface PropertyRow extends Defines {
|
export interface PropertyRow extends Defines {
|
||||||
label: string
|
label: string
|
||||||
value: Segment[]
|
value: PreviewText
|
||||||
items?: PropertyItem[]
|
items?: PropertyItem[]
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -52,8 +58,8 @@ interface BlockViewBase {
|
|||||||
children: BlockView[]
|
children: BlockView[]
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 函数、导入、常量、变量块预览时是属性表,其余是一行译文 */
|
/** 函数、导入、常量、变量块预览时是属性表,注释块是 Markdown 说明,其余是一行译文 */
|
||||||
export type BlockView = BlockViewBase & ({ translation: Segment[] } | { properties: PropertyRow[] })
|
export type BlockView = BlockViewBase & ({ translation: PreviewText } | { properties: PropertyRow[] })
|
||||||
|
|
||||||
export interface TranslatorOption { id: string, label: string }
|
export interface TranslatorOption { id: string, label: string }
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user