✨ feat(vscode): 代码阅读的输入参数排成带表头和边框的表格

This commit is contained in:
cruldra committed 2026-09-26 16:15:59 +08:00
1 parent 6416cdf714
commit 83adefdf5e
10 files changed
+197 -24

No files matched your search

+12 -6
View File
@@ -30,6 +30,7 @@
| 译文用词 | 结构照抄原语言,只翻关键字、内置类型、运算符;用专业术语(`export` → 公共,`catch` → 捕获,`===` → 全等) |
| 函数、类、导入、常量、变量块 | 预览不显示一行译文,显示树形属性表;属性值里的类型、表达式照常直译 |
| 字典 / 对象字面量 | 常量、变量的值是 Python 字典 / TypeScript 对象字面量时不挤成一长行:值写「字典 N 项」/「对象 N 项」,各项展开成「键 \| 值」两列的子行,值又是字典 / 对象时一层层往下展开;参数默认值照旧一行 |
| 输入参数 | 排成带表头和边框的表格:名称 \| 类型 \| 默认值,表头写了「默认值」,格子里不再写「默认 」 |
| 推导式(写在表达式里的遍历) | Python 的列表 / 集合 / 字典推导式、生成器表达式出现在译文任何地方(一行译文、属性表的值、默认值、字典项的值),都原地换成一个带循环图标和结果类型(生成器 / 列表 / 集合 / 字典)的小框,颜色同「循环」块,框里按读的顺序重排:「遍历 来源 的每个 变量,仅当 条件,取 值」。语言插件报结构,翻译插件定用词,webview 只认「框」不认语言 |
## 用户可见行为
@@ -97,12 +98,13 @@
### 属性表(函数、类、导入、常量、变量块)
卡片外观(折叠箭头、种类标签、底色、悬停高亮、「编辑 / 预览」切换)和其他块一样,只是内容换成一张属性表:左列属性名(界面字体、`descriptionForeground` 色,宽 76px),右列是值(等宽字体,按词性着色,长了换行),两列间距 12px,行高 20px。「输入参数」「导入项」和值是字典 / 对象字面量的「值」「初始值」下面展开子行,属性名前带折叠箭头(图标、大小、颜色同块的折叠箭头;点箭头或属性名切换,默认展开,折叠后值「N 个」「字典 N 项」照常显示,只收起子行);子行往右缩进 5px,左边一条 1px 竖线(`editorIndentGuide-background1`)。输入参数的子行分名称 / 类型 / 默认值三列对齐,前两列宽 150px / 220px,编辑区窄时按 28% / 38% 收,默认值列占剩下的;字典 / 对象的子行分键 / 值两列,键列同名称列。子行下面还能再展开一层(嵌套的字典 / 对象):带下一层的子行第一列前面有折叠箭头(点箭头或第一列切换,默认展开),同一层里其余子行在同样的位置留空、第一列对齐;下一层同样往右缩进 5px、左边一条竖线,竖线对着上一层的箭头,每层自己的两列对齐。同一层里没有带下一层的子行时不留箭头的位置(和参数、导入项的子行一样)。
卡片外观(折叠箭头、种类标签、底色、悬停高亮、「编辑 / 预览」切换)和其他块一样,只是内容换成一张属性表:左列属性名(界面字体、`descriptionForeground` 色,宽 76px),右列是值(等宽字体,按词性着色,长了换行),两列间距 12px,行高 20px。「输入参数」「导入项」和值是字典 / 对象字面量的「值」「初始值」下面展开子行,属性名前带折叠箭头(图标、大小、颜色同块的折叠箭头;点箭头或属性名切换,默认展开,折叠后值「N 个」「字典 N 项」照常显示,只收起子行);子行往右缩进 5px,左边一条 1px 竖线(`editorIndentGuide-background1`);输入参数例外,是一张表格(见下)。字典 / 对象的子行分键 / 值两列,键列宽 150px(编辑区窄时按 28% 收),值列占剩下的。子行下面还能再展开一层(嵌套的字典 / 对象):带下一层的子行第一列前面有折叠箭头(点箭头或第一列切换,默认展开),同一层里其余子行在同样的位置留空、第一列对齐;下一层同样往右缩进 5px、左边一条竖线,竖线对着上一层的箭头,每层自己的两列对齐。同一层里没有带下一层的子行时不留箭头的位置(和导入项的子行一样)。
输入参数的表格(原型 `paramRows`):往右缩进 5px,第一行是表头「名称 | 类型 | 默认值」,下面每个参数一行。名称、类型两列宽 150px / 220px(编辑区窄时按 28% / 38% 收),默认值列占剩下的;格子左右留 8px、上下 2px,长类型、长默认值在格子里换行,不挤掉别的列。外框 1px、圆角 4px,格子之间、行之间 1px 分隔线,颜色都是主题的表格分隔色(`--vscode-tree-tableColumnsBorder`,主题没给时退到 `--vscode-editorWidget-border`);表头垫主题的表头底色(`--vscode-keybindingTable-headerBackground`,比编辑区深一档或浅一档),列名界面字体 12px、`descriptionForeground` 色。没有默认值的格子留空。折叠「输入参数」那一行时整张表收起;子行 id、跳到定义闪「它那个参数」、高亮名字的各处出现都和列表一样,闪的是表格里那一行(行边框和相邻行共用,改在行里面描一圈聚焦色)。哪些子行排成表格由翻译插件定:给了列名(`PropertyRow.columns`)的排成表格,没给的(导入项、字典 / 对象的各项)照旧是列表,webview 只看有没有列名。
| 种类 | 行 |
| --- | --- |
| 函数 | 名称(函数色;匿名函数写「匿名」)/ 说明(有说明才显示:JSDoc 去掉 `/**`、`*/` 和每行开头的 `*`,docstring 去掉引号;再去掉共同缩进和首尾空行,`@param` 这类标签行照抄;得到的原文按 Markdown 渲染,见「注释的 Markdown 排版」;里面的名字不能点)/ 访问级别 / 异步(是 / 否)/ 装饰器(有才显示,源码原样照抄、按源码着色,多个换行)/ 输入参数(`N 个` 或「无」,每个参数一行:名称 \| 类型(没标注写「未标注」)\| 默认值(`默认 ` + 译文,没有留空))/ 输出参数(返回类型的译文,没标注写「未标注」) |
| 函数 | 名称(函数色;匿名函数写「匿名」)/ 说明(有说明才显示:JSDoc 去掉 `/**`、`*/` 和每行开头的 `*`,docstring 去掉引号;再去掉共同缩进和首尾空行,`@param` 这类标签行照抄;得到的原文按 Markdown 渲染,见「注释的 Markdown 排版」;里面的名字不能点)/ 访问级别 / 异步(是 / 否)/ 装饰器(有才显示,源码原样照抄、按源码着色,多个换行)/ 输入参数(`N 个` 或「无」,展开成表格,列名 名称 \| 类型 \| 默认值,每个参数一行:名称 \| 类型(没标注写「未标注」)\| 默认值(译文,没有留空))/ 输出参数(返回类型的译文,没标注写「未标注」) |
| 类 | 名称(类型色;匿名类 `export default class {}` 写「匿名」;赋给变量、类字段的类表达式写变量名、字段名)/ 说明(有说明才显示,规则同函数)/ 访问级别 / 抽象(是 / 否,只有 TypeScript / TSX 有,看 `abstract class`,TypeScript 的类都有这一行;JavaScript / JSX 没有)/ 装饰器(同函数)/ 类型参数(有才显示: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 / TSX 有,看 `import type`;JavaScript / JSX 没有)/ 导入项(`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`)就重复几组 |
@@ -357,8 +359,11 @@ export interface Defines { defines?: SourceRange }
/** 属性表里展开的一个子行:几列对齐的格子;items 是展开在它下面的下一层子行(嵌套的字典 / 对象),一层层可以一直往下套。 */
export interface PropertyItem extends Defines { cells: Inline[][], items?: PropertyItem[] }
/** 属性表的一行:属性名、着色的值或 Markdown 说明(函数、类的「说明」)、展开在下面的子行(输入参数、导入项、字典 / 对象的各项)。 */
export interface PropertyRow extends Defines { label: string, value: PreviewText, items?: PropertyItem[] }
/**
* 属性表的一行:属性名、着色的值或 Markdown 说明(函数、类的「说明」)、展开在下面的子行(输入参数、导入项、字典 / 对象的各项)。
* columns 是子行各列的列名(输入参数的名称 / 类型 / 默认值):给了的子行排成带表头的表格(只有一层),没给的排成列表。
*/
export interface PropertyRow extends Defines { label: string, value: PreviewText, items?: PropertyItem[], columns?: string[] }
/** 语言插件:语法树 → 代码块树。 */
export interface LanguageAdapter {
@@ -574,7 +579,7 @@ type BlockView = BlockViewBase & ({ translation: PreviewText } | { properties: P
| hint | `--vscode-descriptionForeground` | 同左 |
| plain | `--vscode-editor-foreground` | 同左 |
属性表里:说明按 comment,访问级别(公共 / 私有 / 受保护)和「作为」按 keyword,是 / 否按 storage,函数名称按 function,类名称按 type,未标注、无、N 个、字典 N 项、对象 N 项、展开、默认、默认导出、全部、整个模块、匿名按 hint。推导式框里的「遍历」「的每个」「仅当」「取」「异步遍历」按 keyword,框头按「循环」块的种类色。父类名(TypeScript `extends` 后面、JavaScript `class_heritage` 里、Python 类名后面括号里直接写的名字)源码着色和属性表里都按 type。
属性表里:说明按 comment,访问级别(公共 / 私有 / 受保护)和「作为」按 keyword,是 / 否按 storage,函数名称按 function,类名称按 type,未标注、无、N 个、字典 N 项、对象 N 项、展开、默认导出、全部、整个模块、匿名按 hint;输入参数表格的列名是界面文字(`descriptionForeground`),不是着色的词。推导式框里的「遍历」「的每个」「仅当」「取」「异步遍历」按 keyword,框头按「循环」块的种类色。父类名(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`。
@@ -605,7 +610,7 @@ vscode/src/codeReader/
editor/definitionAction.ts 查到的定义 → 怎么跳(纯函数)
editor/occurrences.ts 查名字的各处出现(vscode.executeDocumentHighlights)
index.ts registerCodeReader(context): CodeReaderApi
vscode/webview-ui/code-reader.html + src/codeReader/ 预览界面(React,Vite 第二个入口;属性表在 PropertyGrid.tsx,着色的词和框在 SegmentText.tsx,Markdown 渲染在 markdown.ts / TextPreview.tsx,筛选栏在 FilterBar.tsx,筛选规则在 filter.ts,高亮各处出现的规则在 occurrences.ts、接到页面上在 useOccurrences.ts)
vscode/webview-ui/code-reader.html + src/codeReader/ 预览界面(React,Vite 第二个入口;属性表在 PropertyGrid.tsx,子行排成表格还是列表在 subRows.ts,着色的词和框在 SegmentText.tsx,Markdown 渲染在 markdown.ts / TextPreview.tsx,筛选栏在 FilterBar.tsx,筛选规则在 filter.ts,高亮各处出现的规则在 occurrences.ts、接到页面上在 useOccurrences.ts)
```
## 测试
@@ -616,6 +621,7 @@ vscode/webview-ui/code-reader.html + src/codeReader/ 预览界面(React,Vit
- 属性表信息:`envLock.ts`、`sample.py`、`routes.py`(原型里的 FastAPI 路由)加内联片段,覆盖各种导入写法、形参写法、类成员访问修饰符、解构和多变量声明、Python 下划线规则;字典 / 对象字面量的各项:Python 字符串键、名字键、嵌套字典、`**` 展开、空字典、类属性,TypeScript 名字键、字符串键、计算属性名、简写、展开、方法和 getter(头部到主体之前)、嵌套对象、空对象,不是字面量的值没有 entries。
- 字典 / 对象的属性表:项数、键 | 值两列、嵌套一层层展开、「展开」、方法头部、空的一行;键和值里的名字、字符串带下标(嵌套的也是);每项标的定义是键,展开项不标;跳到定义落在对象属性名上只闪那一项、连外层各层一起展开,只是显示着的名字闪整块、展开到它那一层。
- 推导式:Python 语言插件读出的结构(调用唯一参数的生成器带 sharedParentheses、几个参数之一的不带,列表带条件,字典分出键和值,集合的解构变量,几节 for / if 照源码顺序,async for,套着的各自认,字面量和 for 语句不算);中文直译「推导式」一节的样例表逐字一致,框的样式、框头、关键字着色,顿号连写、条件位置、另起一句、异步遍历、集合、框里的框、多个参数旁边的生成器、跨行带注释的生成器,属性表的默认值和字典项的值里也换;框里的名字带下标且指回源码原文,高亮各处出现、跳到定义都认框里的名字(只显示在框里的推导式变量 → 框所在的块);TypeScript 不认。
- 输入参数表格:函数的「输入参数」行带列名 名称 / 类型 / 默认值(TS、Python 一样),默认值格子只有译文、没有就是空格子,别的行没有列名;webview 按有没有列名选表格还是列表,表格的格子按列数取。
- 翻译:上面两张样例表逐字一致;属性名不被翻;每个被翻的词单独成段;节点模板嵌套时正确、槽位里照样翻、没命中模板的节点照抄;属性表里的值和一行译文用同一套表和模板。
- `blockView`:`codeTokens` 拼起来等于 `code`;去缩进正确;函数、类、导入、常量、变量块只带属性表,其余块只带译文。
- 注释的 Markdown:注释块、函数和类「说明」的视图数据是 `{ markdown }`,原文照抄(reST 角色、双反引号、JSDoc 标签、HTML 都不转换),行注释连成的块留得住缩进四格的代码块;渲染时 HTML 当文字、带协议的网址和邮箱成链接、不带协议的文件名不成链接,`javascript:` / `command:` / 相对路径的链接和图片不渲染;跳到定义不认 Markdown 里的名字。
+5 -1
View File
@@ -236,11 +236,15 @@ export interface PropertyItem extends Defines {
items?: PropertyItem[]
}
/** 属性表的一行:左列属性名,右列着色的值或 Markdown 说明;items 是展开在这一行下面的子行(输入参数、导入项、字典 / 对象的各项)。 */
/**
* 属性表的一行:左列属性名,右列着色的值或 Markdown 说明;items 是展开在这一行下面的子行(输入参数、导入项、字典 / 对象的各项)。
* columns 是子行各列的列名(输入参数的名称 / 类型 / 默认值):给了列名的子行排成带表头和边框的表格,没给的排成列表;表格只有一层,子行不再往下套。
*/
export interface PropertyRow extends Defines {
label: string
value: PreviewText
items?: PropertyItem[]
columns?: string[]
}
/** 语言插件:语法树 → 代码块树。 */
@@ -433,7 +433,7 @@ describe('python 写在表达式里的遍历(推导式)', () => {
'CONFIG = {"ids": {k: v for k, v in pairs}}',
].join('\n')
const rows = rowTexts(await describeAll(py, 'python', pythonZhTranslator, source))
expect(rows[1]).toContain(' ids | 未标注 | 默认 ⟦列表|遍历 users 的每个 u,取 u.id⟧')
expect(rows[1]).toContain(' ids | 未标注 | ⟦列表|遍历 users 的每个 u,取 u.id⟧')
expect(rows[2]).toEqual(['名称: CONFIG', '访问级别: 公共', '值: 字典 1 项', ' "ids" | ⟦字典|遍历 pairs 的每个 k, v,取 k: v⟧'])
})
@@ -504,8 +504,8 @@ describe('属性表', () => {
'装饰器: @router.get("/{user_id}", response_model=UserDetailResponse)',
'输入参数: 3 个',
' user_id | Int4Path | ',
' db | AsyncSession | 默认 Depends(get_db)',
' current_user | User | 默认 Depends(get_current_user)',
' db | AsyncSession | Depends(get_db)',
' current_user | User | Depends(get_current_user)',
'输出参数: UserDetailResponse',
])
expect(routes[111]).toContain(' file | Annotated[UploadFile, File(..., description="头像文件")] | ')
@@ -518,7 +518,29 @@ describe('属性表', () => {
expect(port[1]).toEqual(['名称: _default_port', '访问级别: 私有', '初始值: 如果 _role 等于 "beat" 则 5680 否则 5679'])
})
it('属性名是纯文字;值按词性着色,说明词(未标注、N 个、默认)用 hint', async () => {
it('输入参数的子行排成表格:列名是名称 / 类型 / 默认值,默认值格子只写译文、没有就留空;导入项、字典的子行没有列名', async () => {
const pySource = [
'from m import a, b',
'def f(x: int = 1, y=None, *args): pass',
'D = {"k": 1}',
].join('\n')
const py1 = await describeAll(py, 'python', pythonZhTranslator, pySource)
const params = py1.get(2)?.find(row => row.label === '输入参数')
expect(params?.columns).toEqual(['名称', '类型', '默认值'])
expect(colors(params?.items?.map(item => item.cells))).toEqual([
[[{ text: 'x', role: 'variable' }], [{ text: '整数', role: 'type' }], [{ text: '1', role: 'number' }]],
[[{ text: 'y', role: 'variable' }], [{ text: '未标注', role: 'hint' }], [{ text: '空', role: 'storage' }]],
[[{ text: '*', role: 'plain' }, { text: 'args', role: 'variable' }], [{ text: '未标注', role: 'hint' }], []],
])
expect([...py1.values()].flat().filter(row => row.columns).map(row => row.label)).toEqual(['输入参数'])
const tsRows = await describeAll(ts, 'typescript', typescriptZhTranslator, 'function g(a: string, b = 2) {}')
const tsParams = tsRows.get(1)?.find(row => row.label === '输入参数')
expect(tsParams?.columns).toEqual(['名称', '类型', '默认值'])
expect(tsParams?.items?.map(item => item.cells.map(cell => text(cell)))).toEqual([['a', '字符串', ''], ['b', '未标注', '2']])
})
it('属性名是纯文字;值按词性着色,说明词(未标注、N 个)用 hint', async () => {
const env = await describeAll(ts, 'typescript', typescriptZhTranslator, readFixture('envLock.ts.txt'))
const rows = colors(env.get(18) ?? [])
expect(rows.map(row => row.label)).toEqual(['名称', '说明', '访问级别', '异步', '输入参数', '输出参数'])
@@ -547,7 +569,7 @@ describe('属性表', () => {
expect(colors(valueOf(sample.get(3), '来源'))).toEqual([{ text: 'os', role: 'type' }])
expect(colors(sample.get(39)?.find(row => row.label === '输入参数')?.items)).toEqual([
{ cells: [[{ text: 'self', role: 'variable' }], [{ text: '未标注', role: 'hint' }], []] },
{ cells: [[{ text: 'verbose', role: 'variable' }], [{ text: '布尔', role: 'type' }], [{ text: '默认 ', role: 'hint' }, { text: '假', role: 'storage' }]] },
{ cells: [[{ text: 'verbose', role: 'variable' }], [{ text: '布尔', role: 'type' }], [{ text: '假', role: 'storage' }]] },
])
})
@@ -677,7 +699,7 @@ describe('属性表', () => {
].join('\n')
expect(rowTexts(await describeAll(ts, 'typescript', typescriptZhTranslator, source))).toEqual({
1: ['名称: 匿名', '访问级别: 公共', '异步: 是', '输入参数: 无', '输出参数: 未标注'],
2: ['名称: f', '访问级别: 私有', '异步: 否', '输入参数: 2 个', ' b? | 数值 | ', ' c | 未标注 | 默认 1', '输出参数: 无'],
2: ['名称: f', '访问级别: 私有', '异步: 否', '输入参数: 2 个', ' b? | 数值 | ', ' c | 未标注 | 1', '输出参数: 无'],
3: ['名称: inner', '异步: 否', '输入参数: 无', '输出参数: 未标注'],
5: ['名称: K', '访问级别: 私有', '抽象: 否'],
6: ['名称: load', '访问级别: 受保护', '异步: 否', '装饰器: @memo()', '输入参数: 无', '输出参数: x is Foo'],
@@ -550,7 +550,10 @@ function aliased(item: Inline[], alias: Node | undefined, say: Phrases): Inline[
return [...item, { text: ' ', role: 'plain' }, { text: '作为', role: 'keyword' }, { text: ' ', role: 'plain' }, ...say.translated(alias)]
}
/** 函数:名称 / 说明 / 访问级别 / 异步(只有分这个的语言才有)/ 装饰器 / 输入参数(每个参数一行:名称、类型、默认值)/ 输出参数。 */
/**
* 函数:名称 / 说明 / 访问级别 / 异步(只有分这个的语言才有)/ 装饰器 / 输入参数 / 输出参数。
* 输入参数是一张表,列名是名称 / 类型 / 默认值,每个参数一行;没有默认值的格子留空。
*/
function functionRows(facts: FunctionFacts, say: Phrases): PropertyRow[] {
return [
{ label: '名称', value: facts.name ? say.name(facts.name, 'function') : [hint('匿名')] },
@@ -561,11 +564,12 @@ function functionRows(facts: FunctionFacts, say: Phrases): PropertyRow[] {
{
label: '输入参数',
value: count(facts.parameters.length),
columns: ['名称', '类型', '默认值'],
items: facts.parameters.map(param => ({
cells: [
param.optional ? [...say.translated(param.name), { text: '?', role: 'plain' }] : say.translated(param.name),
param.type ? say.translated(param.type) : [hint('未标注')],
param.default ? [hint('默认 '), ...say.translated(param.default)] : [],
param.default ? say.translated(param.default) : [],
],
...defining(param.name),
})),
@@ -4,13 +4,14 @@ import { ChevronDown, ChevronRight } from 'lucide-react'
import { Fragment } from 'react'
import { inlineText } from './segments'
import { SegmentText } from './SegmentText'
import { subRowsLayout, tableCells } from './subRows'
import { TextPreview } from './TextPreview'
import { propertyItemId, propertyRowId } from './viewState'
/** 子行里除最后一列以外各列的宽度(参数的名称、类型,字典的键),最后一列占满剩下的;编辑区窄时按比例收,不把最后一列挤没 */
/** 子行里除最后一列以外各列的宽度(参数表格的名称、类型列,字典的键),最后一列占满剩下的;编辑区窄时按比例收,不把最后一列挤没 */
const CELL_WIDTHS = ['min(150px, 28%)', 'min(220px, 38%)']
function columns(count: number): string {
function gridTemplate(count: number): string {
const fixed = Array.from({ length: count - 1 }, (_, i) => CELL_WIDTHS[i] ?? CELL_WIDTHS[CELL_WIDTHS.length - 1])
return [...fixed, 'minmax(0, 1fr)'].join(' ')
}
@@ -74,7 +75,7 @@ function PropertyItems({ parentId, items, folding }: { parentId: string, items:
<div
className="cr-prop-item grid gap-x-3"
data-id={id}
style={{ gridTemplateColumns: columns(item.cells.length) }}
style={{ gridTemplateColumns: gridTemplate(item.cells.length) }}
>
{item.cells.map((cell, i) => i === 0 && nested
? <ItemHead key={i} id={id} cell={cell} foldable={children.length > 0} folding={folding} />
@@ -93,7 +94,36 @@ function PropertyItems({ parentId, items, folding }: { parentId: string, items:
)
}
/** 函数、类、导入、常量、变量块的预览:属性名一列对齐,值按词性着色;输入参数、导入项、字典 / 对象的值下面展开子行。 */
/**
* 给了列名的子行(输入参数)排成表格:第一行是表头(列名),下面每个子行一行;外框圆角,格子之间、行之间一条分隔线。
* 列宽同列表的子行,最后一列占满剩下的,长了在格子里换行。
*/
function PropertyTable({ parentId, columns, items }: { parentId: string, columns: string[], items: PropertyItem[] }): ReactElement {
const template = { gridTemplateColumns: gridTemplate(columns.length) }
return (
<div className="pl-[5px]">
<div className="cr-table" role="table">
<div className="cr-table-head grid" role="row" style={template}>
{columns.map((name, i) => <span key={i} className="cr-table-cell" role="columnheader">{name}</span>)}
</div>
{items.map((item, index) => (
<div key={index} className="cr-table-row grid" role="row" data-id={propertyItemId(parentId, index)} style={template}>
{tableCells(item, columns).map((cell, i) => (
<p key={i} className="cr-table-cell cr-code min-w-0" role="cell">
<SegmentText segments={cell} />
</p>
))}
</div>
))}
</div>
</div>
)
}
/**
* 函数、类、导入、常量、变量块的预览:属性名一列对齐,值按词性着色;输入参数、导入项、字典 / 对象的值下面展开子行,
* 翻译插件给了列名的(输入参数)排成表格,没给的排成列表。
*/
export function PropertyGrid({ blockId, rows, collapsed, onToggleFold }: {
blockId: string
rows: PropertyRow[]
@@ -103,14 +133,14 @@ export function PropertyGrid({ blockId, rows, collapsed, onToggleFold }: {
return (
<div className="flex flex-col gap-[3px]">
{rows.map((row, index) => {
const items = row.items ?? []
const layout = subRowsLayout(row)
const id = propertyRowId(blockId, index)
const folded = collapsed.has(id)
const Chevron = folded ? ChevronRight : ChevronDown
return (
<Fragment key={index}>
<div className="cr-prop-row flex gap-3" data-id={id}>
{items.length > 0
{layout
? (
<button
type="button"
@@ -126,7 +156,8 @@ export function PropertyGrid({ blockId, rows, collapsed, onToggleFold }: {
: <span className="cr-prop-label flex h-5 w-[76px] shrink-0 items-center">{row.label}</span>}
<TextPreview text={row.value} className="min-w-0 flex-1" />
</div>
{items.length > 0 && !folded && <PropertyItems parentId={id} items={items} folding={{ collapsed, onToggleFold }} />}
{!folded && layout === 'table' && <PropertyTable parentId={id} columns={row.columns ?? []} items={row.items ?? []} />}
{!folded && layout === 'list' && <PropertyItems parentId={id} items={row.items ?? []} folding={{ collapsed, onToggleFold }} />}
</Fragment>
)
})}
@@ -119,6 +119,22 @@ body.vscode-high-contrast-light {
:is(.cr-block, .cr-prop-row, .cr-prop-item)[data-flash] {
animation: cr-flash 500ms ease-in-out 3;
}
/* 表格里的一行(参数):边框是和相邻行共用的分隔线,改用画在行里面的一圈聚焦色 */
@keyframes cr-flash-row {
50% {
background: color-mix(in srgb, var(--vscode-focusBorder) 25%, transparent);
box-shadow: inset 0 0 0 1px var(--vscode-focusBorder);
}
}
@keyframes cr-flash-row-still {
from, to {
background: color-mix(in srgb, var(--vscode-focusBorder) 25%, transparent);
box-shadow: inset 0 0 0 1px var(--vscode-focusBorder);
}
}
.cr-table-row[data-flash] {
animation: cr-flash-row 500ms ease-in-out 3;
}
@media (prefers-reduced-motion: reduce) {
.cr-block,
@@ -128,6 +144,9 @@ body.vscode-high-contrast-light {
:is(.cr-block, .cr-prop-row, .cr-prop-item)[data-flash] {
animation: cr-flash-still 1500ms;
}
.cr-table-row[data-flash] {
animation: cr-flash-row-still 1500ms;
}
}
.cr-badge {
@@ -310,6 +329,37 @@ body.vscode-high-contrast-light {
white-space: nowrap;
}
/*
* 属性表里带列名的子行(输入参数)排成的表格:外框圆角,格子之间、行之间一条主题的表格分隔线;
* 表头垫主题的表头底色(比编辑区深一档或浅一档)、列名用说明色
*/
.cr-table {
--cr-table-border: var(--vscode-tree-tableColumnsBorder, var(--vscode-editorWidget-border, var(--vscode-contrastBorder)));
border: 1px solid var(--cr-table-border);
border-radius: 4px;
overflow: hidden;
}
.cr-table-head {
background: var(--vscode-keybindingTable-headerBackground, color-mix(in srgb, var(--vscode-foreground) 5%, transparent));
}
.cr-table-row {
border-top: 1px solid var(--cr-table-border);
}
.cr-table .cr-table-cell {
padding: 2px 8px;
}
.cr-table .cr-table-cell + .cr-table-cell {
border-left: 1px solid var(--cr-table-border);
}
.cr-table-head .cr-table-cell {
font-size: 12px;
line-height: 20px;
color: var(--vscode-descriptionForeground);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
/* 编辑:透明输入框垫在底下接收输入和选区,着色层盖在上面只负责显示,高度由着色层撑开 */
.cr-editor {
position: relative;
@@ -0,0 +1,36 @@
import type { PropertyRow } from './types'
import { describe, expect, it } from 'vitest'
import { toBlockViews } from '../../../src/codeReader/blockView'
import { createPythonAdapter } from '../../../src/codeReader/languages/python'
import { WASM_DIR, withBlocks } from '../../../src/codeReader/testSupport'
import { pythonZhTranslator } from '../../../src/codeReader/translators/pythonZh'
import { subRowsLayout, tableCells } from './subRows'
const x = { text: 'x', role: 'variable' as const, offset: 4 }
describe('属性行下面的子行怎么排', () => {
it('给了列名的排成表格,没给的排成列表,没有子行的不展开', () => {
const items = [{ cells: [[x], []] }]
expect(subRowsLayout({ label: '输入参数', value: [], items, columns: ['名称', '类型', '默认值'] })).toBe('table')
expect(subRowsLayout({ label: '导入项', value: [], items })).toBe('list')
expect(subRowsLayout({ label: '输入参数', value: [], items: [], columns: ['名称', '类型', '默认值'] })).toBeUndefined()
expect(subRowsLayout({ label: '名称', value: [x] })).toBeUndefined()
})
it('表格的格子按列名的列数取:少给的留空,多给的不显示', () => {
expect(tableCells({ cells: [[x]] }, ['名称', '类型', '默认值'])).toEqual([[x], [], []])
expect(tableCells({ cells: [[x], [x], [x], [x]] }, ['名称', '类型'])).toEqual([[x], [x]])
})
it('真实的 Python 函数:输入参数排成表格,导入项、字典的各项排成列表', async () => {
const source = [
'from m import a',
'def f(k: int, v=1): pass',
'D = {"k": 1}',
].join('\n')
const py = createPythonAdapter(WASM_DIR)
const views = await withBlocks(py, 'python', source, blocks => toBlockViews(blocks, source, py, pythonZhTranslator))
const layouts = views.flatMap(view => 'properties' in view ? view.properties.flatMap((row: PropertyRow) => subRowsLayout(row) ? [[row.label, subRowsLayout(row)]] : []) : [])
expect(layouts).toEqual([['导入项', 'list'], ['输入参数', 'table'], ['值', 'list']])
})
})
@@ -0,0 +1,16 @@
import type { Inline, PropertyItem, PropertyRow } from './types'
/**
* 一行下面展开的子行怎么排:翻译插件给了列名的(输入参数)排成带表头和边框的表格,没给的(导入项、字典 / 对象的各项)排成列表;
* 没有子行就什么都不展开。
*/
export function subRowsLayout(row: PropertyRow): 'table' | 'list' | undefined {
if (!row.items?.length)
return undefined
return row.columns ? 'table' : 'list'
}
/** 表格里一个子行的格子:按列名的列数取,子行少给的格子留空、多给的不显示。 */
export function tableCells(item: PropertyItem, columns: string[]): Inline[][] {
return columns.map((_, i) => item.cells[i] ?? [])
}
+5 -1
View File
@@ -52,11 +52,15 @@ export interface PropertyItem extends Defines {
items?: PropertyItem[]
}
/** 属性表的一行:左列属性名,右列着色的值或 Markdown 说明;items 是展开在这一行下面的子行(输入参数、导入项、字典 / 对象的各项) */
/**
* 属性表的一行:左列属性名,右列着色的值或 Markdown 说明;items 是展开在这一行下面的子行(输入参数、导入项、字典 / 对象的各项)。
* columns 是子行各列的列名(输入参数的名称 / 类型 / 默认值):给了列名的子行排成带表头和边框的表格,没给的排成列表
*/
export interface PropertyRow extends Defines {
label: string
value: PreviewText
items?: PropertyItem[]
columns?: string[]
}
interface BlockViewBase {
@@ -60,7 +60,7 @@ export function useReveal(
return
}
// 块正在编辑时页面上没有属性表,就闪整块
const row = plan?.row === undefined ? null : document.querySelector<HTMLElement>(`:is(.cr-prop-row, .cr-prop-item)[data-id="${plan.row}"]`)
const row = plan?.row === undefined ? null : document.querySelector<HTMLElement>(`:is(.cr-prop-row, .cr-prop-item, .cr-table-row)[data-id="${plan.row}"]`)
const el = row ?? (plan && document.querySelector<HTMLElement>(`.cr-block[data-id="${plan.id}"]`))
if (el) {
const rect = el.getBoundingClientRect()