♻️ refactor(vscode): 代码阅读的种类图标改成插件自己定义的一组(契约里的封闭名字集合),webview 只带这些图标,不再打包 lucide 全套

This commit is contained in:
cruldra committed 2026-09-27 20:23:44 +08:00
1 parent c1d0dd3753
commit b186b007a4
11 files changed
+139 -47

No files matched your search

+32 -3
View File
@@ -414,10 +414,39 @@ export interface Translator {
export interface ThemeColor { dark: string, light: string }
/**
* 库规则的种类在预览里的样子:块左边只画图标(种类色),名字是悬停提示和读屏念的文字,筛选栏里这个种类也叫它。
* icon 是 lucide 图标名(https://lucide.dev/icons 上的 kebab-case 名字,如 `database`)。
* 种类图标:插件自己定义的一组 lucide 图标(https://lucide.dev/icons 上的 kebab-case 名字)。前 18 个给内置种类,后面的给库规则的种类;
* 库规则的种类(KindBadge.icon)只能从这组里挑,要新图标就加进这组(webview 的 kindIcons.tsx 同时给它配上图标),没有按任意名字找图标的路。
*/
export interface KindBadge { label: string, color: ThemeColor, icon: string }
export const KIND_ICON_NAMES = [
'import',
'square-function',
'box',
'braces',
'list-ordered',
'triangle-alert',
'tag',
'lock',
'variable',
'split',
'corner-down-right',
'repeat',
'shield',
'shield-alert',
'shield-check',
'corner-down-left',
'code',
'message-square-text',
'database',
'package',
'server-cog',
'calendar-clock',
'globe',
] as const
export type KindIconName = (typeof KIND_ICON_NAMES)[number]
/** 库规则的种类在预览里的样子:块左边只画图标(种类色),名字是悬停提示和读屏念的文字,筛选栏里这个种类也叫它。 */
export interface KindBadge { label: string, color: ThemeColor, icon: KindIconName }
/**
* 库规则认领一块后给出的替身,预览里它代替原来那块,下面挂着原块留下的子块:
+3 -3
View File
@@ -1,4 +1,4 @@
import type { IndexedFile, IndexEntry, Inline, LibraryRule, Node, PropertyItem, PropertyRow, RuleCard, RuleContext, Segment, ThemeColor, WorkspaceIndex } from '../contract'
import type { IndexedFile, IndexEntry, Inline, KindIconName, LibraryRule, Node, PropertyItem, PropertyRow, RuleCard, RuleContext, Segment, ThemeColor, WorkspaceIndex } from '../contract'
import type { Call } from './pythonTree'
import { aliasesOf, callOf, lastName, stringContent, stringValue, syntaxChildren } from './pythonTree'
import { hint, joined, name, plain, range, word } from './segments'
@@ -21,8 +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'
const APP_ICON: KindIconName = 'server-cog'
const BEAT_ICON: KindIconName = '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']
+2 -2
View File
@@ -1,4 +1,4 @@
import type { Access, ClassBlock, CodeBlock, Inline, LibraryRule, Node, PropertyItem, PropertyRow, RuleCard, RuleContext, ThemeColor } from '../contract'
import type { Access, ClassBlock, CodeBlock, Inline, KindIconName, LibraryRule, Node, PropertyItem, PropertyRow, RuleCard, RuleContext, ThemeColor } from '../contract'
import type { Call, LibraryNames } from './pythonTree'
import { memberDocs, withLeadingComments } from './children'
import { aliasesOf, callOf, canonicalName, genericOf, libraryName, libraryNames, stringValue, syntaxChildren, unwrapType } from './pythonTree'
@@ -16,7 +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'
const ICON: KindIconName = 'package'
/** `@dataclass(...)` 各参数不写时的值:写成这个值等于没写,「选项」里不讲。 */
const OPTION_DEFAULTS: Record<string, boolean> = {
+2 -2
View File
@@ -1,4 +1,4 @@
import type { FunctionBlock, ImportFacts, IndexEntry, Inline, LibraryRule, Node, ParameterFacts, PropertyItem, PropertyRow, RuleCard, RuleContext, Segment, ThemeColor, WorkspaceIndex } from '../contract'
import type { FunctionBlock, ImportFacts, IndexEntry, Inline, KindIconName, LibraryRule, Node, ParameterFacts, PropertyItem, PropertyRow, RuleCard, RuleContext, Segment, ThemeColor, WorkspaceIndex } from '../contract'
import type { Call, LibraryNames } from './pythonTree'
import { callOf, genericOf, lastName, libraryName, stringContent, syntaxChildren, unwrapType } from './pythonTree'
import { hint, joined, name, range, word } from './segments'
@@ -14,7 +14,7 @@ import { hint, joined, name, range, word } from './segments'
const RULE_ID = 'fastapi'
const KIND = 'fastapi.route'
const COLOR: ThemeColor = { dark: '#61afef', light: '#1a6fb5' }
const ICON = 'globe'
const ICON: KindIconName = 'globe'
/** 认路由对象、参数标记、HTTPException 时看从这几个库导入的名字(HTTPException、status 也可以从 starlette 来)。 */
const NAME_LIBRARIES = ['fastapi', 'starlette']
+2 -2
View File
@@ -1,4 +1,4 @@
import type { ClassBlock, CodeBlock, Inline, LibraryRule, Node, PropertyItem, PropertyRow, RuleCard, RuleContext, ThemeColor, TokenRole } from '../contract'
import type { ClassBlock, CodeBlock, Inline, KindIconName, LibraryRule, Node, PropertyItem, PropertyRow, RuleCard, RuleContext, ThemeColor, TokenRole } from '../contract'
import type { Call } from './pythonTree'
import { withLeadingComments } from './children'
import { aliasesOf, callOf, canonicalName, genericOf, lastName, stringValue, syntaxChildren, unwrapType } from './pythonTree'
@@ -20,7 +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'
const TABLE_ICON: KindIconName = 'database'
/** 一种显示语言的用词。 */
interface Words {
@@ -3,7 +3,7 @@ import type { CSSProperties, ReactElement } from 'react'
import type { GroupStyle, Inline, SegmentGroup } from './types'
import { useContext } from 'react'
import { DisplayNameContext, shownName } from './displayNames'
import { KIND_ICONS, lucideIcon } from './kindIcons'
import { KIND_ICON_COMPONENTS, KIND_ICONS } from './kindIcons'
import { isCurrent, segmentOccurrence } from './occurrences'
import { HighlightContext } from './useOccurrences'
@@ -43,7 +43,7 @@ export function SegmentText({ segments }: { segments: Inline[] }): ReactElement
}
/** 框的样式 → 框头图标:遍历同「循环」块的图标;颜色在 codeReader.css 的 .cr-group[data-style] 里。 */
const GROUP_ICONS: Record<GroupStyle, LucideIcon> = { iteration: lucideIcon(KIND_ICONS.loop) }
const GROUP_ICONS: Record<GroupStyle, LucideIcon> = { iteration: KIND_ICON_COMPONENTS[KIND_ICONS.loop] }
/** 框起来的一组词:嵌在前后文字里的小框,放不下时整个挪到下一行,比一行还长时在框里换行。 */
function GroupBox({ group }: { group: SegmentGroup }): ReactElement {
@@ -0,0 +1,10 @@
import { describe, expect, it } from 'vitest'
import { KIND_ICON_NAMES } from '../../../src/codeReader/contract'
import { KIND_ICON_COMPONENTS } from './kindIcons'
// webview 的类型照扩展端手抄一份(types.ts);这组图标名两边对不上时,规则给的图标在页面上就没有图,这里核对
describe('种类图标这组名字', () => {
it('webview 给扩展端 contract 里的每个名字都配了图标,没有多的', () => {
expect(Object.keys(KIND_ICON_COMPONENTS).sort()).toEqual([...KIND_ICON_NAMES].sort())
})
})
@@ -1,8 +1,7 @@
import type { BlockView } from './types'
import { Database, SquareDashed, SquareFunction } from 'lucide-react'
import type { BlockView, KindIconName } from './types'
import { renderToStaticMarkup } from 'react-dom/server'
import { describe, expect, it } from 'vitest'
import { KIND_ICONS, KindIcon, lucideIcon } from './kindIcons'
import { KIND_ICON_COMPONENTS, KIND_ICONS, KindIcon } from './kindIcons'
function render(block: Pick<BlockView, 'kind' | 'badge'>, name: string): string {
return renderToStaticMarkup(<KindIcon block={block} name={name} />)
@@ -33,14 +32,13 @@ describe('块的种类图标', () => {
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('这组图标里每个名字画的就是 lucide 里同名的图标', () => {
for (const name of Object.keys(KIND_ICON_COMPONENTS) as KindIconName[]) {
const html = render({ kind: 'sqlalchemy.table', badge: { label: name, color: { dark: '#fff', light: '#000' }, icon: name } }, name)
expect(html, name).toContain(`class="lucide lucide-${name}"`)
}
})
it('只画图标不带字,名字在悬停提示和 aria-label 里;20px 的框、16px 的图形', () => {
+58 -15
View File
@@ -1,13 +1,64 @@
import type { LucideIcon } from 'lucide-react'
import type { ReactElement } from 'react'
import type { BlockKind, BlockView } from './types'
import { icons, SquareDashed } from 'lucide-react'
import type { BlockKind, BlockView, KindIconName } from './types'
import {
Box,
Braces,
CalendarClock,
Code,
CornerDownLeft,
CornerDownRight,
Database,
Globe,
Import,
ListOrdered,
Lock,
MessageSquareText,
Package,
Repeat,
ServerCog,
Shield,
ShieldAlert,
ShieldCheck,
Split,
SquareFunction,
Tag,
TriangleAlert,
Variable,
} from 'lucide-react'
/** 插件定义的这组种类图标,每个名字一个 lucide 图标(逐个 import,打包只带这些)。 */
export const KIND_ICON_COMPONENTS: Record<KindIconName, LucideIcon> = {
'import': Import,
'square-function': SquareFunction,
'box': Box,
'braces': Braces,
'list-ordered': ListOrdered,
'triangle-alert': TriangleAlert,
'tag': Tag,
'lock': Lock,
'variable': Variable,
'split': Split,
'corner-down-right': CornerDownRight,
'repeat': Repeat,
'shield': Shield,
'shield-alert': ShieldAlert,
'shield-check': ShieldCheck,
'corner-down-left': CornerDownLeft,
'code': Code,
'message-square-text': MessageSquareText,
'database': Database,
'package': Package,
'server-cog': ServerCog,
'calendar-clock': CalendarClock,
'globe': Globe,
}
/**
* 内置种类的图标(lucide 图标名,和库规则给的 badge.icon 同一种写法):块左边只画图标,名字是悬停提示。
* 内置种类的图标:块左边只画图标,名字是悬停提示。
* 同一类语义共用一个颜色(codeReader.css 的 --cr-kind-*:常量 / 变量 / 字段都是数据,尝试 / 捕获都是异常处理),图标区分具体是哪种。
*/
export const KIND_ICONS: Record<BlockKind, string> = {
export const KIND_ICONS: Record<BlockKind, KindIconName> = {
import: 'import',
function: 'square-function',
class: 'box',
@@ -28,21 +79,13 @@ export const KIND_ICONS: Record<BlockKind, string> = {
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 里。
* 没有就是内置种类的(KIND_ICONS,没带 badge 的块 kind 是 BlockKind);名字(翻译插件给种类起的,库规则的种类是规则给的)
* 写在悬停提示(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] ?? '')
const Icon = KIND_ICON_COMPONENTS[block.badge?.icon ?? KIND_ICONS[block.kind as BlockKind]]
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 />
+8 -2
View File
@@ -73,8 +73,14 @@ export interface PropertyRow extends Defines {
/** 一种颜色在暗色、亮色主题下各用哪个(CSS 颜色值);高对比主题跟着暗 / 亮走 */
export interface ThemeColor { dark: string, light: string }
/** 库规则的种类:名字(悬停提示、读屏念的文字)、颜色和 lucide 图标名;块左边只画图标 */
export interface KindBadge { label: string, color: ThemeColor, icon: string }
/** 种类图标:插件自己定义的一组 lucide 图标(同扩展端 contract 的 KIND_ICON_NAMES),每个名字在 kindIcons.tsx 里配一个图标 */
export type KindIconName
= | 'import' | 'square-function' | 'box' | 'braces' | 'list-ordered' | 'triangle-alert' | 'tag' | 'lock' | 'variable'
| 'split' | 'corner-down-right' | 'repeat' | 'shield' | 'shield-alert' | 'shield-check' | 'corner-down-left' | 'code'
| 'message-square-text' | 'database' | 'package' | 'server-cog' | 'calendar-clock' | 'globe'
/** 库规则的种类:名字(悬停提示、读屏念的文字)、颜色和图标(从 KindIconName 里挑);块左边只画图标 */
export interface KindBadge { label: string, color: ThemeColor, icon: KindIconName }
interface BlockViewBase {
/** 在树里的位置,例如 '6.1.0' */