diff --git a/docs/superpowers/specs/code-reader/spec.md b/docs/superpowers/specs/code-reader/spec.md index 8598b30..6fb7b64 100644 --- a/docs/superpowers/specs/code-reader/spec.md +++ b/docs/superpowers/specs/code-reader/spec.md @@ -18,7 +18,7 @@ | 扩展方式 | 写代码注册:语言插件、翻译插件都实现约定接口,通过本插件 `activate()` 导出的 API 注册;内置实现也走同一个 API | | 多种翻译怎么选 | 预览顶部筛选栏最右边的下拉框,整个文件一起换,按语言记住选择;只有一种时不显示 | | 块筛选 | 预览顶部钉住一条筛选栏,按块的属性(种类、访问级别、异步……)筛;属性由语言插件按和语言无关的 id 报(通用的由宿主从 facts 推),名字由翻译插件起,webview 不认识任何语言;筛选整个工作区共用、记住 | -| 块的种类 | 块左边只画种类图标(lucide 图标,和一行字等高,种类色),种类名是鼠标停上去的提示和读屏念的文字,不再写字;内置种类的图标、颜色、名字由插件定(图标在 webview 一处、颜色在 CSS、名字由翻译插件起),库规则的种类由规则给(`KindBadge` 的名字、颜色、图标)。见「预览(代码块树)」 | +| 块的种类 | 块左边只画种类图标(插件自己定义的一组 lucide 图标里的一个,和一行字等高,种类色),种类名是鼠标停上去的提示和读屏念的文字,不再写字;内置种类的图标、颜色、名字由插件定(图标在 webview 一处、颜色在 CSS、名字由翻译插件起),库规则的种类由规则给(`KindBadge` 的名字、颜色、图标)。见「预览(代码块树)」 | | 侧栏点文件 | 支持的语言默认打开成「预览」;不支持的语言用普通编辑器打开 | | 块内修改 | 改的就是文件本身:未保存标记、⌘S 保存、⌘Z 撤销都照常,切回「预览」按新代码重新翻译 | | 折叠 | 有子块的块、属性表里带子行的行、带下一层的子行(字典 / 对象里嵌套的字典 / 对象)能折叠;函数块(含方法、嵌套函数)、类块、枚举块、异常块和库规则的卡片默认收起主体、属性表照常显示,其余块、属性行和子行默认展开;用户点过的按他点的记住 | @@ -115,7 +115,7 @@ - 每一句是一块:导入、函数、类、枚举、异常、类型、字段、常量、变量、条件、否则、循环、尝试、捕获、最终、返回、语句、注释。 - 嵌套的块放在父块下面、往右缩进;每一层左边一条竖线(颜色 `--vscode-editorIndentGuide-background`)。 - 块的组成(从左到右):折叠箭头(只有有子块的块才显示,其余占位对齐)、种类图标、内容、右上角一排图标按钮。 -- 种类图标(原型 `kindBadge`、画板「Prototype / 块种类图标」):块左边只画图标、不带字,和一行字等高(20px 的框,图形 16px,lucide 的线宽 2.2),颜色是种类色;种类的名字(翻译插件给 `kind` 值起的,见「筛选」;库规则的种类是规则给的 `badge.label`)写在悬停提示(`title`,同块右上角按钮的悬停提示)和 `aria-label` 里,`role="img"`。和折叠箭头、内容同一行顶对齐。每种内置种类由插件定图标、颜色、名字:图标是 lucide 图标名,只在 webview 的一处(`kindIcons.tsx` 的 `KIND_ICONS`);颜色在 `codeReader.css` 的 `--cr-kind-*`(见「颜色」);名字由翻译插件给。同一类语义共用一个颜色,图标区分具体是哪种: +- 种类图标(原型 `kindBadge`、画板「Prototype / 块种类图标」):块左边只画图标、不带字,和一行字等高(20px 的框,图形 16px,lucide 的线宽 2.2),颜色是种类色;种类的名字(翻译插件给 `kind` 值起的,见「筛选」;库规则的种类是规则给的 `badge.label`)写在悬停提示(`title`,同块右上角按钮的悬停提示)和 `aria-label` 里,`role="img"`。和折叠箭头、内容同一行顶对齐。每种内置种类由插件定图标、颜色、名字:图标从插件定义的一组图标里取(见下),内置种类 → 图标只在 webview 的一处(`kindIcons.tsx` 的 `KIND_ICONS`);颜色在 `codeReader.css` 的 `--cr-kind-*`(见「颜色」);名字由翻译插件给。同一类语义共用一个颜色,图标区分具体是哪种: | 种类 | 图标 | 种类 | 图标 | 种类 | 图标 | | --- | --- | --- | --- | --- | --- | @@ -126,7 +126,9 @@ | enum 枚举 | `list-ordered` | else 否则 | `corner-down-right` | statement 语句 | `code` | | exception 异常 | `triangle-alert` | loop 循环 | `repeat` | comment 注释 | `message-square-text` | - 库规则的种类用规则给的图标(`KindBadge.icon`):SQLAlchemy 数据表 `database`、数据类 `package`、Celery 应用 `server-cog`、定时任务 `calendar-clock`、FastAPI 接口 `globe`。webview 按 lucide 图标名画(`lucide-react` 的全套图标),认不出的名字画虚线方块(`square-dashed`)。 + 库规则的种类用规则给的图标(`KindBadge.icon`):SQLAlchemy 数据表 `database`、数据类 `package`、Celery 应用 `server-cog`、定时任务 `calendar-clock`、FastAPI 接口 `globe`。 + + 这组图标由插件自己定义,是一个封闭的集合:契约里的 `KIND_ICON_NAMES`(上表 18 个加库规则用的 5 个,lucide 的 kebab-case 名字),类型 `KindIconName` 是它们的联合,`KindBadge.icon` 只能写其中一个,写别的是类型错误。webview 给每个名字配一个逐个 import 的 lucide 图标组件(`kindIcons.tsx` 的 `KIND_ICON_COMPONENTS`,打包只带这些,预览脚本约 157 kB);webview 的 `KindIconName` 照契约手抄,`kindIconNames.test.ts` 核对两边一样。规则要新图标就把名字加进这组、在 webview 配上图标,没有按任意名字找图标的路。 - 右上角的图标按钮(原型组件 `Reader / BlockTools`,普通块、属性表块、库规则的卡片共用):只有图标、不带字,名字写在悬停提示(`title`)和 `aria-label` 里;整排是 `role="group"`(`aria-label="块操作"`)。先是「问 AI」(sparkles 图标,点开 / 收起块下面的问答区,展开时带按下的底色、`aria-expanded`;文件不在工作区文件夹里时没有,见「问 AI」),再是「编辑 / 预览」切换:预览时铅笔「编辑」,编辑时眼睛「预览」、带按下的底色(`--vscode-toolbar-activeBackground`)。按钮宽 24px、高 20px(和种类图标同高),图标 14px、`descriptionForeground` 色;悬停 `--vscode-toolbar-hoverBackground`,键盘聚焦描 1px `--vscode-focusBorder`;Tab 能走到,回车 / 空格切换。 - 种类图标和底色按种类区分(颜色见「颜色」);注释块虚线框。 - 内容两种状态,每块单独切: @@ -662,8 +664,12 @@ export interface Translator { /** 一种颜色在暗色、亮色主题下各用哪个;高对比主题跟着暗 / 亮走。 */ export interface ThemeColor { dark: string, light: string } -/** 库规则的种类:块左边只画图标(种类色),label 是悬停提示和读屏念的名字,筛选栏里也叫它;icon 是 lucide 图标名(kebab-case,如 database)。 */ -export interface KindBadge { label: string, color: ThemeColor, icon: string } +/** 种类图标:插件自己定义的一组 lucide 图标(kebab-case 名字);前 18 个给内置种类,后面的给库规则。规则要新图标就加进这组,没有按任意名字找图标的路。 */ +export const KIND_ICON_NAMES = ['import', 'square-function', /* …18 个内置种类的… */ 'database', 'package', 'server-cog', 'calendar-clock', 'globe'] as const +export type KindIconName = (typeof KIND_ICON_NAMES)[number] + +/** 库规则的种类:块左边只画图标(种类色),label 是悬停提示和读屏念的名字,筛选栏里也叫它;icon 只能从 KindIconName 里挑。 */ +export interface KindBadge { label: string, color: ThemeColor, icon: KindIconName } /** 库规则认领一块后给出的替身,下面挂着原块留下的子块:规则自己的卡片,或认成的现成属性表块。 */ export type RuleBlock = RuleCard | RuleFacts @@ -1355,7 +1361,7 @@ vscode/src/codeReader/ editor/occurrences.ts 查名字的各处出现(vscode.executeDocumentHighlights) editor/occurrenceNavigation.ts 跳到下一处 / 上一处高亮:命令、上下文键 index.ts registerCodeReader(context): CodeReaderApi -vscode/webview-ui/code-reader.html + src/codeReader/ 预览界面(React,Vite 第二个入口;块右上角的图标按钮在 BlockTools.tsx,块的种类图标(内置种类 → lucide 图标名的唯一一处、按名字画图标)在 kindIcons.tsx,属性表在 PropertyGrid.tsx,子行排成表格还是列表在 subRows.ts,着色的词和框在 SegmentText.tsx,中文名换字在 displayNames.ts,Markdown 渲染在 markdown.ts / TextPreview.tsx,筛选栏在 FilterBar.tsx,筛选规则在 filter.ts,编辑时盖住哪些块、点「编辑」退出哪些块在 editing.ts,高亮各处出现的规则在 occurrences.ts、接到页面上在 useOccurrences.ts,问 AI 的问答区在 AskThread.tsx、状态和规则在 qa.ts、贴图拖图的规则在 qaImages.ts、收发在 useQa.ts) +vscode/webview-ui/code-reader.html + src/codeReader/ 预览界面(React,Vite 第二个入口;块右上角的图标按钮在 BlockTools.tsx,块的种类图标(这组图标名 → 图标组件、内置种类 → 图标名,都只在这一处)在 kindIcons.tsx,属性表在 PropertyGrid.tsx,子行排成表格还是列表在 subRows.ts,着色的词和框在 SegmentText.tsx,中文名换字在 displayNames.ts,Markdown 渲染在 markdown.ts / TextPreview.tsx,筛选栏在 FilterBar.tsx,筛选规则在 filter.ts,编辑时盖住哪些块、点「编辑」退出哪些块在 editing.ts,高亮各处出现的规则在 occurrences.ts、接到页面上在 useOccurrences.ts,问 AI 的问答区在 AskThread.tsx、状态和规则在 qa.ts、贴图拖图的规则在 qaImages.ts、收发在 useQa.ts) ``` ## 测试 @@ -1405,7 +1411,7 @@ vscode/webview-ui/code-reader.html + src/codeReader/ 预览界面(React,Vit - 工作副本(`test/git/reviewWorktree.test.ts`,真的 git:bare 的 origin 上推 `refs/pull/7/head`):拉 PR 的提交建不挂分支的 worktree(父目录不在也建、本机不多出分支),目录手删后同一路径能再建;快进:PR 往前走了且干净 → 快进,已经最新 / 本地在它之上 → 不动,只有未跟踪的文件照样快进,有没提交的改动、本地另有提交 → 不动且改动留着,目录不是工作副本的根 → 报错、外面的仓库不动。 - 复制出什么(`explorer/copyText.test.ts`):文件、目录的名字 / 相对路径 / 绝对路径;不带后缀只去最后一个扩展名(`a.test.ts`、`archive.tar.gz`),`.env` 原样、`.env.local` → `.env`、没有扩展名的原样;相对路径总用 `/`,绝对路径照本机写法(Windows 反斜杠)。 - 块右上角的图标按钮(webview `BlockTools.test.tsx`,渲染成 HTML):一组一个按钮、没有字、`title` 和 `aria-label` 是「编辑」/「预览」、有图标、编辑时带按下标记;给了「问 AI」时它排在前面、只有图标、问答区展开时带按下标记和 `aria-expanded`。 -- 块的种类图标(webview `kindIcons.test.tsx`,渲染成 HTML):内置种类的图标和原型一一对应、都是 lucide 里有的图标;图标名按 kebab-case 认,认不出的画虚线方块;只有图标、没有字,`role="img"`,`title` 和 `aria-label` 是种类名,20px 的框、16px 的图形;库规则的卡片用规则给的图标。各库规则的测试核对 `badge` 的名字、颜色、图标。 +- 块的种类图标(webview `kindIcons.test.tsx`,渲染成 HTML):内置种类的图标和原型一一对应;这组里每个名字画的就是 lucide 里同名的图标;webview 配了图标的名字和契约的 `KIND_ICON_NAMES` 一样、没有多的(`kindIconNames.test.ts`);只有图标、没有字,`role="img"`,`title` 和 `aria-label` 是种类名,20px 的框、16px 的图形;库规则的卡片用规则给的图标。各库规则的测试核对 `badge` 的名字、颜色、图标。 - 问 AI: - 位置记号(`qa/anchor.test.ts`,真实的 Python 解析):有名字 / 没名字的块的记号、同一层重复的编号、两个函数里一样的条件互不影响;上面插几行、改空白记号不变;改别的函数的名字只动它和它里面的块;没名字的块自己的源码改了记号变、有名字的块改签名不变。 - 存法(`qa/qaMap.test.ts`):记一次、删一次、删光不挂 / 不记;文件缺失、解析失败、不是对象当没有;认不出的块和问答跳过、没会话 id 的照认;再问时接着这块最近那条的会话(删了最近的接着剩下里最近的,没问过、没记会话 id 的重新开);用哪个 profile 答的跟着这条存、读得回来,没有的、空的、不是字符串的当没有;手改的顺序、重复的块读进来整理好;写出来排好序、两格缩进、读得回来、删光留 `{}`;改名、挪目录、删除跟着走。图片:文件名跟着这条存、读得回来,跑到别处的路径、不是字符串的名字跳过;删一次问答它的图片跟着删、别的留着;源文件删了图片跟着删,改名、挪目录图片不动。 diff --git a/vscode/src/codeReader/contract.ts b/vscode/src/codeReader/contract.ts index 5486a5f..d3263f2 100644 --- a/vscode/src/codeReader/contract.ts +++ b/vscode/src/codeReader/contract.ts @@ -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 } /** * 库规则认领一块后给出的替身,预览里它代替原来那块,下面挂着原块留下的子块: diff --git a/vscode/src/codeReader/rules/celery.ts b/vscode/src/codeReader/rules/celery.ts index b45d6f9..dccf376 100644 --- a/vscode/src/codeReader/rules/celery.ts +++ b/vscode/src/codeReader/rules/celery.ts @@ -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'] diff --git a/vscode/src/codeReader/rules/dataclass.ts b/vscode/src/codeReader/rules/dataclass.ts index 5b9ce97..5bb8eb2 100644 --- a/vscode/src/codeReader/rules/dataclass.ts +++ b/vscode/src/codeReader/rules/dataclass.ts @@ -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 = { diff --git a/vscode/src/codeReader/rules/fastapi.ts b/vscode/src/codeReader/rules/fastapi.ts index fb88674..a0ccc21 100644 --- a/vscode/src/codeReader/rules/fastapi.ts +++ b/vscode/src/codeReader/rules/fastapi.ts @@ -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'] diff --git a/vscode/src/codeReader/rules/sqlalchemy.ts b/vscode/src/codeReader/rules/sqlalchemy.ts index 4216ad8..ea81cdf 100644 --- a/vscode/src/codeReader/rules/sqlalchemy.ts +++ b/vscode/src/codeReader/rules/sqlalchemy.ts @@ -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 { diff --git a/vscode/webview-ui/src/codeReader/SegmentText.tsx b/vscode/webview-ui/src/codeReader/SegmentText.tsx index fea928f..b1234da 100644 --- a/vscode/webview-ui/src/codeReader/SegmentText.tsx +++ b/vscode/webview-ui/src/codeReader/SegmentText.tsx @@ -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 = { iteration: lucideIcon(KIND_ICONS.loop) } +const GROUP_ICONS: Record = { iteration: KIND_ICON_COMPONENTS[KIND_ICONS.loop] } /** 框起来的一组词:嵌在前后文字里的小框,放不下时整个挪到下一行,比一行还长时在框里换行。 */ function GroupBox({ group }: { group: SegmentGroup }): ReactElement { diff --git a/vscode/webview-ui/src/codeReader/kindIconNames.test.ts b/vscode/webview-ui/src/codeReader/kindIconNames.test.ts new file mode 100644 index 0000000..1e7bfd0 --- /dev/null +++ b/vscode/webview-ui/src/codeReader/kindIconNames.test.ts @@ -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()) + }) +}) diff --git a/vscode/webview-ui/src/codeReader/kindIcons.test.tsx b/vscode/webview-ui/src/codeReader/kindIcons.test.tsx index 944605a..1fe20c4 100644 --- a/vscode/webview-ui/src/codeReader/kindIcons.test.tsx +++ b/vscode/webview-ui/src/codeReader/kindIcons.test.tsx @@ -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, name: string): string { return renderToStaticMarkup() @@ -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 的图形', () => { diff --git a/vscode/webview-ui/src/codeReader/kindIcons.tsx b/vscode/webview-ui/src/codeReader/kindIcons.tsx index 3e5d0a2..1ebdd70 100644 --- a/vscode/webview-ui/src/codeReader/kindIcons.tsx +++ b/vscode/webview-ui/src/codeReader/kindIcons.tsx @@ -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 = { + '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 = { +export const KIND_ICONS: Record = { import: 'import', function: 'square-function', class: 'box', @@ -28,21 +79,13 @@ export const KIND_ICONS: Record = { comment: 'message-square-text', } -const LUCIDE: Record = 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> = 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, 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 ( diff --git a/vscode/webview-ui/src/codeReader/types.ts b/vscode/webview-ui/src/codeReader/types.ts index 14e1559..d4d6937 100644 --- a/vscode/webview-ui/src/codeReader/types.ts +++ b/vscode/webview-ui/src/codeReader/types.ts @@ -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' */