✨ feat(vscode): 代码阅读的块种类只画图标(lucide,种类色),种类名放进鼠标停上去的提示和读屏文字;库规则给自己的图标
This commit is contained in:
1 parent
33a1b5d57c
commit
1dfc0c7b1e
19 files changed
+185
-56
No files matched your search
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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'])
|
||||
})
|
||||
|
||||
|
||||
Reference in new issue
Block a user