✨ feat(vscode): 代码阅读预览里跨几行写的列表按项拆开,列表里整项是调用的带着自己的实参表,一层层能往下套

This commit is contained in:
cruldra committed 2026-10-13 13:32:45 +11:00
1 parent 7d6e7592c9
commit 2c490cac2d
22 files changed
+848 -314

No files matched your search

@@ -244,7 +244,7 @@ function BlockPreview({ block, tree, marks }: { block: BlockView, tree: BlockTre
{marks?.translation ? <DiffText diff={marks.translation} /> : <TextPreview text={block.translation} />}
{block.table && (
<div className="mt-[3px]">
<PropertyTable parentId={tableId(block.id)} columns={block.table.columns} items={block.table.items} marks={marks?.table} />
<PropertyTable parentId={tableId(block.id)} columns={block.table.columns} items={block.table.items} marks={marks?.table} folding={{ collapsed: tree.collapsed, onToggleFold: tree.onToggleFold }} />
</div>
)}
{marks?.doc ? <DiffText diff={marks.doc} className="mt-0.5" /> : block.doc && <TextPreview text={block.doc} className="mt-0.5" />}
@@ -0,0 +1,125 @@
// @vitest-environment happy-dom
import type { Root } from 'react-dom/client'
import type { BlockKind, CallFacts } from '../../../src/codeReader/contract'
import type { PropertyRow } from './types'
import { act } from 'react'
import { createRoot } from 'react-dom/client'
import { afterEach, beforeEach, describe, expect, it, vi } 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 { PropertyGrid } from './PropertyGrid'
import { propertyRowId } from './viewState'
const py = createPythonAdapter(WASM_DIR)
let container: HTMLDivElement
let root: Root
beforeEach(() => {
vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true)
container = document.createElement('div')
document.body.append(container)
root = createRoot(container)
})
afterEach(async () => {
await act(async () => root.unmount())
container.remove()
vi.unstubAllGlobals()
})
/** Filter 的定义是类。 */
const calleeKind = (call: CallFacts): BlockKind | undefined => call.callee.text === 'Filter' ? 'class' : undefined
/** 一段 Python 里第一个变量块的属性表(经过宿主转成视图数据)。 */
async function variableRows(source: string): Promise<PropertyRow[]> {
const views = await withBlocks(py, 'python', source, blocks => toBlockViews(blocks, { source, language: py, markers: [], calleeKind }, pythonZhTranslator))
const variable = views.find(view => view.kind === 'variable')
if (!variable || !('properties' in variable))
throw new Error('没有变量块')
return variable.properties
}
async function render(rows: PropertyRow[], collapsed: ReadonlySet<string> = new Set(), onToggleFold: (id: string) => void = () => {}): Promise<void> {
await act(async () => root.render(<PropertyGrid blockId="0" rows={rows} collapsed={collapsed} onToggleFold={onToggleFold} />))
}
function byId(id: string): HTMLElement {
const found = container.querySelector<HTMLElement>(`[data-id="${id}"]`)
if (!found)
throw new Error(`页面上没有 ${id}`)
return found
}
/** a 在页面上排在 b 前面。 */
function before(a: Element, b: Element): boolean {
return (a.compareDocumentPosition(b) & Node.DOCUMENT_POSITION_FOLLOWING) !== 0
}
describe('拆开的列表', () => {
it('列表的项下面挂着它的实参表:表排在这一项下面、下一项上面,往右缩一层、左边一条竖线,带表头', async () => {
const rows = await variableRows([
'filters = [',
' Filter(field="user_id", value=current_user.id),',
' Filter(',
' field="archived_at",',
' value=None,',
' ),',
']',
].join('\n'))
await render(rows)
const row = propertyRowId('0', rows.findIndex(item => item.label === '初始值'))
const first = byId(`${row}/0`)
const second = byId(`${row}/1`)
expect(first.textContent).toBe('第 1 项新建 Filter · 2 个参数')
expect(second.textContent).toBe('第 2 项新建 Filter · 2 个参数')
const cell = byId(`${row}/0/t/0`)
expect(cell.textContent).toBe('field"user_id"')
expect(byId(`${row}/1/t/1`).textContent).toBe('value空')
expect(before(first, cell) && before(cell, second)).toBe(true)
const table = cell.closest('.cr-table')
expect([...table?.querySelectorAll('[role="columnheader"]') ?? []].map(head => head.textContent)).toEqual(['参数', '值'])
// 表不在项那一行里,在它下面那一层:和下一层子行一样左边一条竖线
expect(first.contains(table)).toBe(false)
let level = table?.parentElement
while (level && !level.firstElementChild?.classList.contains('cr-guide'))
level = level.parentElement
expect(level?.contains(cell)).toBe(true)
expect(level?.contains(first)).toBe(false)
})
it('实参表的格子里拆开的列表:排在那一格里、文字下面;项下面挂着自己的实参表,字典项的下一层能折叠', async () => {
const rows = await variableRows([
'result = session.query(',
' filters=[',
' Filter(field="a"),',
' {"k": v},',
' ],',
')',
].join('\n'))
const onToggleFold = vi.fn()
await render(rows, new Set(), onToggleFold)
const row = propertyRowId('0', rows.findIndex(item => item.label === '初始值'))
const argument = byId(`${row}/0`)
const cells = argument.querySelectorAll(':scope > [role="cell"]')
const valueCell = cells[cells.length - 1]
expect(valueCell.firstElementChild?.textContent).toBe('列表 2 项')
const call = byId(`${row}/0/0`)
const map = byId(`${row}/0/1`)
expect(valueCell.contains(call) && valueCell.contains(map)).toBe(true)
expect(call.textContent).toBe('第 1 项新建 Filter · 1 个参数')
expect(byId(`${row}/0/0/t/0`).textContent).toBe('field"a"')
expect(byId(`${row}/0/1/0`).textContent).toBe('"k"v')
const fold = map.querySelector('button')
expect(fold?.getAttribute('aria-expanded')).toBe('true')
await act(async () => fold?.click())
expect(onToggleFold).toHaveBeenCalledWith(`${row}/0/1`)
await render(rows, new Set([`${row}/0/1`]))
expect(container.querySelector(`[data-id="${row}/0/1/0"]`)).toBeNull()
expect(container.querySelector(`[data-id="${row}/0/0/t/0"]`)).not.toBeNull()
})
})
@@ -122,9 +122,20 @@ function ItemHead({ id, cell, foldable, folding }: { id: string, cell: PreviewTe
)
}
/** 往右缩进一层、左边一条竖线:一行或一个子行下面的一层子行,列表的一项下面挂的实参表 */
function Nested({ children }: { children: ReactNode }): ReactElement {
return (
<div className="flex gap-2.5 pl-[5px]">
<div className="cr-guide w-px shrink-0" />
<div className="flex min-w-0 flex-1 flex-col gap-0.5">{children}</div>
</div>
)
}
/**
* 展开在一行或一个子行(parentId)下面的一层子行:左边一条竖线,每个子行几列对齐。
* 带下一层的子行(字典 / 对象的值又是字典 / 对象),下一层展开在它下面、再往右缩进一层,默认展开。
* 带下一层的子行(字典 / 对象、跨几行写的列表的各项),下一层展开在它下面、再往右缩进一层,默认展开,能折叠;
* 带实参表的子行(列表里整项是调用),表排在它下面、同样往右缩进一层,总是展开。
* 标差异时(marks)新加的子行淡绿底、左边 +,删掉的放回原来的位置、淡红底划掉、左边 −,改了的格子逐截标。
*/
function PropertyItems({ parentId, items, folding, marks }: { parentId: string, items: PropertyItem[], folding: Folding, marks?: ItemsDiff }): ReactElement {
@@ -133,6 +144,17 @@ function PropertyItems({ parentId, items, folding, marks }: { parentId: string,
const itemRow = (item: PropertyItem, id: string, change: 'add' | 'del' | undefined, mark: ItemDiff | undefined): ReactElement => {
const children = item.items ?? []
const folds = change === 'del' ? FIXED : folding
const open = children.length > 0 && !folds.collapsed.has(id)
const below = (
<>
{open && <PropertyItems parentId={id} items={children} folding={folds} marks={mark?.items} />}
{item.table && (
<Nested>
<PropertyTable parentId={tableId(id)} columns={item.table.columns} items={item.table.items} marks={mark?.table} folding={folds} />
</Nested>
)}
</>
)
return (
<>
<div
@@ -145,49 +167,43 @@ function PropertyItems({ parentId, items, folding, marks }: { parentId: string,
? <ItemHead key={i} id={id} cell={cell} foldable={children.length > 0 && change !== 'del'} folding={folds} />
: <CellBox key={i} cell={cell} parts={mark?.cells?.[i]} />)}
</div>
{children.length > 0 && !folds.collapsed.has(id) && (change === 'del'
? (
<div className="cr-diff-gone">
<PropertyItems parentId={id} items={children} folding={folds} />
</div>
)
: <PropertyItems parentId={id} items={children} folding={folds} marks={mark?.items} />)}
{(open || item.table) && (change === 'del' ? <div className="cr-diff-gone">{below}</div> : below)}
</>
)
}
const removed = (after: number): ReactElement[] => removedAt(marks?.removed, after)
.map(({ value, index }) => <Fragment key={`removed-${index}`}>{itemRow(value, `${parentId}/x${index}`, 'del', undefined)}</Fragment>)
return (
<div className="flex gap-2.5 pl-[5px]">
<div className="cr-guide w-px shrink-0" />
<div className="flex min-w-0 flex-1 flex-col gap-0.5">
{removed(-1)}
{items.map((item, index) => {
const mark = marks?.changed[index]
return (
<Fragment key={index}>
{itemRow(item, propertyItemId(parentId, index), mark?.added && 'add', mark)}
{removed(index)}
</Fragment>
)
})}
</div>
</div>
<Nested>
{removed(-1)}
{items.map((item, index) => {
const mark = marks?.changed[index]
return (
<Fragment key={index}>
{itemRow(item, propertyItemId(parentId, index), mark?.added && 'add', mark)}
{removed(index)}
</Fragment>
)
})}
</Nested>
)
}
/**
* 给了列名的子行(输入参数、数据表的字段)排成表格:第一行是表头(列名),下面每个子行一行;外框圆角,格子之间、行之间一条分隔线。
* 列宽按整列的内容定(见 tableTemplate),最后一列分剩下的,长了在格子里换行。一行译文下面摊开实参的表也用它;
* 行带 table(实参的值里跨几行写的调用)时,那张表排在最后一格里、文字下面,往右缩一点,一层层往下套。
* 行带 items(实参的值是跨几行写的列表)时各项排在最后一格里、文字下面,同属性行下面的子行(项下面的下一层按 folding 折叠);
* 行带 table(实参的值里跨几行写的调用)时那张表排在它们后面,往右缩一点。一层层往下套。
* 标差异时(marks)有加行、删行的表每行第一格前多一个记号位(+ / −,没变的空着),加的行淡绿底,删的行放回原来的位置、淡红底划掉。
* 讲某个子块的行(toggles,Rust 实现块的方法表)第一格是按钮,点它在表下面展开那一块,展开时这一行淡淡垫上那一块的种类色。
*/
export function PropertyTable({ parentId, columns, items, marks, toggles }: {
export function PropertyTable({ parentId, columns, items, marks, folding, toggles }: {
parentId: string
columns: string[]
items: PropertyItem[]
marks?: ItemsDiff
/** 格子里拆开的子行下面的下一层眼下折叠着没有 */
folding: Folding
/** 按子行下标:讲它的子块(点第一格展开,见 GridSlots) */
toggles?: ReadonlyMap<number, ItemToggle>
}): ReactElement {
@@ -196,6 +212,7 @@ export function PropertyTable({ parentId, columns, items, marks, toggles }: {
/** 一行:删掉的(change 是 del)不带 id,套着的表不再标;toggle 是这一行讲的子块,第一格成了展开它的按钮 */
const tableRow = (item: PropertyItem, id: string, change: 'add' | 'del' | undefined, mark: ItemDiff | undefined, toggle?: ItemToggle): ReactElement => {
const cells = tableCells(item, columns)
const listed = (item.items?.length ?? 0) > 0
const sign = (i: number): ReactNode => signed && i === 0 && <span className="cr-diff-sign" data-change={change} aria-hidden>{SIGNS[change ?? 'none']}</span>
/** 一格:讲子块的那一格是展开它的按钮(只放着色的词),别的照常(见 CellBox) */
const cellBox = (cell: PreviewText, i: number, className?: string): ReactElement => toggle && i === 0
@@ -213,11 +230,12 @@ export function PropertyTable({ parentId, columns, items, marks, toggles }: {
: {}
return (
<div className="cr-table-row" role="row" data-id={change === 'del' ? undefined : id} data-diff={change} data-open={toggle?.open ? '' : undefined} {...tint}>
{cells.map((cell, i) => item.table && i === cells.length - 1
{cells.map((cell, i) => (listed || item.table) && i === cells.length - 1
? (
<div key={i} className="cr-table-cell flex min-w-0 flex-col gap-[3px]" role="cell">
<CellBox cell={cell} parts={mark?.cells?.[i]} lead={sign(i)} />
<PropertyTable parentId={tableId(id)} columns={item.table.columns} items={item.table.items} marks={mark?.table} />
{listed && <PropertyItems parentId={id} items={item.items ?? []} folding={change === 'del' ? FIXED : folding} marks={mark?.items} />}
{item.table && <PropertyTable parentId={tableId(id)} columns={item.table.columns} items={item.table.items} marks={mark?.table} folding={change === 'del' ? FIXED : folding} />}
</div>
)
: <Fragment key={i}>{cellBox(cell, i, 'cr-table-cell')}</Fragment>)}
@@ -262,6 +280,7 @@ export function PropertyGrid({ blockId, rows, collapsed, onToggleFold, marks, sl
/** 挂在这张属性表子行上的子块(库规则的卡片),见 GridSlots */
slots?: GridSlots
}): ReactElement {
const folding: Folding = { collapsed, onToggleFold }
const removed = (after: number): ReactElement[] => removedAt(marks?.removed, after)
.map(({ value, index }) => <RemovedRow key={`removed-${index}`} id={`${blockId}/x${index}`} row={value} />)
return (
@@ -298,8 +317,8 @@ export function PropertyGrid({ blockId, rows, collapsed, onToggleFold, marks, sl
: <span className="cr-prop-label flex h-5 items-center">{row.label}</span>}
<RowValue row={row} mark={mark} />
</div>
{!folded && layout === 'table' && <PropertyTable parentId={id} columns={row.columns ?? []} items={row.items ?? []} marks={mark?.items} toggles={toggles} />}
{!folded && layout === 'list' && <PropertyItems parentId={id} items={row.items ?? []} folding={{ collapsed, onToggleFold }} marks={mark?.items} />}
{!folded && layout === 'table' && <PropertyTable parentId={id} columns={row.columns ?? []} items={row.items ?? []} marks={mark?.items} folding={folding} toggles={toggles} />}
{!folded && layout === 'list' && <PropertyItems parentId={id} items={row.items ?? []} folding={folding} marks={mark?.items} />}
{!folded && slots && shownBlocks.length > 0 && <div className="cr-slot">{slots.render(shownBlocks)}</div>}
{removed(index)}
</Fragment>
@@ -337,7 +356,7 @@ function RemovedRow({ id, row }: { id: string, row: PropertyRow }): ReactElement
{layout && (
<div className="cr-diff-gone">
{layout === 'table'
? <PropertyTable parentId={id} columns={row.columns ?? []} items={row.items ?? []} />
? <PropertyTable parentId={id} columns={row.columns ?? []} items={row.items ?? []} folding={FIXED} />
: <PropertyItems parentId={id} items={row.items ?? []} folding={FIXED} />}
</div>
)}
@@ -116,6 +116,24 @@ describe('跳到定义:滚到哪块、展开哪些', () => {
expect(occurrencePlan(blocks, { start: 70, end: 83, kind: 'read' }, id => id === '0')).toEqual({ id: '1', expand: ['1/p0'] })
})
it('名字在拆开的列表里:列表项下面的实参表总是展开的;实参表格子里拆开的列表总是展开的,它的项下面的子行要展开那一项', () => {
const filter = (at: number): PropertyItem => ({ cells: [[word('第 1 项')], [word('新建'), name('Filter', at)]], table: { columns: ['参数', '值'], items: [{ cells: [[name('field', at + 10)], [name('user_id', at + 20)]] }] } })
const map = (at: number): PropertyItem => ({ cells: [[word('第 2 项')], [word('字典 1 项')]], items: [{ cells: [[name('key', at)], [name('value', at + 5)]], defines: { startIndex: at, endIndex: at + 3 } }] })
const blocks: BlockView[] = [
factBlock('0', 'variable', 0, [{ label: '初始值', value: [word('列表 2 项')], items: [filter(10), map(40)] }]),
{ ...textBlock('1', 'statement', 5, [word('调用'), name('query', 100)]), table: { columns: ['参数', '值'], items: [{ cells: [[name('filters', 110)], [word('列表 2 项')]], items: [filter(120), map(160)] }] } },
factBlock('2', 'variable', 9, [{ label: '初始值', value: [word('调用'), name('query', 200)], columns: ['参数', '值'], items: [{ cells: [[name('filters', 210)], [word('列表 2 项')]], items: [filter(220), map(260)] }] }]),
]
expect(revealPlan(blocks, 0, 30)).toEqual({ id: '0', expand: ['0/p0'] })
expect(revealPlan(blocks, 0, 40)).toEqual({ id: '0', row: '0/p0/1/0', expand: ['0/p0', '0/p0/1'] })
expect(revealPlan(blocks, 5, 140)).toEqual({ id: '1', expand: [] })
expect(revealPlan(blocks, 5, 165)).toEqual({ id: '1', expand: ['1/t/0/1'] })
expect(revealPlan(blocks, 9, 240)).toEqual({ id: '2', expand: ['2/p0'] })
expect(revealPlan(blocks, 9, 260)).toEqual({ id: '2', row: '2/p0/0/1/0', expand: ['2/p0', '2/p0/0/1'] })
expect(occurrencePlan(blocks, { start: 165, end: 170, kind: 'read' }, () => false)).toEqual({ id: '1', expand: ['1/t/0/1'] })
expect(occurrencePlan(blocks, { start: 140, end: 147, kind: 'read' }, () => false)).toEqual({ id: '1', expand: [] })
})
it('名字在框里(套了几层都算)也是显示着它', () => {
const inner = { style: 'iteration' as const, label: '列表', segments: [word('遍历'), name('row', 30), word('取'), name('y', 40)] }
const blocks: BlockView[] = [
+47 -62
View File
@@ -1,7 +1,7 @@
import type { BlockView, Inline, Occurrence, PreviewTable, PreviewText, PropertyItem, SourceRange } from './types'
import type { BlockView, Inline, Occurrence, PreviewTable, PreviewText, PropertyItem, PropertyRow, SourceRange } from './types'
import { blockPath } from './filter'
import { showsOccurrence, sourceOf } from './occurrences'
import { propertyItemId, propertyRowId, unfoldToShow } from './viewState'
import { propertyItemId, propertyRowId, tableId, unfoldToShow } from './viewState'
/** 跳到定义时要滚到的块,就是这处定义的属性行 / 子行(只闪它),以及要展开的块、属性行和子行:它们折叠着,目标就不在页面上。 */
export interface RevealPlan {
@@ -62,24 +62,34 @@ function width(range: SourceRange): number {
return range.endIndex - range.startIndex
}
/** 实参表(一行译文下面那张、值那一格里套着的,都总是展开的)里有格子盖住 offset 时,那段有多长;套着的表一层层都算。 */
function tableSpan(table: PreviewTable | undefined, offset: number): number | undefined {
for (const item of table?.items ?? []) {
for (const cell of item.cells) {
const span = textSpan(cell, offset)
if (span !== undefined)
return span
}
const nested = tableSpan(item.table, offset)
if (nested !== undefined)
return nested
}
return undefined
/** 页面上的一个子行:它的 id,和要让它露出来得展开的属性行、子行(从外往里)。 */
interface PlacedItem {
item: PropertyItem
id: string
expand: string[]
}
/** 实参表(一行译文下面那张、值那一格里套着的,都总是展开的)里有格子满足 test,套着的表一层层都算。 */
function tableShows(table: PreviewTable | undefined, test: (cell: PreviewText) => boolean): boolean {
return table?.items.some(item => item.cells.some(test) || tableShows(item.table, test)) ?? false
/**
* parentId 下面一层层的子行,按页面上的先后(子行、它下一层的子行、它的实参表);expand 是让这一层露出来得展开的。
* 排成列表的子行(listed)下面的下一层折叠在它自己的 id 上;表格的行格子里拆开的子行、实参表(id 见 tableId)总是展开的。
*/
function* nestedItems(items: PropertyItem[] | undefined, parentId: string, expand: string[], listed: boolean): Generator<PlacedItem> {
for (const [index, item] of (items ?? []).entries()) {
const id = propertyItemId(parentId, index)
yield { item, id, expand }
yield* nestedItems(item.items, id, listed ? [...expand, id] : expand, true)
yield* nestedItems(item.table?.items, tableId(id), expand, false)
}
}
/** 属性行下面一层层的子行:子行折叠在属性行上,给了列名的是表格的行。 */
function rowItems(row: PropertyRow, id: string): Generator<PlacedItem> {
return nestedItems(row.items, id, [id], !row.columns)
}
/** 一行译文下面那张实参表(总是展开的)里一层层的子行。 */
function tableItems(blockId: string, table: PreviewTable | undefined): Generator<PlacedItem> {
return nestedItems(table?.items, tableId(blockId), [], false)
}
/**
@@ -94,71 +104,46 @@ function locate(block: BlockView, offset: number): Spot | undefined {
if (block.defines && covers(block.defines, offset))
return { expand: [], span: width(block.defines) }
if ('translation' in block) {
const span = textSpan(block.translation, offset) ?? tableSpan(block.table, offset)
return span === undefined ? undefined : { expand: [], span }
const span = textSpan(block.translation, offset)
return span === undefined ? shownItem(tableItems(block.id, block.table), offset) : { expand: [], span }
}
const rows = block.properties.map((row, index) => ({ row, id: propertyRowId(block.id, index) }))
for (const { row, id } of rows) {
if (row.defines && covers(row.defines, offset))
return { row: id, expand: [], span: width(row.defines) }
const item = definingItem(row.items, id, offset)
if (item)
return item
for (const { item, id: itemId, expand } of rowItems(row, id)) {
if (item.defines && covers(item.defines, offset))
return { row: itemId, expand, span: width(item.defines) }
}
}
for (const { row, id } of rows) {
const value = textSpan(row.value, offset)
if (value !== undefined)
return { expand: [], span: value }
const shown = shownItem(row.items, id, offset)
const shown = shownItem(rowItems(row, id), offset)
if (shown)
return shown
}
return undefined
}
/** parentId 下面一层层的子行里,defines 盖住 offset 的那个:闪它,展开它上面的各层(parentId 和它所在的那串上层子行)。 */
function definingItem(items: PropertyItem[] | undefined, parentId: string, offset: number): Spot | undefined {
for (const [index, item] of (items ?? []).entries()) {
const id = propertyItemId(parentId, index)
if (item.defines && covers(item.defines, offset))
return { row: id, expand: [parentId], span: width(item.defines) }
const inner = definingItem(item.items, id, offset)
if (inner)
return { ...inner, expand: [parentId, ...inner.expand] }
}
return undefined
}
/** parentId 下面一层层的子行里,格子(连值那一格里套着的实参表)盖住 offset 的:展开它上面的各层,span 是那段有多长。 */
function shownItem(items: PropertyItem[] | undefined, parentId: string, offset: number): Spot | undefined {
for (const [index, item] of (items ?? []).entries()) {
let span: number | undefined
/** 一层层的子行里,格子盖住 offset 的第一个:要展开的各层,span 是那段有多长。 */
function shownItem(items: Iterable<PlacedItem>, offset: number): Spot | undefined {
for (const { item, expand } of items) {
for (const cell of item.cells) {
span = textSpan(cell, offset)
const span = textSpan(cell, offset)
if (span !== undefined)
break
return { expand, span }
}
span ??= tableSpan(item.table, offset)
if (span !== undefined)
return { expand: [parentId], span }
const inner = shownItem(item.items, propertyItemId(parentId, index), offset)
if (inner)
return { ...inner, expand: [parentId, ...inner.expand] }
}
return undefined
}
/**
* parentId 下面一层层的子行里,哪个的格子(连它值那一格里套着的实参表)满足 test(显示着要找的名字):
* 要展开的各层(parentId 到那个子行所在的那一层)。
*/
function showingItem(items: PropertyItem[] | undefined, parentId: string, test: (cell: PreviewText) => boolean): string[] | undefined {
for (const [index, item] of (items ?? []).entries()) {
if (item.cells.some(test) || tableShows(item.table, test))
return [parentId]
const inner = showingItem(item.items, propertyItemId(parentId, index), test)
if (inner)
return [parentId, ...inner]
/** 一层层的子行里,格子满足 test(显示着要找的名字)的第一个:要展开的各层。 */
function showingItem(items: Iterable<PlacedItem>, test: (cell: PreviewText) => boolean): string[] | undefined {
for (const { item, expand } of items) {
if (item.cells.some(test))
return expand
}
return undefined
}
@@ -204,11 +189,11 @@ export function uncoveredPlan(plan: RevealPlan, covered: ReadonlyMap<string, str
/** 块里哪儿显示着满足 test 的文字(译文和它下面的表、属性行的值、各层子行的格子):要展开的属性行和子行;不在这块里是 undefined。 */
function showingSpot(block: BlockView, test: (text: PreviewText) => boolean): string[] | undefined {
if ('translation' in block)
return test(block.translation) || tableShows(block.table, test) ? [] : undefined
return test(block.translation) ? [] : showingItem(tableItems(block.id, block.table), test)
for (const [index, row] of block.properties.entries()) {
if (test(row.value))
return []
const expand = showingItem(row.items, propertyRowId(block.id, index), test)
const expand = showingItem(rowItems(row, propertyRowId(block.id, index)), test)
if (expand)
return expand
}
+8 -5
View File
@@ -70,18 +70,21 @@ export interface SourceRange { startIndex: number, endIndex: number }
*/
export interface Defines { defines?: SourceRange }
/** 属性表里展开的一个子行:几列对齐的格子,例如参数的名称 / 类型 / 默认值、字典一项的键 / 值;格子是着色的词或 Markdown(参数的说明、异常的触发条件) */
/** 属性表里展开的一个子行:几列对齐的格子,例如参数的名称 / 类型 / 默认值、字典一项的键 / 值、列表的第 N 项 / 值;格子是着色的词或 Markdown(参数的说明、异常的触发条件) */
export interface PropertyItem extends Defines {
cells: PreviewText[]
/** 展开在这个子行下面的下一层子行(字典 / 对象的值又是字典 / 对象时,它的各项),一层层可以一直往下套 */
/**
* 最后一格的值拆开的下一层子行(字典 / 对象、跨几行写的列表的各项):排成列表的子行展开在它下面、能折叠,
* 表格的行排在最后一格里、文字下面、总是展开;一层层可以一直往下套
*/
items?: PropertyItem[]
/** 排在这一行最后一格里、那格文字下面的表(实参的值里跨几行写的调用的实参表),表里的行又可以带 table;总是展开 */
/** 最后一格的值里那次调用的实参表:表格的行排在最后一格里、文字下面,排成列表的子行排在它下面;总是展开,表里的行又可以带 items、table */
table?: PreviewTable
}
/**
* 属性表的一行:左列属性名,右列着色的值或 Markdown 说明;items 是展开在这一行下面的子行(输入参数、导入项、字典 / 对象的各项)。
* columns 是子行各列的列名(输入参数的名称 / 类型 / 默认值):给了列名的子行排成带表头和边框的表格,没给的排成列表
* 属性表的一行:左列属性名,右列着色的值或 Markdown 说明;items 是展开在这一行下面的子行(输入参数、导入项、字典 / 对象、列表的各项)。
* columns 是子行各列的列名(输入参数的名称 / 类型 / 默认值):给了列名的子行排成带表头和边框的表格,没给的排成列表;两种子行都能再带下一层
*/
export interface PropertyRow extends Defines {
label: string