✨ feat(vscode): 代码阅读页面画链式调用卡片:竖轨道、小链、收起的闭包和合成的表

链的各步排在值下面的竖轨道上,方法一列对齐、轨道线是函数色淡到 35%;实参是链的缩进成空心点小链,注释挂在步骤下;收起的闭包是能点的小框,点开那一步才画挂着的子块(折叠 id 记在属性行下面);跳到定义、查找、高度估算、只看改动都算上链里的字和步骤下的子块
This commit is contained in:
cruldra committed 2026-10-15 21:05:07 +11:00
1 parent ee595f3da9
commit 1956879aa1
13 files changed
+531 -47

No files matched your search

@@ -1,14 +1,15 @@
import type { CSSProperties, ReactElement, ReactNode } from 'react'
import type { BlockView, ItemDiff, ItemsDiff, KindBadge, PreviewText, PropertyItem, PropertyRow, Removed, RowDiff, RowsDiff, TextDiff } from './types'
import type { BlockView, ChainSlot, ChainStep, Inline, ItemDiff, ItemsDiff, KindBadge, PreviewText, PropertyItem, PropertyRow, Removed, RowDiff, RowsDiff, TextDiff } from './types'
import { ChevronDown, ChevronRight } from 'lucide-react'
import { Fragment } from 'react'
import { cn } from '@/lib/utils'
import { ChainFoldContext } from './chainFold'
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, textKey } from './viewState'
import { chainStepId, propertyItemId, propertyRowId, tableId, textKey } from './viewState'
/** 列表子行里除最后一列以外各列的宽度(字典的键),最后一列占满剩下的;编辑区窄时按比例收,不把最后一列挤没 */
const CELL_WIDTHS = ['min(150px, 28%)', 'min(220px, 38%)']
@@ -28,6 +29,17 @@ export interface GridSlots {
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
@@ -36,7 +48,7 @@ interface ItemToggle {
}
/** 眼下折叠着哪些属性行、子行,点折叠箭头时切换哪个。 */
interface Folding {
export interface Folding {
collapsed: ReadonlySet<string>
onToggleFold: (id: string) => void
}
@@ -274,7 +286,7 @@ export function PropertyTable({ parentId, columns, items, marks, folding, toggle
* 给了列名的(输入参数、数据表的字段)排成表格,没给的排成列表。
* 标差异时(marks)新加的行淡绿底、左边 +,删掉的行放回原来的位置、淡红底划掉、左边 −,值改了的按旧值 → 新值或逐词标。
*/
export function PropertyGrid({ blockId, rows, collapsed, onToggleFold, marks, slots }: {
export function PropertyGrid({ blockId, rows, collapsed, onToggleFold, marks, slots, chain }: {
blockId: string
rows: PropertyRow[]
collapsed: ReadonlySet<string>
@@ -283,6 +295,8 @@ export function PropertyGrid({ blockId, rows, collapsed, onToggleFold, marks, sl
marks?: RowsDiff
/** 挂在这张属性表子行上的子块(库规则的卡片),见 GridSlots */
slots?: GridSlots
/** 挂在某一行链式调用某一步下面的子块,见 ChainSlots */
chain?: ChainSlots
}): ReactElement {
const folding: Folding = { collapsed, onToggleFold }
const removed = (after: number): ReactElement[] => removedAt(marks?.removed, after)
@@ -320,7 +334,14 @@ export function PropertyGrid({ blockId, rows, collapsed, onToggleFold, marks, sl
</button>
)
: <span className="cr-prop-label flex h-5 items-center" data-find={textKey(id, 'label')}>{row.label}</span>}
<RowValue row={row} mark={mark} id={id} />
{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} />}
</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} />}
@@ -357,7 +378,10 @@ function RemovedRow({ id, row }: { id: string, row: PropertyRow }): ReactElement
<>
<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>
<RowValue row={row} mark={undefined} />
<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">
@@ -369,3 +393,85 @@ function RemovedRow({ id, row }: { id: string, row: PropertyRow }): ReactElement
</>
)
}
/** 一列着色的词大约占几个等宽字宽:汉字这类全角字算两个。 */
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>
)
}