♻️ refactor(vscode): 卡片里的子块只留挂在子行上、点了才展开的一种,测试假数据换掉旧 React 卡片的名字

This commit is contained in:
cruldra committed 2026-10-08 07:17:25 +08:00
1 parent f6bc217e91
commit 5e994cd90e
16 files changed
+121 -130

No files matched your search

@@ -12,7 +12,7 @@ vi.mock('./useOccurrences', async () => {
})
vi.mock('./vscode', () => ({ postMessage: () => {}, onMessage: () => () => {}, viewState: { get: () => ({ editing: [], folds: {} }), update: () => {} } }))
const BADGE: KindBadge = { label: 'React 组件', color: { dark: '#61dafb', light: '#0a7ea4' }, icon: 'atom' }
const BADGE: KindBadge = { label: '卡片', color: { dark: '#61dafb', light: '#0a7ea4' }, icon: 'box' }
function view(id: string, kind: string, text: string, extra: Partial<BlockView> = {}): BlockView {
const badge = extra.badge
@@ -32,23 +32,21 @@ function view(id: string, kind: string, text: string, extra: Partial<BlockView>
} as BlockView
}
/** 一张组件卡片:第 1 行下面放着一个副作用,第 2 行的两个子行各讲一个处理函数,后面还有一个留下的注释块。 */
/** 一张卡片:第 1 行的两个子行各挂一张子卡片,后面还有一个留下的注释块。 */
const CARD: BlockView = {
...view('0', 'react.component', ''),
...view('0', 'demo.card', ''),
badge: BADGE,
properties: [
{ label: '名称', value: [{ text: 'Page', role: 'function' }] },
{ label: '副作用', value: [{ text: '1 个', role: 'hint' }] },
{ label: '事件处理', value: [{ text: '2 个', role: 'hint' }], columns: ['名称', '绑定在', '做什么'], items: [
{ cells: [[{ text: 'handleA', role: 'function' }], [], []] },
{ cells: [[{ text: 'handleB', role: 'function' }], [], []] },
{ label: '名称', value: [{ text: 'Server', role: 'type' }] },
{ label: '方法', value: [{ text: '2 个', role: 'hint' }], columns: ['名称', '参数', '返回'], items: [
{ cells: [[{ text: 'start', role: 'function' }], [], []] },
{ cells: [[{ text: 'stop', role: 'function' }], [], []] },
] },
],
children: [
view('0.0', 'react.effect', '滚回顶部', { slot: { row: 1 }, badge: { ...BADGE, label: '副作用', icon: 'zap' } }),
view('0.1', 'react.handler', '处理 A', { slot: { row: 2, item: 0 }, badge: { ...BADGE, label: '事件处理', icon: 'mouse-pointer-click' } }),
view('0.2', 'react.handler', '处理 B', { slot: { row: 2, item: 1 }, badge: { ...BADGE, label: '事件处理', icon: 'mouse-pointer-click' } }),
view('0.3', 'comment', '留下的注释'),
view('0.0', 'demo.method', '启动服务', { slot: { row: 1, item: 0 }, badge: { ...BADGE, label: '方法', icon: 'square-function' } }),
view('0.1', 'demo.method', '停止服务', { slot: { row: 1, item: 1 }, badge: { ...BADGE, label: '方法', icon: 'square-function' } }),
view('0.2', 'comment', '留下的注释'),
],
}
@@ -75,28 +73,28 @@ function render(folds: Record<string, boolean>): string {
return renderToStaticMarkup(<BlockLevel blocks={[CARD]} tree={tree} />)
}
describe('放进卡片属性表里的子块', () => {
it('副作用画在它那一行下面;点子行才展开的处理函数默认不画,名称是展开它的按钮;卡片收着时留下的子块不画', () => {
describe('挂在卡片子行上的子块', () => {
it('默认不画,子行的名称是展开它的按钮;卡片收着时留下的子块不画', () => {
const html = render({})
expect(html).toContain('滚回顶部')
expect(html).not.toContain('处理 A')
expect(html).not.toContain('启动服务')
expect(html).not.toContain('停止服务')
expect(html).not.toContain('留下的注释')
expect(html).toContain('<button type="button" class="cr-item-toggle" aria-expanded="false" aria-label="展开handleA">')
// 副作用画在「副作用」那一行后面、「事件处理」那一行前面
expect(html.indexOf('滚回顶部')).toBeGreaterThan(html.indexOf('副作用</button>'))
expect(html.indexOf('滚回顶部')).toBeLessThan(html.indexOf('事件处理</button>'))
expect(html).toContain('<button type="button" class="cr-item-toggle" aria-expanded="false" aria-label="展开start">')
})
it('展开一个处理函数:画在表下面,那一行标成展开、垫上它的种类色,卡片左上角的箭头收起它', () => {
const html = render({ 0.1: false, 0: false })
expect(html).toContain('处理 A')
expect(html).not.toContain('处理 B')
it('展开一张:画在表下面,那一行标成展开、垫上它的种类色,卡片左上角的箭头收起它', () => {
const html = render({ '0.0': false, '0': false })
expect(html).toContain('启动服务')
expect(html).not.toContain('停止服务')
expect(html).toContain('data-open="" data-badge="" style="--kind-dark:#61dafb;--kind-light:#0a7ea4"')
expect(html).toContain('aria-label="收起"')
expect(html).toContain('留下的注释')
// 子卡片画在「方法」那一行后面
expect(html.indexOf('启动服务')).toBeGreaterThan(html.indexOf('方法</button>'))
})
it('折叠放着子块的那一行:它下面的子块跟着收起', () => {
expect(render({ '0/p1': true })).not.toContain('滚回顶部')
it('折叠挂着子块的那一行:展开着的子块跟着收起', () => {
expect(render({ '0.0': false })).toContain('启动服务')
expect(render({ '0.0': false, '0/p1': true })).not.toContain('启动服务')
})
})
@@ -67,7 +67,7 @@ export interface QaTree {
/**
* 块下面有没有画出来的子块(删掉的子块也算):子块都被筛掉、被盖住时不留空的一层、不给折叠箭头。
* 放进卡片属性表里的子块(BlockSlot)画在卡片里,不算。
* 挂在卡片子行上的子块(BlockSlot)画在卡片属性表里,不算。
*/
function hasShownChildren(block: BlockView, tree: BlockTreeState): boolean {
return block.children.some(child => !child.slot && shown(child, tree)) || (!tree.covered.has(block.id) && tree.diff?.removed.has(block.id) === true)
@@ -125,7 +125,7 @@ function kindProps(block: BlockView | undefined): { 'data-kind'?: string, 'data-
* slotted:这一层是画在卡片属性表某一行下面的子块(BlockSlot),blocks 只有那几块。
*/
export function BlockLevel({ blocks, owner, tree, slotted = false }: { blocks: BlockView[], owner?: BlockView, tree: BlockTreeState, slotted?: boolean }): ReactElement {
// 放进卡片属性表里的子块画在卡片里(slotted 的那一层),这一层不画;删掉的块都放回卡片后面这一层
// 挂在卡片子行上的子块画在卡片属性表里(slotted 的那一层),这一层不画;删掉的块都放回卡片后面这一层
const listed = slotted ? blocks : blocks.filter(block => !block.slot)
const removed = slotted ? [] : tree.diff?.removed.get(owner?.id ?? '') ?? []
const listedIds = new Set(listed.map(block => block.id))
@@ -212,8 +212,8 @@ function BlockPreview({ block, tree, marks }: { block: BlockView, tree: BlockTre
}
/**
* 卡片属性表里放的子块(BlockSlot)怎么画:一行下面的几块画成一层块(同块树);点子行才展开的,点那个子行的第一格展开它、
* 同一行里别的收起,展开着的再点收起。卡片没有放进属性表的子块时没有。
* 挂在卡片子行上的子块(BlockSlot)怎么画:一行下面展开着的几块画成一层块(同块树);点那个子行的第一格展开它、
* 同一行里别的收起,展开着的再点收起。卡片没有挂在子行上的子块时没有。
*/
function cardSlots(block: BlockView, tree: BlockTreeState): GridSlots | undefined {
const blocks = block.children.filter(child => child.slot)
@@ -227,7 +227,7 @@ function cardSlots(block: BlockView, tree: BlockTreeState): GridSlots | undefine
tree.onSetFolds({ [child.id]: true })
return
}
const others = blocks.filter(other => other !== child && other.slot?.row === child.slot?.row && other.slot?.item !== undefined)
const others = blocks.filter(other => other !== child && other.slot?.row === child.slot?.row)
tree.onSetFolds({ ...Object.fromEntries(others.map(other => [other.id, true])), [child.id]: false })
},
}
@@ -238,7 +238,7 @@ function BlockCard({ block, tree }: { block: BlockView, tree: BlockTreeState }):
const editing = tree.editing.has(block.id)
const collapsed = tree.collapsed.has(block.id)
// 点父卡片子行才展开的子块:画出来就是展开着的,箭头收起整张
const toggled = block.slot?.item !== undefined
const toggled = block.slot !== undefined
const hasChildren = toggled || hasShownChildren(block, tree)
const Chevron = collapsed && !toggled ? ChevronRight : ChevronDown
const change = tree.diff?.changes.get(block.id)
@@ -18,8 +18,8 @@ function gridTemplate(count: number): string {
}
/**
* 库规则放进卡片属性表里的子块(BlockSlot):blocks 是卡片的这些子块,render 把一行下面的几块画成一层块(同块树),
* onToggle 是点子行第一格时展开 / 收起讲它的那一块(点子行才展开的,见 BlockSlot.item)。
* 库规则挂在卡片子行上的子块(BlockSlot):blocks 是卡片的这些子块,render 把一行下面展开着的几块画成一层块(同块树),
* onToggle 是点子行第一格时展开 / 收起讲它的那一块。
*/
export interface GridSlots {
blocks: BlockView[]
@@ -234,7 +234,7 @@ export function PropertyGrid({ blockId, rows, collapsed, onToggleFold, marks, sl
onToggleFold: (id: string) => void
/** 标差异时这张属性表改了哪些行 */
marks?: RowsDiff
/** 放进这张属性表里的子块(库规则的卡片),见 GridSlots */
/** 挂在这张属性表子行上的子块(库规则的卡片),见 GridSlots */
slots?: GridSlots
}): ReactElement {
const removed = (after: number): ReactElement[] => removedAt(marks?.removed, after)
@@ -249,15 +249,15 @@ export function PropertyGrid({ blockId, rows, collapsed, onToggleFold, marks, sl
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 => block.slot?.item === undefined || !collapsed.has(block.id))
const toggles = new Map(placed.flatMap((block): [number, ItemToggle][] => block.slot?.item === undefined
? []
: [[block.slot.item, { open: !collapsed.has(block.id), badge: block.badge, onToggle: () => slots?.onToggle(block) }]]))
// 挂在子行上的子块只画展开着的
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 || placed.length > 0
{layout
? (
<button
type="button"
@@ -512,7 +512,7 @@ body:is(.vscode-light, .vscode-high-contrast-light) .cr-group[data-colored] { --
text-decoration: underline;
}
/* 放进卡片属性表某一行下面的子块(副作用、展开的处理函数):和表格一样往里缩一点 */
/* 在卡片属性表某一行下面展开的子块(Rust 实现块展开的方法):和表格一样往里缩一点 */
.cr-slot {
padding-left: 5px;
}
@@ -115,31 +115,29 @@ describe('foldUnchanged', () => {
})
})
/** 一张卡片:两个副作用放在第 7 行下面,两个处理函数放在第 8 行的子行上,后面一个留下的子块;只有第二个处理函数改了 */
/** 一张卡片:两个子块挂在第 8 行的子行上,后面一个留下的子块;只有第二个子块改了,它里面挂在第 2 行子行上的子块自己改了 */
const CARD_BLOCKS = [
block('0', 'card', [
block('0.0', 'e1', [], { slot: { row: 7 } }),
block('0.1', 'e2', [], { slot: { row: 7 } }),
block('0.2', 'h1', [], { slot: { row: 8, item: 0 } }),
block('0.3', 'h2', [block('0.3.0', 'h2s', [], { slot: { row: 2 } })], { slot: { row: 8, item: 1 }, startLine: 4 }),
block('0.4', 'rest'),
block('0.0', 'm1', [], { slot: { row: 8, item: 0 } }),
block('0.1', 'm2', [block('0.1.0', 'm2s', [], { slot: { row: 2, item: 0 } })], { slot: { row: 8, item: 1 }, startLine: 4 }),
block('0.2', 'rest'),
]),
]
function cardTree(): NonNullable<ReturnType<typeof diffTree>> {
return diffTree(CARD_BLOCKS, diff({ blocks: { card: { change: 'mod' }, h2: { change: 'mod' }, h2s: { change: 'mod', own: true } } }), 2, () => true)!
return diffTree(CARD_BLOCKS, diff({ blocks: { card: { change: 'mod' }, m2: { change: 'mod' }, m2s: { change: 'mod', own: true } } }), 2, () => true)!
}
describe('放进卡片属性表里的子块', () => {
it('只看改动:画在卡片里的按所在的行分开折,点子行才展开的不折、改了的照样往里看', () => {
describe('挂在卡片子行上的子块', () => {
it('只看改动:挂在子行上的不折、改了的照样往里看,卡片后面留下的照常折', () => {
const { runs, folded } = foldUnchanged(CARD_BLOCKS, cardTree(), () => true, new Set(), new Set())
expect([...runs]).toEqual([['0.0', 2], ['0.4', 1]])
expect([...folded].sort()).toEqual(['0.0', '0.1', '0.4'])
expect([...runs]).toEqual([['0.2', 1]])
expect([...folded].sort()).toEqual(['0.2'])
})
it('上一处 / 下一处:要展开的是子块所在的那一行(点子行才展开的连它自己),不是卡片', () => {
it('上一处 / 下一处:要展开的是子块所在的那一行和它自己,不是卡片', () => {
expect(changeStops(CARD_BLOCKS, cardTree(), new Set()).map(stop => [stop.id, stop.ancestors])).toEqual([
['0.3.0', ['0/p8', '0.3', '0.3/p2']],
['0.1.0', ['0/p8', '0.1', '0.1/p2', '0.1.0']],
])
})
})
+5 -12
View File
@@ -114,21 +114,14 @@ export function foldUnchanged(blocks: readonly BlockView[], tree: DiffTree, show
}
close()
}
/** 一层子块:放进父卡片里的按所在的行分开折(各画在各的地方),点子行才展开的不折、只往里看改动 */
/** 一层子块:挂在父卡片子行上、点子行才展开的不折、只往里看改动,别的照常折 */
function visit(list: readonly BlockView[], owner: string): void {
const breaks = new Set((tree.removed.get(owner) ?? []).map(item => item.after))
const places = new Map<string, BlockView[]>()
for (const block of list) {
if (block.slot?.item !== undefined) {
if (tree.changes.has(block.id) || kept(block))
visit(block.children, block.id)
continue
}
const place = block.slot ? `${block.slot.row}` : ''
places.set(place, [...places.get(place) ?? [], block])
if (block.slot && (tree.changes.has(block.id) || kept(block)))
visit(block.children, block.id)
}
for (const group of places.values())
visitRun(group, breaks)
visitRun(list.filter(block => !block.slot), breaks)
}
visit(blocks, '')
return { runs, folded }
@@ -137,7 +130,7 @@ export function foldUnchanged(blocks: readonly BlockView[], tree: DiffTree, show
/**
* 「上一处 / 下一处」和总览条上的一处改动:新增的最外面那块、自己改了的修改块、删掉的块,照页面上的先后。
* id 是页面上的块 id(删掉的块是 PlacedRemoved.id);from / to 是它在新文件里的行(0 起),修改块只算它自己那几行(到第一个子块之前);
* ancestors 是要展开才看得见它的各层块和属性行(放进父卡片里的子块要展开它所在的那一行,见 unfoldToShow)。
* ancestors 是要展开才看得见它的各层块和属性行(挂在父卡片子行上的子块要展开它所在的那一行和它自己,见 unfoldToShow)。
*/
export interface ChangeStop {
id: string
@@ -172,14 +172,14 @@ describe('跳到定义:滚到哪块、展开哪些', () => {
expect(hidden({ '1': false, '1.0': true })).toEqual(['1.0'])
})
it('放进卡片属性表里的子块:展开它所在的那一行,不展开卡片自己;点子行才展开的连它自己一起展开', () => {
const statement = { ...textBlock('0.1.0', 'statement', 5, [name('scrollTo', 300)]), slot: { row: 2 } }
const handler = { ...factBlock('0.1', 'function', 4, [{ label: '名称', value: [name('handleBack', 200)] }], [statement]), slot: { row: 3, item: 0 } }
const effect = { ...textBlock('0.0', 'statement', 2, [name('useEffect', 100)]), slot: { row: 2 } }
const card = factBlock('0', 'function', 1, [{ label: '名称', value: [name('Page', 10)] }], [effect, handler])
expect(revealPlan([card], 0, 100)).toEqual({ id: '0.0', expand: [propertyRowId('0', 2)] })
it('挂在卡片子行上的子块:展开它所在的那一行和它自己,不展开卡片自己', () => {
const statement = { ...textBlock('0.1.0', 'statement', 5, [name('listen', 300)]), slot: { row: 2, item: 0 } }
const start = { ...factBlock('0.1', 'function', 4, [{ label: '名称', value: [name('start', 200)] }], [statement]), slot: { row: 3, item: 1 } }
const create = { ...factBlock('0.0', 'function', 2, [{ label: '名称', value: [name('new', 100)] }]), slot: { row: 3, item: 0 } }
const card = factBlock('0', 'class', 1, [{ label: '名称', value: [name('Server', 10)] }], [create, start])
expect(revealPlan([card], 0, 100)).toEqual({ id: '0.0', expand: [propertyRowId('0', 3), '0.0'] })
expect(revealPlan([card], 0, 200)).toEqual({ id: '0.1', expand: [propertyRowId('0', 3), '0.1'] })
expect(revealPlan([card], 0, 300)).toEqual({ id: '0.1.0', expand: [propertyRowId('0', 3), '0.1', propertyRowId('0.1', 2)] })
expect(revealPlan([card], 0, 300)).toEqual({ id: '0.1.0', expand: [propertyRowId('0', 3), '0.1', propertyRowId('0.1', 2), '0.1.0'] })
})
it('按行找:同一行有几块、一样大时取外层那块;行在所有块之前取第一块;没有块时没有目标', () => {
+4 -4
View File
@@ -142,18 +142,18 @@ interface BlockViewBase {
commentEndLine?: number
/** 库规则的卡片才有:卡片边框的颜色,筛选栏里这个种类的名字。和类块一样默认收起子块 */
badge?: KindBadge
/** 库规则放进父卡片里的子块才有:画在父卡片属性表第 row 行下面,不排在父卡片的子块里;item 见 BlockSlot */
/** 库规则挂在父卡片子行上的子块才有:展开时画在父卡片属性表第 row 行下面,不排在父卡片的子块里;见 BlockSlot */
slot?: BlockSlot
children: BlockView[]
}
/**
* 子块画在父卡片的哪儿:属性表第 row 行下面(折叠这一行时跟着收起);item 是讲它的那个子行,
* 这时它默认收着(它的块 id 记成折叠着),点那个子行的第一格才展开、一次只展开一个(Rust 实现块的方法表点方法名)
* 子块挂在父卡片的哪儿:属性表第 row 行的第 item 个子行。它默认收着(它的块 id 记成折叠着),点那个子行的第一格才在这一行下面展开、
* 同一行一次只展开一个,折叠这一行时跟着收起(Rust 实现块的方法表点方法名)
*/
export interface BlockSlot {
row: number
item?: number
item: number
}
/**
@@ -119,15 +119,14 @@ describe('眼下折叠着哪些', () => {
expect([...collapsedIds([block('0', 'exception', [block('0.0', 'function')]), block('1', 'if')], {})]).toEqual(['0', '0.0'])
})
it('放进父卡片里的子块:点子行才展开的默认收着(整张不画),别的照种类;要让它看得见得展开它所在的那一行(点子行才展开的还有它自己)', () => {
const badge = { label: '副作用', color: { dark: '#e5c07b', light: '#9a6a00' }, icon: 'zap' as const }
const effect: BlockView = { ...block('0.0', 'statement', [{ ...block('0.0.0', 'variable'), slot: { row: 1 } }]), badge, kind: 'react.effect', slot: { row: 7 } }
const handler: BlockView = { ...block('0.1', 'function'), badge, kind: 'react.handler', slot: { row: 8, item: 0 } }
const card: BlockView = { ...block('0', 'if', [effect, handler]), badge, kind: 'react.component' }
it('挂在父卡片子行上的子块:默认收着(整张不画),不看种类;要让它看得见得展开它所在的那一行和它自己', () => {
const badge = { label: '卡片', color: { dark: '#dcdcaa', light: '#795e26' }, icon: 'box' as const }
const first: BlockView = { ...block('0.0', 'statement'), slot: { row: 1, item: 0 } }
const second: BlockView = { ...block('0.1', 'statement'), slot: { row: 1, item: 1 } }
const card: BlockView = { ...block('0', 'if', [first, second, block('0.2', 'comment')]), badge, kind: 'demo.card' }
expect([...collapsedIds([card], {})].sort()).toEqual(['0', '0.0', '0.1'])
expect([...collapsedIds([card], { 0.1: false })].sort()).toEqual(['0', '0.0'])
expect(unfoldToShow('0', effect)).toEqual(['0/p7'])
expect(unfoldToShow('0', handler)).toEqual(['0/p8', '0.1'])
expect(unfoldToShow('0', second)).toEqual(['0/p1', '0.1'])
expect(unfoldToShow('0', block('0.2', 'comment'))).toEqual(['0'])
})
@@ -56,10 +56,10 @@ function readViewState(saved: unknown): ViewState {
/**
* 块默认折叠吗:函数、类、枚举、异常块和库规则的卡片收起子块(属性表照常显示),其余块展开。属性行和带下一层的子行默认都展开。
* 父卡片里点子行才展开的子块(BlockSlot.item)默认收着:它折叠着就是整张不画。
* 挂在父卡片子行上、点子行才展开的子块(BlockSlot)默认收着:它折叠着就是整张不画。
*/
export function foldedByDefault(block: Pick<BlockView, 'kind' | 'badge' | 'slot'>): boolean {
return block.slot?.item !== undefined || block.badge !== undefined || ['function', 'class', 'enum', 'exception'].includes(block.kind)
return block.slot !== undefined || block.badge !== undefined || ['function', 'class', 'enum', 'exception'].includes(block.kind)
}
/** 眼下折叠着的块、属性行和子行:用户点过的按他点的,没点过的按 foldedByDefault;opened 里的块(标了改动的)默认展开。 */
@@ -104,10 +104,10 @@ export function tableId(ownerId: string): string {
/**
* 父块看得见时,要让它的一个子块也看得见得展开的(块 id、属性行 id):一般的子块是父块自己(收起子块时不画);
* 放进父卡片属性表里的子块(BlockSlot)是它所在的那一行,点子行才展开的还有它自己。
* 挂在父卡片子行上的子块(BlockSlot)是它所在的那一行和它自己。
*/
export function unfoldToShow(parentId: string, child: Pick<BlockView, 'id' | 'slot'>): string[] {
if (!child.slot)
return [parentId]
return [propertyRowId(parentId, child.slot.row), ...(child.slot.item !== undefined ? [child.id] : [])]
return [propertyRowId(parentId, child.slot.row), child.id]
}