Files
superwork/vscode/webview-ui/src/codeReader/PropertyGrid.tsx
T

261 lines
13 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import type { ReactElement } from 'react'
import type { Inline, InlinePart, ItemDiff, ItemsDiff, PropertyItem, PropertyRow, Removed, RowsDiff } from './types'
import { ChevronDown, ChevronRight } from 'lucide-react'
import { Fragment } from 'react'
import { DiffInline, DiffText } from './DiffText'
import { inlineText } from './segments'
import { SegmentText } from './SegmentText'
import { subRowsLayout, tableCells, tableTemplate } from './subRows'
import { TextPreview } from './TextPreview'
import { propertyItemId, propertyRowId, tableId } from './viewState'
/** 列表子行里除最后一列以外各列的宽度(字典的键),最后一列占满剩下的;编辑区窄时按比例收,不把最后一列挤没 */
const CELL_WIDTHS = ['min(150px, 28%)', 'min(220px, 38%)']
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(' ')
}
/** 眼下折叠着哪些属性行、子行,点折叠箭头时切换哪个。 */
interface Folding {
collapsed: ReadonlySet<string>
onToggleFold: (id: string) => void
}
/** 删掉的行下面的子行:总是展开,点了也不收(旧内容只是看看)。 */
const FIXED: Folding = { collapsed: new Set(), onToggleFold: () => {} }
/** 表格行第一格前的记号:加的 +、删的 −,没变的空一格,列照样对齐。 */
const SIGNS = { add: '+', del: '−', none: ' ' } as const
/** 删掉的行(子行)里排在新的第 after 行后面的那些,带着它在删掉的行里的序号(页面上认它用)。 */
function removedAt<T>(removed: readonly Removed<T>[] | undefined, after: number): { value: T, index: number }[] {
return (removed ?? []).flatMap((entry, index) => entry.after === after ? [{ value: entry.value, index }] : [])
}
/** 一格的字:这一格的值改了就逐截标(DiffInline),否则照常。 */
function CellText({ cell, parts }: { cell: Inline[], parts: InlinePart[] | undefined }): ReactElement {
return parts ? <DiffInline parts={parts} /> : <SegmentText segments={cell} />
}
/**
* 子行的第一列(字典的键):带下一层的子行前面是折叠箭头(图标、大小、颜色同属性行的),点箭头或这一列切换;
* 同一层里不带下一层的子行在同样的位置留空,第一列才对得齐。
*/
function ItemHead({ id, cell, foldable, folding }: { id: string, cell: Inline[], foldable: boolean, folding: Folding }): ReactElement {
if (!foldable) {
return (
<div className="flex min-w-0 gap-1">
<span className="w-3.5 shrink-0" />
<p className="cr-code min-w-0">
<SegmentText segments={cell} />
</p>
</div>
)
}
const folded = folding.collapsed.has(id)
const Chevron = folded ? ChevronRight : ChevronDown
return (
<button
type="button"
className="group flex min-w-0 items-start gap-1 rounded-sm text-left"
aria-label={`${folded ? '展开' : '折叠'}${inlineText(cell)}`}
aria-expanded={!folded}
onClick={() => folding.onToggleFold(id)}
>
<span className="flex h-5 w-3.5 shrink-0 items-center">
<Chevron size={14} strokeWidth={1.8} className="text-[var(--vscode-descriptionForeground)] group-hover:text-[var(--vscode-foreground)]" />
</span>
<span className="cr-code min-w-0">
<SegmentText segments={cell} />
</span>
</button>
)
}
/**
* 展开在一行或一个子行(parentId)下面的一层子行:左边一条竖线,每个子行几列对齐。
* 带下一层的子行(字典 / 对象的值又是字典 / 对象),下一层展开在它下面、再往右缩进一层,默认展开。
* 标差异时(marks)新加的子行淡绿底、左边 +,删掉的放回原来的位置、淡红底划掉、左边 −,改了的格子逐截标。
*/
function PropertyItems({ parentId, items, folding, marks }: { parentId: string, items: PropertyItem[], folding: Folding, marks?: ItemsDiff }): ReactElement {
const nested = items.some(item => (item.items?.length ?? 0) > 0)
/** 一个子行:删掉的(change 是 del)不带 id、下面的子行总是展开 */
const itemRow = (item: PropertyItem, id: string, change: 'add' | 'del' | undefined, mark: ItemDiff | undefined): ReactElement => {
const children = item.items ?? []
const folds = change === 'del' ? FIXED : folding
return (
<>
<div
className="cr-prop-item grid gap-x-3"
data-id={change === 'del' ? undefined : id}
data-diff={change}
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 && change !== 'del'} folding={folds} />
: (
<p key={i} className="cr-code min-w-0">
<CellText cell={cell} parts={mark?.cells?.[i]} />
</p>
))}
</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} />)}
</>
)
}
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>
)
}
/**
* 给了列名的子行(输入参数、数据表的字段)排成表格:第一行是表头(列名),下面每个子行一行;外框圆角,格子之间、行之间一条分隔线。
* 列宽按整列的内容定(见 tableTemplate),最后一列分剩下的,长了在格子里换行。一行译文下面摊开实参的表也用它;
* 行带 table(实参的值里跨几行写的调用)时,那张表排在最后一格里、文字下面,往右缩一点,一层层往下套。
* 标差异时(marks)有加行、删行的表每行第一格前多一个记号位(+ / −,没变的空着),加的行淡绿底,删的行放回原来的位置、淡红底划掉。
*/
export function PropertyTable({ parentId, columns, items, marks }: { parentId: string, columns: string[], items: PropertyItem[], marks?: ItemsDiff }): ReactElement {
// 有加行、删行的表每行第一格前留一个记号位
const signed = marks !== undefined && (marks.removed.length > 0 || Object.values(marks.changed).some(mark => mark.added))
/** 一行:删掉的(change 是 del)不带 id,套着的表不再标 */
const tableRow = (item: PropertyItem, id: string, change: 'add' | 'del' | undefined, mark: ItemDiff | undefined): ReactElement => {
const cells = tableCells(item, columns)
const text = (cell: Inline[], i: number): ReactElement => (
<>
{signed && i === 0 && <span className="cr-diff-sign" data-change={change} aria-hidden>{SIGNS[change ?? 'none']}</span>}
<CellText cell={cell} parts={mark?.cells?.[i]} />
</>
)
return (
<div className="cr-table-row" role="row" data-id={change === 'del' ? undefined : id} data-diff={change}>
{cells.map((cell, i) => item.table && i === cells.length - 1
? (
<div key={i} className="cr-table-cell flex min-w-0 flex-col gap-[3px]" role="cell">
<p className="cr-code">{text(cell, i)}</p>
<PropertyTable parentId={tableId(id)} columns={item.table.columns} items={item.table.items} marks={mark?.table} />
</div>
)
: <p key={i} className="cr-table-cell cr-code min-w-0" role="cell">{text(cell, i)}</p>)}
</div>
)
}
const removed = (after: number): ReactElement[] => removedAt(marks?.removed, after)
.map(({ value, index }) => <Fragment key={`removed-${index}`}>{tableRow(value, `${parentId}/x${index}`, 'del', undefined)}</Fragment>)
return (
<div className="pl-[5px]">
<div className="cr-table" role="table" style={{ gridTemplateColumns: tableTemplate(columns.length) }}>
<div className="cr-table-head" role="row">
{columns.map((name, i) => <span key={i} className="cr-table-cell" role="columnheader">{name}</span>)}
</div>
{removed(-1)}
{items.map((item, index) => {
const mark = marks?.changed[index]
return (
<Fragment key={index}>
{tableRow(item, propertyItemId(parentId, index), mark?.added && 'add', mark)}
{removed(index)}
</Fragment>
)
})}
</div>
</div>
)
}
/**
* 函数、类、枚举、导入、常量、变量块和库规则的卡片的预览:属性名一列对齐,值按词性着色;输入参数、导入项、字典 / 对象的值下面展开子行,
* 给了列名的(输入参数、数据表的字段)排成表格,没给的排成列表。
* 标差异时(marks)新加的行淡绿底、左边 +,删掉的行放回原来的位置、淡红底划掉、左边 −,值改了的按旧值 → 新值或逐词标。
*/
export function PropertyGrid({ blockId, rows, collapsed, onToggleFold, marks }: {
blockId: string
rows: PropertyRow[]
collapsed: ReadonlySet<string>
onToggleFold: (id: string) => void
/** 标差异时这张属性表改了哪些行 */
marks?: RowsDiff
}): ReactElement {
const removed = (after: number): ReactElement[] => removedAt(marks?.removed, after)
.map(({ value, index }) => <RemovedRow key={`removed-${index}`} id={`${blockId}/x${index}`} row={value} />)
return (
<div className="grid grid-cols-[minmax(76px,max-content)_minmax(0,1fr)] gap-x-3 gap-y-[3px] *:col-span-full">
{removed(-1)}
{rows.map((row, index) => {
const layout = subRowsLayout(row)
const id = propertyRowId(blockId, index)
const folded = collapsed.has(id)
const Chevron = folded ? ChevronRight : ChevronDown
const mark = marks?.changed[index]
return (
<Fragment key={index}>
<div className="cr-prop-row grid grid-cols-subgrid" data-id={id} data-diff={mark?.added && 'add'}>
{layout
? (
<button
type="button"
className="cr-prop-label group flex h-5 items-center gap-1 rounded-sm"
aria-label={`${folded ? '展开' : '折叠'}${row.label}`}
aria-expanded={!folded}
onClick={() => onToggleFold(id)}
>
<Chevron size={14} strokeWidth={1.8} className="shrink-0 text-[var(--vscode-descriptionForeground)] group-hover:text-[var(--vscode-foreground)]" />
{row.label}
</button>
)
: <span className="cr-prop-label flex h-5 items-center">{row.label}</span>}
{mark?.value ? <DiffText diff={mark.value} className="min-w-0" /> : <TextPreview text={row.value} className="min-w-0" />}
</div>
{!folded && layout === 'table' && <PropertyTable parentId={id} columns={row.columns ?? []} items={row.items ?? []} marks={mark?.items} />}
{!folded && layout === 'list' && <PropertyItems parentId={id} items={row.items ?? []} folding={{ collapsed, onToggleFold }} marks={mark?.items} />}
{removed(index)}
</Fragment>
)
})}
</div>
)
}
/** 属性表里删掉的一行:整行淡红底、划掉,左边 −;它下面的子行照样摊开、划掉。 */
function RemovedRow({ id, row }: { id: string, row: PropertyRow }): ReactElement {
const layout = subRowsLayout(row)
return (
<>
<div className="cr-prop-row grid grid-cols-subgrid" data-diff="del">
<span className="cr-prop-label flex h-5 items-center">{row.label}</span>
<TextPreview text={row.value} className="min-w-0" />
</div>
{layout && (
<div className="cr-diff-gone">
{layout === 'table'
? <PropertyTable parentId={id} columns={row.columns ?? []} items={row.items ?? []} />
: <PropertyItems parentId={id} items={row.items ?? []} folding={FIXED} />}
</div>
)}
</>
)
}