✨ feat(vscode): 代码阅读的块种类只画图标(lucide,种类色),种类名放进鼠标停上去的提示和读屏文字;库规则给自己的图标

This commit is contained in:
cruldra committed 2026-09-27 20:02:32 +08:00
1 parent 33a1b5d57c
commit 1dfc0c7b1e
19 files changed
+185 -56

No files matched your search

+2 -2
View File
@@ -46,14 +46,14 @@ export function blockAttributes(block: CodeBlock): BlockAttributes {
interface Attributed {
attributes: BlockAttributes
/** 库规则认领的块的种类标签:这个种类在筛选栏里也叫它。 */
/** 库规则认领的块的种类(名字、颜色、图标):这个种类在筛选栏里也叫它的名字。 */
badge?: { label: string }
children: Attributed[]
}
/**
* 整份文件(所有层的块)里出现过的属性,每个值有几块。通用属性按 COMMON_ATTRIBUTES 的顺序排在前面,
* 语言自己的属性按第一次出现的先后排在后面。显示名取翻译插件的 names,没给的用 id;库规则的种类用规则给的标签。
* 语言自己的属性按第一次出现的先后排在后面。显示名取翻译插件的 names,没给的用 id;库规则的种类用规则给的名字(badge.label)。
*/
export function fileAttributes(blocks: readonly Attributed[], names: AttributeNames | undefined): FileAttribute[] {
const counts = new Map<string, Map<string, number>>()
+1 -1
View File
@@ -26,7 +26,7 @@ interface BlockViewBase {
defines?: SourceRange
/** 筛选属性(含 kind),见 blockAttributes;库规则认领的块叠上规则给的,kind 是规则的种类 id。 */
attributes: BlockAttributes
/** 库规则的卡片才有:种类标签的文字和颜色(RuleCard.badge)。预览同类块一样默认收起它的子块。 */
/** 库规则的卡片才有:种类的名字、颜色和图标(RuleCard.badge)。预览同类块一样默认收起它的子块。 */
badge?: KindBadge
children: BlockView[]
}
+8 -5
View File
@@ -382,7 +382,7 @@ export interface SourceDocument {
markers: SourceRange[]
}
/** 筛选属性和值的显示名,筛选栏、种类标签用它;没给名字的显示 id。 */
/** 筛选属性和值的显示名,筛选栏、块的种类(图标的悬停提示)用它;没给名字的显示 id。 */
export interface AttributeNames {
/** 属性 id → 属性名,例如 access → 访问级别。 */
attributes: Record<string, string>
@@ -412,8 +412,11 @@ export interface Translator {
/** 一种颜色在暗色、亮色主题下各用哪个(CSS 颜色值);高对比主题跟着暗 / 亮走。 */
export interface ThemeColor { dark: string, light: string }
/** 种类标签:文字和颜色。 */
export interface KindBadge { label: string, color: ThemeColor }
/**
* 库规则的种类在预览里的样子:块左边只画图标(种类色),名字是悬停提示和读屏念的文字,筛选栏里这个种类也叫它。
* icon 是 lucide 图标名(https://lucide.dev/icons 上的 kebab-case 名字,如 `database`)。
*/
export interface KindBadge { label: string, color: ThemeColor, icon: string }
/**
* 库规则认领一块后给出的替身,预览里它代替原来那块,下面挂着原块留下的子块:
@@ -421,11 +424,11 @@ export interface KindBadge { label: string, color: ThemeColor }
*/
export type RuleBlock = RuleCard | RuleFacts
/** 规则自己的卡片:一张属性表,种类标签、每一行都由规则给(用词归规则)。 */
/** 规则自己的卡片:一张属性表,种类的图标、颜色、名字和每一行都由规则给(用词归规则)。 */
export interface RuleCard {
/** 种类 id,写成「规则 id.名字」(sqlalchemy.table):筛选的 kind 属性值就是它,筛选记住的也是它,要稳定。 */
kind: string
/** 种类标签的文字和颜色:筛选栏里这个种类也叫这个名字。 */
/** 种类的名字、颜色和图标:筛选栏里这个种类也叫这个名字。 */
badge: KindBadge
rows: PropertyRow[]
/** 块自己的名字(类名)在源码里的范围:跳到的定义就是它时闪整块。 */
@@ -296,7 +296,7 @@ describe('异常卡片(TypeScript)', () => {
})
describe('异常卡片(宿主、筛选、中文名)', () => {
it('筛选:种类是 exception、叫「异常」,访问级别同原来的类;没有种类标签(badge),库规则不认', async () => {
it('筛选:种类是 exception、叫「异常」,访问级别同原来的类;没有规则给的种类(badge),库规则不认', async () => {
const list = await pyViews(PY_CASES)
expect(named(list, 'Base').attributes).toEqual({ kind: 'exception', access: 'public' })
expect(named(list, 'Base').badge).toBeUndefined()
+5 -5
View File
@@ -32,7 +32,7 @@ function cardRule(id: string, overrides: Partial<RuleBlock> = {}): LibraryRule {
languageIds: ['python'],
libraries: ['sqlalchemy'],
claim: block => block.kind === 'class'
? { kind: `${id}.card`, badge: { label: `${id} 卡片`, color: { dark: '#fff', light: '#000' } }, rows: [], absorbs: block.children.slice(0, 1), ...overrides }
? { kind: `${id}.card`, badge: { label: `${id} 卡片`, color: { dark: '#fff', light: '#000' }, icon: 'box' }, rows: [], absorbs: block.children.slice(0, 1), ...overrides }
: undefined,
}
}
@@ -91,14 +91,14 @@ describe('库规则:启用', () => {
})
describe('库规则:认领', () => {
it('认领的块换成替身:种类、标签、编辑范围是整个类;规则没并进来的子块(夹在中间的注释、方法)照源码顺序留下', async () => {
it('认领的块换成替身:种类、badge、编辑范围是整个类;规则没并进来的子块(夹在中间的注释、方法)照源码顺序留下', async () => {
const rule: LibraryRule = {
...cardRule('orm'),
claim: (block) => {
if (block.kind !== 'class')
return undefined
const [table, , column] = block.children
return { kind: 'orm.card', badge: { label: '卡片', color: { dark: '#fff', light: '#000' } }, rows: [{ label: '名称', value: [{ text: 'User', role: 'type' }] }], absorbs: [table, column] }
return { kind: 'orm.card', badge: { label: '卡片', color: { dark: '#fff', light: '#000' }, icon: 'box' }, rows: [{ label: '名称', value: [{ text: 'User', role: 'type' }] }], absorbs: [table, column] }
},
}
const [, card] = await views(MODEL, [rule])
@@ -128,7 +128,7 @@ describe('库规则:认领', () => {
'',
].join('\n')
const fields: LibraryRule = { ...cardRule('orm'), claim: block => block.kind === 'class'
? { kind: 'orm.card', badge: { label: '卡片', color: { dark: '#fff', light: '#000' } }, rows: [], absorbs: block.children.filter(child => child.kind === 'variable') }
? { kind: 'orm.card', badge: { label: '卡片', color: { dark: '#fff', light: '#000' }, icon: 'box' }, rows: [], absorbs: block.children.filter(child => child.kind === 'variable') }
: undefined }
const [, card] = await views(source, [fields])
expect(card.children.map(child => [child.kind, child.startLine, child.endLine])).toEqual([['function', 5, 5]])
@@ -198,7 +198,7 @@ describe('库规则:认领', () => {
expect(reports).toEqual(['库规则 lib 给第 3 行的 class 块的替身不合约定,这块照常显示:要并进来的块不是这块的子块'])
})
it('筛选:规则的种类和别的种类一样计数,名字取规则给的标签', async () => {
it('筛选:规则的种类和别的种类一样计数,名字取规则给的名字(badge.label)', async () => {
const list = await views(MODEL, [cardRule('orm')])
expect(fileAttributes(list, pythonZhTranslator.attributeNames).find(attribute => attribute.id === 'kind')?.values).toEqual([
{ id: 'import', name: '导入', count: 1 },
+2 -2
View File
@@ -46,7 +46,7 @@ function segmentsOf(rows: PropertyRow[]): Segment[] {
describe('celery 库规则:应用', () => {
it('luffy 的 celery_app:名称 / 应用名 / 消息队列 / 结果存储', async () => {
const app = await card(SNAPSHOT, 'celery.app')
expect(app.badge).toEqual({ label: 'Celery 应用', color: { dark: '#98c379', light: '#4d7f17' } })
expect(app.badge).toEqual({ label: 'Celery 应用', color: { dark: '#98c379', light: '#4d7f17' }, icon: 'server-cog' })
expect(propertyLines(properties(app))).toEqual([
'名称: celery_app',
'应用名: "luffy_agent"',
@@ -90,7 +90,7 @@ describe('celery 库规则:应用', () => {
describe('celery 库规则:定时任务', () => {
it('luffy 的 beat_schedule:应用、任务表,频率换算成人话,说明取注释,展开另起一行', async () => {
const beat = await card(SNAPSHOT, 'celery.beat')
expect(beat.badge).toEqual({ label: '定时任务', color: { dark: '#56b6c2', light: '#127f8c' } })
expect(beat.badge).toEqual({ label: '定时任务', color: { dark: '#56b6c2', light: '#127f8c' }, icon: 'calendar-clock' })
expect(propertyLines(properties(beat))).toEqual([
'应用: celery_app',
'任务: 12 条,另有展开 1 处',
+4 -2
View File
@@ -21,6 +21,8 @@ const APP_KIND = 'celery.app'
const BEAT_KIND = 'celery.beat'
const APP_COLOR: ThemeColor = { dark: '#98c379', light: '#4d7f17' }
const BEAT_COLOR: ThemeColor = { dark: '#56b6c2', light: '#127f8c' }
const APP_ICON = 'server-cog'
const BEAT_ICON = 'calendar-clock'
/** `Celery(...)` 的位置参数依次是哪个参数(celery.app.base.Celery.__init__)。 */
const APP_PARAMETERS = ['main', 'loader', 'backend', 'amqp', 'events', 'log', 'control', 'set_as_current', 'tasks', 'broker', 'include', 'changes', 'config_source', 'fixups', 'task_cls', 'autofinalize', 'namespace', 'strict_typing']
@@ -500,7 +502,7 @@ function beatBlock(app: Node, schedule: Node, settings: [string, Node][], doc: N
items: entries.map(entry => entryItem(entry, withOptions, env)),
},
]
return { kind: BEAT_KIND, badge: { label: words.beatKind, color: BEAT_COLOR }, rows, absorbs: [] }
return { kind: BEAT_KIND, badge: { label: words.beatKind, color: BEAT_COLOR, icon: BEAT_ICON }, rows, absorbs: [] }
}
// ─── Celery 应用 ──────────────────────────────────────────────────────────────
@@ -527,7 +529,7 @@ function appBlock(target: Node, call: Call, doc: Node | undefined, env: Env): Ru
]
return {
kind: APP_KIND,
badge: { label: words.appKind, color: APP_COLOR },
badge: { label: words.appKind, color: APP_COLOR, icon: APP_ICON },
rows,
...(target.type === 'identifier' && { defines: range(target) }),
absorbs: [],
@@ -128,11 +128,11 @@ describe('dataclasses 库规则(Python)', () => {
expect(sandbox.children.map(child => [child.kind, child.startLine])).toEqual([['function', 52]])
})
it('筛选:种类是 dataclasses.dataclass、叫「数据类」,访问级别同原来的类;种类标签有自己的颜色,默认收起子块', async () => {
it('筛选:种类是 dataclasses.dataclass、叫「数据类」,访问级别同原来的类;种类有自己的颜色和图标,默认收起子块', async () => {
const list = await views(SNAPSHOT)
const sandbox = named(list, 'SandboxAccess')
expect(sandbox.attributes).toEqual({ kind: 'dataclasses.dataclass', access: 'public' })
expect(sandbox.badge).toEqual({ label: '数据类', color: { dark: '#ef7fb0', light: '#b3306c' } })
expect(sandbox.badge).toEqual({ label: '数据类', color: { dark: '#ef7fb0', light: '#b3306c' }, icon: 'package' })
expect(fileAttributes(list, pythonZhTranslator.attributeNames).find(attribute => attribute.id === 'kind')?.values)
.toContainEqual({ id: 'dataclasses.dataclass', name: '数据类', count: 4 })
})
+2 -1
View File
@@ -16,6 +16,7 @@ const LIBRARY = 'dataclasses'
const TYPE_LIBRARIES = ['dataclasses', 'typing', 'typing_extensions']
const KIND = 'dataclasses.dataclass'
const COLOR: ThemeColor = { dark: '#ef7fb0', light: '#b3306c' }
const ICON = 'package'
/** `@dataclass(...)` 各参数不写时的值:写成这个值等于没写,「选项」里不讲。 */
const OPTION_DEFAULTS: Record<string, boolean> = {
@@ -261,7 +262,7 @@ function dataclassCard(block: ClassBlock, decorator: Node, call: Call | undefine
]
return {
kind: KIND,
badge: { label: words.kind, color: COLOR },
badge: { label: words.kind, color: COLOR, icon: ICON },
rows,
...(facts.name && { defines: range(facts.name) }),
absorbs: withLeadingComments(children, [...fields.map(field => field.block), ...markers]),
@@ -47,7 +47,7 @@ describe('sqlAlchemy 库规则', () => {
it('referralRelation:和原型一样的数据表卡片', async () => {
const table = await model(SNAPSHOT, 'ReferralRelation')
expect(table.kind).toBe('sqlalchemy.table')
expect(table.badge).toEqual({ label: '数据表', color: { dark: '#e8a860', light: '#a3590d' } })
expect(table.badge).toEqual({ label: '数据表', color: { dark: '#e8a860', light: '#a3590d' }, icon: 'database' })
expect(propertyLines(properties(table))).toEqual([
'名称: ReferralRelation',
'表名: referral_relations',
+2 -1
View File
@@ -20,6 +20,7 @@ const SCHEMA_ITEMS = new Set(['ForeignKey', 'Sequence', 'Identity', 'Computed',
const SIZE_KEYWORDS = ['length', 'precision', 'scale']
const TABLE_KIND = 'sqlalchemy.table'
const TABLE_COLOR: ThemeColor = { dark: '#e8a860', light: '#a3590d' }
const TABLE_ICON = 'database'
/** 一种显示语言的用词。 */
interface Words {
@@ -435,7 +436,7 @@ function tableBlock(block: ClassBlock, model: Model, env: Env): RuleCard {
const members = [model.table, model.args, ...model.columns.map(column => column.member), ...model.relationships.map(relationship => relationship.member)]
return {
kind: TABLE_KIND,
badge: { label: words.kind, color: TABLE_COLOR },
badge: { label: words.kind, color: TABLE_COLOR, icon: TABLE_ICON },
rows,
...(facts.name && { defines: range(facts.name) }),
absorbs: withLeadingComments(block.children, members.filter((member): member is Member => member !== undefined).map(member => member.block)),
@@ -7,6 +7,7 @@ import { Fragment } from 'react'
import { AskThread } from './AskThread'
import { BlockTools } from './BlockTools'
import { CodeEditor } from './CodeEditor'
import { KindIcon } from './kindIcons'
import { PropertyGrid } from './PropertyGrid'
import { asksOf, historyOf } from './qa'
import { TextPreview } from './TextPreview'
@@ -22,7 +23,7 @@ export interface BlockTreeState {
temporary: ReadonlySet<string>
/** 被正在编辑的祖先盖住、不画的块(它们的行在祖先的编辑范围里),见 coveredByEditing */
covered: ReadonlyMap<string, string>
/** 种类标签上写什么:翻译插件给种类起的名字(库规则的种类是规则给的标签),没给就是 id */
/** 种类叫什么(种类图标的悬停提示和 aria-label):翻译插件给种类起的名字(库规则的种类是规则给的 badge.label),没给就是 id */
kindName: (kind: string) => string
/** 刚点了「编辑」、要把光标放进去的块 */
focusId: string | null
@@ -123,7 +124,7 @@ function BlockPreview({ block, tree }: { block: BlockView, tree: BlockTreeState
)
}
/** 一块:折叠箭头、种类标签、预览内容或源码、右上角的一排图标按钮(BlockTools)。 */
/** 一块:折叠箭头、种类图标、预览内容或源码、右上角的一排图标按钮(BlockTools)。 */
function BlockCard({ block, tree }: { block: BlockView, tree: BlockTreeState }): ReactElement {
const editing = tree.editing.has(block.id)
const collapsed = tree.collapsed.has(block.id)
@@ -150,9 +151,7 @@ function BlockCard({ block, tree }: { block: BlockView, tree: BlockTreeState }):
</button>
)
: <span className="w-3.5 shrink-0" />}
<span className="cr-badge flex h-5 min-w-11 shrink-0 items-center justify-center rounded px-1 text-[11px] leading-[14px] font-medium whitespace-nowrap">
{tree.kindName(block.kind)}
</span>
<KindIcon block={block} name={tree.kindName(block.kind)} />
<div className="min-w-0 flex-1">
{editing
? (
@@ -1,9 +1,9 @@
import type { LucideIcon } from 'lucide-react'
import type { ReactElement } from 'react'
import type { GroupStyle, Inline, SegmentGroup } from './types'
import { Repeat } from 'lucide-react'
import { useContext } from 'react'
import { DisplayNameContext, shownName } from './displayNames'
import { KIND_ICONS, lucideIcon } from './kindIcons'
import { isCurrent, segmentOccurrence } from './occurrences'
import { HighlightContext } from './useOccurrences'
@@ -41,8 +41,8 @@ export function SegmentText({ segments }: { segments: Inline[] }): ReactElement
)
}
/** 框的样式 → 框头图标;颜色在 codeReader.css 的 .cr-group[data-style] 里。 */
const GROUP_ICONS: Record<GroupStyle, LucideIcon> = { iteration: Repeat }
/** 框的样式 → 框头图标:遍历同「循环」块的图标;颜色在 codeReader.css 的 .cr-group[data-style] 里。 */
const GROUP_ICONS: Record<GroupStyle, LucideIcon> = { iteration: lucideIcon(KIND_ICONS.loop) }
/** 框起来的一组词:嵌在前后文字里的小框,放不下时整个挪到下一行,比一行还长时在框里换行。 */
function GroupBox({ group }: { group: SegmentGroup }): ReactElement {
@@ -56,7 +56,7 @@ body.vscode-high-contrast-light {
--cr-kind-comment: #008000;
}
/* 种类颜色:同一类语义共用一个颜色,标签文字区分具体是哪种。卡片取自己的种类,子层取所属块的种类 */
/* 种类颜色:同一类语义共用一个颜色,图标区分具体是哪种(kindIcons.tsx)。卡片取自己的种类,子层取所属块的种类 */
[data-kind="import"] { --kind: var(--cr-kind-import); }
[data-kind="function"] { --kind: var(--cr-kind-function); }
:is([data-kind="class"], [data-kind="type"]) { --kind: var(--cr-kind-type); }
@@ -174,9 +174,8 @@ body:is(.vscode-light, .vscode-high-contrast-light) [data-badge] { --kind: var(-
}
}
.cr-badge {
.cr-kind {
color: var(--kind);
background: color-mix(in srgb, var(--kind) 18%, transparent);
}
/* 译文和源码共用的文字排版;源码编辑时输入框和着色层都用它,两层逐字对齐 */
@@ -0,0 +1,59 @@
import type { BlockView } from './types'
import { Database, SquareDashed, SquareFunction } from 'lucide-react'
import { renderToStaticMarkup } from 'react-dom/server'
import { describe, expect, it } from 'vitest'
import { KIND_ICONS, KindIcon, lucideIcon } from './kindIcons'
function render(block: Pick<BlockView, 'kind' | 'badge'>, name: string): string {
return renderToStaticMarkup(<KindIcon block={block} name={name} />)
}
/** 标签之间的字:图标(svg)没有字 */
const visibleText = (html: string): string => html.replace(/<[^>]*>/g, '').trim()
describe('块的种类图标', () => {
it('内置种类各有一个图标,和原型的一一对应', () => {
expect(KIND_ICONS).toEqual({
import: 'import',
function: 'square-function',
class: 'box',
type: 'braces',
enum: 'list-ordered',
exception: 'triangle-alert',
field: 'tag',
const: 'lock',
variable: 'variable',
if: 'split',
else: 'corner-down-right',
loop: 'repeat',
try: 'shield',
catch: 'shield-alert',
finally: 'shield-check',
return: 'corner-down-left',
statement: 'code',
comment: 'message-square-text',
})
for (const name of Object.values(KIND_ICONS))
expect(lucideIcon(name), name).not.toBe(SquareDashed)
})
it('lucide 图标名按 kebab-case 认;认不出的名字画虚线方块', () => {
expect(lucideIcon('square-function')).toBe(SquareFunction)
expect(lucideIcon('database')).toBe(Database)
expect(lucideIcon('no-such-icon')).toBe(SquareDashed)
})
it('只画图标不带字,名字在悬停提示和 aria-label 里;20px 的框、16px 的图形', () => {
const html = render({ kind: 'function' }, '函数')
expect(visibleText(html)).toBe('')
expect(html).toMatch(/^<span class="cr-kind [^"]*size-5[^"]*" role="img" title="函数" aria-label="函数"><svg/)
expect(html).toContain('lucide-square-function')
expect(html).toContain('width="16" height="16"')
})
it('库规则的卡片用规则给的图标,名字是规则给的', () => {
const html = render({ kind: 'sqlalchemy.table', badge: { label: '数据表', color: { dark: '#e8a860', light: '#a3590d' }, icon: 'database' } }, '数据表')
expect(html).toContain('lucide-database')
expect(html).toContain('title="数据表" aria-label="数据表"')
})
})
@@ -0,0 +1,51 @@
import type { LucideIcon } from 'lucide-react'
import type { ReactElement } from 'react'
import type { BlockKind, BlockView } from './types'
import { icons, SquareDashed } from 'lucide-react'
/**
* 内置种类的图标(lucide 图标名,和库规则给的 badge.icon 同一种写法):块左边只画图标,名字是悬停提示。
* 同一类语义共用一个颜色(codeReader.css 的 --cr-kind-*:常量 / 变量 / 字段都是数据,尝试 / 捕获都是异常处理),图标区分具体是哪种。
*/
export const KIND_ICONS: Record<BlockKind, string> = {
import: 'import',
function: 'square-function',
class: 'box',
type: 'braces',
enum: 'list-ordered',
exception: 'triangle-alert',
field: 'tag',
const: 'lock',
variable: 'variable',
if: 'split',
else: 'corner-down-right',
loop: 'repeat',
try: 'shield',
catch: 'shield-alert',
finally: 'shield-check',
return: 'corner-down-left',
statement: 'code',
comment: 'message-square-text',
}
const LUCIDE: Record<string, LucideIcon | undefined> = icons
/** lucide 图标名(kebab-case,`square-function`)→ 图标组件;认不出的名字(规则写错了)画虚线方块。 */
export function lucideIcon(name: string): LucideIcon {
return LUCIDE[name.replace(/(?:^|-)([a-z0-9])/g, (_, first: string) => first.toUpperCase())] ?? SquareDashed
}
const BUILT_IN: Partial<Record<string, string>> = KIND_ICONS
/**
* 块左边的种类:只画图标,和一行字等高(20px 的框、16px 的图形),颜色是种类色(--kind)。图标是库规则给的(badge.icon),
* 没有就按内置种类查 KIND_ICONS;名字(翻译插件给种类起的,库规则的种类是规则给的)写在悬停提示(title)和读屏念的 aria-label 里。
*/
export function KindIcon({ block, name }: { block: Pick<BlockView, 'kind' | 'badge'>, name: string }): ReactElement {
const Icon = lucideIcon(block.badge?.icon ?? BUILT_IN[block.kind] ?? '')
return (
<span className="cr-kind flex size-5 shrink-0 items-center justify-center" role="img" title={name} aria-label={name}>
<Icon size={16} strokeWidth={2.2} aria-hidden />
</span>
)
}
+3 -3
View File
@@ -72,8 +72,8 @@ export interface PropertyRow extends Defines {
/** 一种颜色在暗色、亮色主题下各用哪个(CSS 颜色值);高对比主题跟着暗 / 亮走 */
export interface ThemeColor { dark: string, light: string }
/** 种类标签:文字和颜色 */
export interface KindBadge { label: string, color: ThemeColor }
/** 库规则的种类:名字(悬停提示、读屏念的文字)、颜色和 lucide 图标名;块左边只画图标 */
export interface KindBadge { label: string, color: ThemeColor, icon: string }
interface BlockViewBase {
/** 在树里的位置,例如 '6.1.0' */
@@ -93,7 +93,7 @@ interface BlockViewBase {
defines?: SourceRange
/** 筛选属性:属性 id → 值 id(含 kind),都是和语言无关的英文 id,显示名在 DocumentMessage.attributes 里 */
attributes: Record<string, string>
/** 库规则认领的块才有:种类标签的文字和颜色,由规则给;和类块一样默认收起子块 */
/** 库规则认领的块才有:种类的名字、颜色和图标,由规则给;和类块一样默认收起子块 */
badge?: KindBadge
children: BlockView[]
}
@@ -106,8 +106,8 @@ describe('眼下折叠着哪些', () => {
expect([...collapsedIds(BLOCKS, {})].sort()).toEqual(['1', '1.0.0', '2', '2.0'])
})
it('没点过:库规则认领的块(带种类标签)和类块一样折叠,不看种类 id', () => {
const table: BlockView = { ...block('0', 'if', [block('0.0', 'return')]), kind: 'sqlalchemy.table', badge: { label: '数据表', color: { dark: '#e8a860', light: '#a3590d' } } }
it('没点过:库规则认领的块(带 badge)和类块一样折叠,不看种类 id', () => {
const table: BlockView = { ...block('0', 'if', [block('0.0', 'return')]), kind: 'sqlalchemy.table', badge: { label: '数据表', color: { dark: '#e8a860', light: '#a3590d' }, icon: 'database' } }
expect([...collapsedIds([table, block('1', 'if')], {})]).toEqual(['0'])
})