Files
superwork/vscode/webview-ui/src/codeReader/PropertyGrid.tsx
T
cruldra 86beaac371 ✨ feat(vscode): React 钩子卡片按变量讲,属性表加操作列、调用树能查所有引用
- 钩子调用讲成状态、引用、记忆值、记忆函数、上下文值、过渡、副作用、钩子返回值、钩子调用几种卡片,行照签名表 reactHooks.ts 排,去掉 react.hook
- 解构接住时名称一行写出解构,展开是「第几项 | 名称 | 说明」表,每项一个调用树操作;单个名字的按钮在名称一行最右边
- 属性表的行、子行可以带操作(RowAction),表格多一列「操作」,宿主按块记号加序号起键
- 调用树服务按键数次数,新增查所有引用(references):按所在函数分组,去掉定义处和依赖数组里的
- 钩子收的函数参数都收成小框,函数体是卡片子块,直接返回的表达式由 functionBody 排成「返回 …」卡片;小框和卡片折叠箭头同一个开关
- 规则卡片可指定语言种类图标(languageKind)、种类图标悬停字和颜色(KindBadge.tooltip / iconColor)
2026-10-16 22:16:09 +11:00

523 lines
27 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 { CSSProperties, ReactElement, ReactNode } from 'react'
import type { BlockView, ChainSlot, ChainStep, Inline, ItemDiff, ItemsDiff, KindBadge, PreviewText, PropertyItem, PropertyRow, Removed, RowAction, RowDiff, RowsDiff, TextDiff } from './types'
import { ChevronDown, ChevronRight } from 'lucide-react'
import { Fragment, useContext } from 'react'
import { cn } from '@/lib/utils'
import { CallTreeButton } from './BlockTools'
import { ChainFoldContext } from './chainFold'
import { DiffInline, DiffText } from './DiffText'
import { RowActionsContext } from './rowActions'
import { inlineText } from './segments'
import { SegmentText } from './SegmentText'
import { subRowsLayout, tableCells, tableTemplate } from './subRows'
import { TextPreview } from './TextPreview'
import { chainStepId, propertyItemId, propertyRowId, tableId, textKey } 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(' ')
}
/**
* 库规则挂在卡片子行上的子块(BlockSlot):blocks 是卡片的这些子块,render 把一行下面展开着的几块画成一层块(同块树),
* onToggle 是点子行第一格时展开 / 收起讲它的那一块。
*/
export interface GridSlots {
blocks: BlockView[]
render: (blocks: BlockView[]) => ReactNode
onToggle: (block: BlockView) => void
}
/**
* 挂在卡片链式调用某一步下面的子块(ChainSlot):blocks 是卡片的这些子块,render 把一步下面的几块画成一层块(同块树),
* open 是那一步眼下展开着没有,onToggle 是点那一步收起来的闭包时展开 / 收起它。
*/
export interface ChainSlots {
blocks: BlockView[]
render: (blocks: BlockView[]) => ReactNode
open: (slot: ChainSlot) => boolean
onToggle: (slot: ChainSlot) => void
}
/** 表格里一个子行讲的子块:展开着没有,它的种类(展开时这一行淡淡垫上它的颜色),点第一格时怎么切换。 */
interface ItemToggle {
open: boolean
badge?: KindBadge
onToggle: () => void
}
/** 眼下折叠着哪些属性行、子行,点折叠箭头时切换哪个。 */
export 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 }] : [])
}
/** 一格是不是成块排的 Markdown 说明(参数的说明、异常的触发条件):它不能放进 .cr-code 的段落里。 */
function isBlock(cell: PreviewText, parts: TextDiff | undefined): boolean {
return 'markdown' in cell || (parts !== undefined && 'markdown' in parts)
}
/** 一格的字:着色的词照常,改了的逐截标(DiffInline);Markdown 说明照常排版,改了的逐词标(DiffText)。 */
function CellText({ cell, parts }: { cell: PreviewText, parts: TextDiff | undefined }): ReactElement {
if (parts)
return 'inline' in parts ? <DiffInline parts={parts.inline} /> : <DiffText diff={parts} className="min-w-0" />
return 'markdown' in cell ? <TextPreview text={cell} className="min-w-0" /> : <SegmentText segments={cell} />
}
/** 一格连外面那层:着色的词排在 .cr-code 的段落里,Markdown 说明是块,lead(差异记号、展开按钮)排在它左边。`find` 是这一格的记号(见 [textKey]),记在外面那层上。 */
function CellBox({ cell, parts, className, lead, find, ...attrs }: { cell: PreviewText, parts: TextDiff | undefined, className?: string, lead?: ReactNode, find?: string, role?: string }): ReactElement {
if (isBlock(cell, parts)) {
return (
<div className={cn('flex min-w-0 items-start gap-1', className)} data-find={find} {...attrs}>
{lead}
<CellText cell={cell} parts={parts} />
</div>
)
}
return (
<p className={cn('cr-code min-w-0', className)} data-find={find} {...attrs}>
{lead}
<CellText cell={cell} parts={parts} />
</p>
)
}
/** 格子写成一串字(读屏念的展开 / 折叠按钮名):着色的词拼起来,Markdown 取原文。 */
function cellLabel(cell: PreviewText): string {
return 'markdown' in cell ? cell.markdown : inlineText(cell)
}
/**
* 子行的第一列(字典的键):带下一层的子行前面是折叠箭头(图标、大小、颜色同属性行的),点箭头或这一列切换;
* 同一层里不带下一层的子行在同样的位置留空,第一列才对得齐。
*/
function ItemHead({ id, cell, foldable, folding, find }: { id: string, cell: PreviewText, foldable: boolean, folding: Folding, find?: string }): ReactElement {
if (!foldable) {
return (
<div className="flex min-w-0 gap-1">
<span className="w-3.5 shrink-0" />
<CellBox cell={cell} parts={undefined} find={find} />
</div>
)
}
const folded = folding.collapsed.has(id)
const Chevron = folded ? ChevronRight : ChevronDown
const toggle = (): void => folding.onToggleFold(id)
// 只有箭头是按钮:这一列里还可能有别的按钮(渲染大纲的「样式 N 个类」),按钮里不能套按钮;点这一列的字照样切换
return (
<div className="group flex min-w-0 items-start gap-1">
<button
type="button"
className="flex h-5 w-3.5 shrink-0 items-center rounded-sm"
aria-label={`${folded ? '展开' : '折叠'}${cellLabel(cell)}`}
aria-expanded={!folded}
onClick={toggle}
>
<Chevron size={14} strokeWidth={1.8} className="text-[var(--vscode-descriptionForeground)] group-hover:text-[var(--vscode-foreground)]" />
</button>
<div className="min-w-0 cursor-pointer" onClick={toggle}>
<CellBox cell={cell} parts={undefined} find={find} />
</div>
</div>
)
}
/**
* 一行能做的操作([RowAction]),按钮从左往右排:现在都是看调用树([CallTreeButton]),悬停写操作自己的说法,次数、开着没有照卡片给的(见 [RowActionsContext])。
* 卡片没给(定义卡片浮窗、删掉的块的旧内容)、操作没有键时不画。
*/
function RowActionButtons({ actions }: { actions: readonly RowAction[] | undefined }): ReactElement | null {
const state = useContext(RowActionsContext)
const keyed = state ? (actions ?? []).flatMap(action => action.key === undefined ? [] : [{ action, key: action.key }]) : []
if (!state || keyed.length === 0)
return null
return (
<span className="cr-row-actions" role="group" aria-label="操作">
{keyed.map(({ action, key }) => (
<CallTreeButton key={key} callKey={key} label={action.label} count={state.counts.get(key) ?? 0} open={state.openKey === key} onOpen={() => state.onOpen(key)} />
))}
</span>
)
}
/** 往右缩进一层、左边一条竖线:一行或一个子行下面的一层子行,列表的一项下面挂的实参表 */
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)新加的子行淡绿底、左边 +,删掉的放回原来的位置、淡红底划掉、左边 −,改了的格子逐截标。
*/
export 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
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
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) => {
const find = change === 'del' ? undefined : textKey(id, String(i))
return i === 0 && nested
? <ItemHead key={i} id={id} cell={cell} foldable={children.length > 0 && change !== 'del'} folding={folds} find={find} />
: <CellBox key={i} cell={cell} parts={mark?.cells?.[i]} find={find} />
})}
</div>
{(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 (
<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),最后一列分剩下的,长了在格子里换行。一行译文下面摊开实参的表也用它;
* 行带 items(实参的值是跨几行写的列表)时各项排在最后一格里、文字下面,同属性行下面的子行(项下面的下一层按 folding 折叠);
* 行带 table(实参的值里跨几行写的调用)时那张表排在它们后面,往右缩一点。一层层往下套。
* 标差异时(marks)有加行、删行的表每行第一格前多一个记号位(+ / −,没变的空着),加的行淡绿底,删的行放回原来的位置、淡红底划掉。
* 讲某个子块的行(toggles,Rust 实现块的方法表)第一格是按钮,点它在表下面展开那一块,展开时这一行淡淡垫上那一块的种类色。
* 有行带操作([PropertyItem.actions])、卡片又给了操作的状态([RowActionsContext])时,最后多一列「操作」,宽度按按钮最多的那一行,
* 各行的操作按钮在这一格里从左往右排,没有操作的行空着。
*/
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 {
// 有加行、删行的表每行第一格前留一个记号位
const signed = marks !== undefined && (marks.removed.length > 0 || Object.values(marks.changed).some(mark => mark.added))
const operable = useContext(RowActionsContext) !== undefined && items.some(item => (item.actions?.length ?? 0) > 0)
/** 一行:删掉的(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>
const find = (i: number): string | undefined => change === 'del' ? undefined : textKey(id, String(i))
/** 一格:讲子块的那一格是展开它的按钮(只放着色的词),别的照常(见 CellBox) */
const cellBox = (cell: PreviewText, i: number, className?: string): ReactElement => toggle && i === 0
? (
<p className={cn('cr-code min-w-0', className)} role="cell" data-find={find(i)}>
{sign(i)}
<button type="button" className="cr-item-toggle" aria-expanded={toggle.open} aria-label={`${toggle.open ? '收起' : '展开'}${cellLabel(cell)}`} onClick={toggle.onToggle}>
<CellText cell={cell} parts={mark?.cells?.[i]} />
</button>
</p>
)
: <CellBox cell={cell} parts={mark?.cells?.[i]} className={className} lead={sign(i)} find={find(i)} role="cell" />
const tint = toggle?.open && toggle.badge
? { 'data-badge': '', 'style': { '--kind-dark': toggle.badge.color.dark, '--kind-light': toggle.badge.color.light } as CSSProperties }
: {}
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) => (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)} find={find(i)} />
{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>)}
{operable && (
<div className="cr-table-cell cr-table-ops" role="cell">
{change !== 'del' && <RowActionButtons actions={item.actions} />}
</div>
)}
</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: operable ? `${tableTemplate(columns.length)} max-content` : tableTemplate(columns.length) }}>
<div className="cr-table-head" role="row">
{columns.map((name, i) => <span key={i} className="cr-table-cell" role="columnheader" data-find={textKey(parentId, `h${i}`)}>{name}</span>)}
{operable && <span className="cr-table-cell" role="columnheader">操作</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, toggles?.get(index))}
{removed(index)}
</Fragment>
)
})}
</div>
</div>
)
}
/**
* 函数、类、枚举、导入、常量、变量块和库规则的卡片的预览:属性名一列对齐,值按词性着色;输入参数、导入项、字典 / 对象的值下面展开子行,
* 给了列名的(输入参数、数据表的字段)排成表格,没给的排成列表。一行带操作([PropertyRow.actions],React 卡片只接一个名字时的「名称」一行)时,
* 按钮排在这一行最右边(见 [RowActionButtons])。
* 标差异时(marks)新加的行淡绿底、左边 +,删掉的行放回原来的位置、淡红底划掉、左边 −,值改了的按旧值 → 新值或逐词标。
*/
export function PropertyGrid({ blockId, rows, collapsed, onToggleFold, marks, slots, chain }: {
blockId: string
rows: PropertyRow[]
collapsed: ReadonlySet<string>
onToggleFold: (id: string) => void
/** 标差异时这张属性表改了哪些行 */
marks?: RowsDiff
/** 挂在这张属性表子行上的子块(库规则的卡片),见 GridSlots */
slots?: GridSlots
/** 挂在某一行链式调用某一步下面的子块,见 ChainSlots */
chain?: ChainSlots
}): 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 (
<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]
const placed = slots?.blocks.filter(block => block.slot?.row === index) ?? []
// 挂在子行上的子块只画展开着的
const shownBlocks = placed.filter(block => !collapsed.has(block.id))
const toggles = new Map(placed.flatMap((block): [number, ItemToggle][] => block.slot
? [[block.slot.item, { open: !collapsed.has(block.id), badge: block.badge, onToggle: () => slots?.onToggle(block) }]]
: []))
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}
data-find={textKey(id, 'label')}
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" data-find={textKey(id, 'label')}>{row.label}</span>}
<WithActions actions={row.actions}>
{row.chain
? (
<div className="min-w-0">
<RowValue row={row} mark={mark} id={id} />
<ChainRail blockId={blockId} row={index} steps={row.chain} folding={folding} slots={chain} />
</div>
)
: <RowValue row={row} mark={mark} id={id} />}
</WithActions>
</div>
{!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>
)
})}
</div>
)
}
/** 属性行的值,这一行带操作、卡片又给了操作的状态时,操作按钮排在值的最右边;不然只有值。 */
function WithActions({ actions, children }: { actions: readonly RowAction[] | undefined, children: ReactElement }): ReactElement {
const state = useContext(RowActionsContext)
if (!state || !actions?.some(action => action.key !== undefined))
return children
return (
<div className="flex min-w-0 items-start gap-2">
<div className="min-w-0 flex-1">{children}</div>
<RowActionButtons actions={actions} />
</div>
)
}
/**
* 一行的值:着色的词或 Markdown(改了的逐截 / 逐词标);跟着说明(PropertyRow.note,输出参数的 `@return`)时说明接在值后面,
* 同一行放得下就并排,剩的地方不到 12 个字宽时换到下一行。`id` 是这一行的 id,值和说明按它记上记号(见 [textKey]);删掉的行没有。
*/
function RowValue({ row, mark, id }: { row: PropertyRow, mark: RowDiff | undefined, id?: string }): ReactElement {
const find = (part: string): string | undefined => id === undefined ? undefined : textKey(id, part)
const value = mark?.value ? <DiffText diff={mark.value} className="min-w-0" find={find('value')} /> : <TextPreview text={row.value} className="min-w-0" find={find('value')} />
if (!row.note)
return value
return (
<div className="flex min-w-0 flex-wrap items-baseline gap-x-2">
{value}
{mark?.note ? <DiffText diff={mark.note} className="min-w-0 flex-[1_1_12em]" find={find('note')} /> : <TextPreview text={row.note} className="min-w-0 flex-[1_1_12em]" find={find('note')} />}
</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>
<div className="min-w-0">
<RowValue row={row} mark={undefined} />
{row.chain && <ChainRail blockId={id} row={0} steps={row.chain} folding={FIXED} />}
</div>
</div>
{layout && (
<div className="cr-diff-gone">
{layout === 'table'
? <PropertyTable parentId={id} columns={row.columns ?? []} items={row.items ?? []} folding={FIXED} />
: <PropertyItems parentId={id} items={row.items ?? []} folding={FIXED} />}
</div>
)}
</>
)
}
/** 一列着色的词大约占几个等宽字宽:汉字这类全角字算两个。 */
function textWidth(pieces: Inline[]): number {
let width = 0
for (const char of inlineText(pieces))
width += /[\u2E80-\uFFEF]/.test(char) ? 2 : 1
return width
}
/**
* 链式调用的各步排成的竖轨道,接在属性行的值(链的起点)下面:一步一个实心点,点与点之间一条函数色的线;方法那一列按最长的那个对齐,
* 值那一列照抄实参,收起来的闭包是能点的小框,点开时这一步挂着的子块(slots)画在这一步下面。
* 一步下面依次是小链(空心点,一节一行)、几行属性(写在上面的注释,属性名在方法那一列)、表(合成的一步各次的实参)、展开着的子块。
* 删掉的行里画的旧链没有 slots,闭包的小框不能点。
*
* ![链式调用卡片](docs/prototype/code-reader/openpencil/exports/rust-chain-a.png)
*/
export function ChainRail({ blockId, row, steps, folding, slots }: {
blockId: string
/** 链在属性表的第几行 */
row: number
steps: ChainStep[]
folding: Folding
slots?: ChainSlots
}): ReactElement {
const width = Math.max(4, ...steps.map(step => textWidth(step.name)))
return (
<div className="cr-chain" style={{ '--cr-chain-name': `${width}ch` } as CSSProperties}>
{steps.map((step, index) => {
const slot = { row, step: index }
const id = chainStepId(blockId, slot)
const blocks = slots?.blocks.filter(block => block.step?.row === row && block.step.step === index) ?? []
const fold = slots && blocks.length > 0 ? { open: slots.open(slot), onToggle: () => slots.onToggle(slot) } : undefined
return (
<div key={index} className="cr-chain-step" data-id={id}>
<ChainDot first={index === 0} last={index === steps.length - 1} />
<div className="cr-chain-body">
<ChainFoldContext value={fold}>
<div className="cr-chain-line">
<p className="cr-code cr-chain-name" data-find={textKey(id, 'name')}><SegmentText segments={step.name} /></p>
<p className="cr-code min-w-0" data-find={textKey(id, 'value')}><SegmentText segments={step.value} /></p>
</div>
{step.lines && (
<div className="cr-chain-line">
<span />
<div className="cr-chain-mini">
{step.lines.map((line, at) => (
<div key={at} className="cr-chain-step">
<ChainDot first={at === 0} last={at === (step.lines?.length ?? 0) - 1} hollow />
<p className="cr-code min-w-0" data-find={textKey(id, `l${at}`)}><SegmentText segments={line} /></p>
</div>
))}
</div>
</div>
)}
</ChainFoldContext>
{step.rows?.map((item, at) => (
<div key={at} className="cr-chain-line">
<span className="cr-chain-label" data-find={textKey(id, `r${at}-label`)}>{item.label}</span>
<TextPreview text={item.value} className="min-w-0" find={textKey(id, `r${at}`)} />
</div>
))}
{step.table && <PropertyTable parentId={tableId(id)} columns={step.table.columns} items={step.table.items} folding={folding} />}
{fold?.open && <div className="cr-chain-blocks">{slots?.render(blocks)}</div>}
</div>
</div>
)
})}
</div>
)
}
/** 轨道上的一个点,上下各一截线连着前后两步(第一步上面、最后一步下面不画);小链的点是空心的。 */
function ChainDot({ first, last, hollow = false }: { first: boolean, last: boolean, hollow?: boolean }): ReactElement {
return (
<span className="cr-chain-rail" data-first={first ? '' : undefined} data-last={last ? '' : undefined}>
<span className="cr-chain-up" />
<span className="cr-chain-dot" data-hollow={hollow ? '' : undefined} />
<span className="cr-chain-down" />
</span>
)
}