✨ feat(vscode): 代码阅读页面画链式调用卡片:竖轨道、小链、收起的闭包和合成的表
链的各步排在值下面的竖轨道上,方法一列对齐、轨道线是函数色淡到 35%;实参是链的缩进成空心点小链,注释挂在步骤下;收起的闭包是能点的小框,点开那一步才画挂着的子块(折叠 id 记在属性行下面);跳到定义、查找、高度估算、只看改动都算上链里的字和步骤下的子块
This commit is contained in:
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,闭包的小框不能点。
|
||||
*
|
||||
* 
|
||||
*/
|
||||
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>
|
||||
)
|
||||
}
|
||||
Reference in new issue
Block a user