✨ feat(vscode): 代码阅读的输入参数排成带表头和边框的表格
This commit is contained in:
1 parent
6416cdf714
commit
83adefdf5e
10 files changed
+197
-24
No files matched your search
@@ -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] ?? [])
|
||||
}
|
||||
@@ -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()
|
||||
|
||||
Reference in new issue
Block a user