✨ feat(vscode): 代码阅读卡片横排多个概念图标
This commit is contained in:
1 parent
11fcbef225
commit
394cd2fc1c
19 files changed
+328
-99
No files matched your search
@@ -15,7 +15,21 @@ vi.mock('./vscode', () => ({ postMessage: () => {}, onMessage: () => () => {}, v
|
||||
const BADGE: KindBadge = { label: 'React 组件', color: { dark: '#61dafb', light: '#0a7ea4' }, icon: 'atom' }
|
||||
|
||||
function view(id: string, kind: string, text: string, extra: Partial<BlockView> = {}): BlockView {
|
||||
return { id, anchor: id, kind, startLine: 0, endLine: 0, code: '', codeTokens: [], attributes: { kind }, children: [], translation: [{ text, role: 'plain' }], ...extra } as BlockView
|
||||
const badge = extra.badge
|
||||
return {
|
||||
id,
|
||||
anchor: id,
|
||||
kind,
|
||||
startLine: 0,
|
||||
endLine: 0,
|
||||
code: '',
|
||||
codeTokens: [],
|
||||
attributes: { kind },
|
||||
concepts: [{ id: kind, label: badge?.label ?? kind, icon: badge?.icon ?? 'box', ...(badge && { color: badge.color }) }],
|
||||
children: [],
|
||||
translation: [{ text, role: 'plain' }],
|
||||
...extra,
|
||||
} as BlockView
|
||||
}
|
||||
|
||||
/** 一张组件卡片:第 1 行下面放着一个副作用,第 2 行的两个子行各讲一个处理函数,后面还有一个留下的注释块。 */
|
||||
|
||||
@@ -251,7 +251,7 @@ function BlockCard({ block, tree }: { block: BlockView, tree: BlockTreeState }):
|
||||
</button>
|
||||
)
|
||||
: <span className="w-3.5 shrink-0" />}
|
||||
<KindIcon block={block} name={tree.kindName(block.kind)} />
|
||||
<KindIcon concepts={block.concepts} />
|
||||
<div className="min-w-0 flex-1">
|
||||
{editing
|
||||
? (
|
||||
@@ -303,7 +303,7 @@ function RemovedCard({ placed, tree }: { placed: PlacedRemoved, tree: BlockTreeS
|
||||
>
|
||||
<Chevron size={14} strokeWidth={1.8} />
|
||||
</button>
|
||||
<KindIcon block={block} name={kindName} />
|
||||
<KindIcon concepts={block.concepts} />
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="cr-removed-head cursor-pointer truncate text-[12.5px] leading-5" onClick={toggle}>
|
||||
<span className="cr-removed-label">已删除 · </span>
|
||||
|
||||
@@ -3,7 +3,7 @@ import type { CSSProperties, ReactElement } from 'react'
|
||||
import type { GroupStyle, Inline, SegmentGroup } from './types'
|
||||
import { useContext, useState } from 'react'
|
||||
import { DisplayNameContext, shownName } from './displayNames'
|
||||
import { KIND_ICON_COMPONENTS, KIND_ICONS } from './kindIcons'
|
||||
import { KIND_ICON_COMPONENTS } from './kindIcons'
|
||||
import { isCurrent, segmentOccurrence, sourceOf } from './occurrences'
|
||||
import { HighlightContext } from './useOccurrences'
|
||||
|
||||
@@ -47,10 +47,10 @@ export function SegmentText({ segments }: { segments: Inline[] }): ReactElement
|
||||
* 颜色在 codeReader.css 的 .cr-group[data-style] 里,框自己给了颜色(color)时用它。
|
||||
*/
|
||||
const GROUP_ICONS: Record<GroupStyle, LucideIcon | undefined> = {
|
||||
iteration: KIND_ICON_COMPONENTS[KIND_ICONS.loop],
|
||||
condition: KIND_ICON_COMPONENTS[KIND_ICONS.if],
|
||||
iteration: KIND_ICON_COMPONENTS.repeat,
|
||||
condition: KIND_ICON_COMPONENTS.split,
|
||||
branch: undefined,
|
||||
skip: KIND_ICON_COMPONENTS[KIND_ICONS.return],
|
||||
skip: KIND_ICON_COMPONENTS['corner-down-left'],
|
||||
tag: undefined,
|
||||
classes: undefined,
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ import { describe, expect, it } from 'vitest'
|
||||
import { changeStops, diffTree, foldUnchanged, nextStop, openedByDiff, removedTitle, statsParts } from './diffView'
|
||||
|
||||
function block(id: string, anchor: string, children: BlockView[] = [], extra: Partial<BlockView> = {}): BlockView {
|
||||
return { id, anchor, kind: 'function', startLine: 0, endLine: 0, code: '', codeTokens: [], attributes: { kind: 'function' }, children, translation: [], ...extra } as BlockView
|
||||
return { id, anchor, kind: 'function', startLine: 0, endLine: 0, code: '', codeTokens: [], attributes: { kind: 'function' }, concepts: [{ id: 'function', label: '函数', icon: 'square-function' }], children, translation: [], ...extra } as BlockView
|
||||
}
|
||||
|
||||
function removed(parent: string, after: string | undefined, kind = 'function'): RemovedView {
|
||||
|
||||
@@ -4,7 +4,7 @@ import { coveredByEditing, startEditing } from './editing'
|
||||
import { revealPlan, uncoveredPlan } from './reveal'
|
||||
|
||||
function block(id: string, kind: string, [startLine, endLine]: [number, number], children: BlockView[] = []): BlockView {
|
||||
return { id, kind, startLine, endLine, code: '', codeTokens: [], attributes: { kind }, children, properties: [] }
|
||||
return { id, kind, startLine, endLine, code: '', codeTokens: [], attributes: { kind }, concepts: [{ id: kind, label: kind, icon: 'box' }], children, properties: [] }
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -3,7 +3,7 @@ import { describe, expect, it } from 'vitest'
|
||||
import { applyFilter, blockPath, excludedBy, facetLabel, facetsOf, keptWhileEditing, setChecked, toggleValue } from './filter'
|
||||
|
||||
function block(id: string, kind: BlockKind, extra: Record<string, string> = {}, children: BlockView[] = []): BlockView {
|
||||
return { id, kind, startLine: 0, endLine: 0, code: '', codeTokens: [], attributes: { kind, ...extra }, children, translation: [] }
|
||||
return { id, kind, startLine: 0, endLine: 0, code: '', codeTokens: [], attributes: { kind, ...extra }, concepts: [{ id: kind, label: kind, icon: 'box' }], children, translation: [] }
|
||||
}
|
||||
|
||||
const NONE = new Set<string>()
|
||||
|
||||
@@ -1,10 +1,15 @@
|
||||
import type { BlockView, KindIconName } from './types'
|
||||
import type { BlockConcept, KindIconName } from './types'
|
||||
import { renderToStaticMarkup } from 'react-dom/server'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { KIND_ICON_COMPONENTS, KIND_ICONS, KindIcon } from './kindIcons'
|
||||
import { KIND_ICONS } from '../../../src/codeReader/contract'
|
||||
import { KIND_ICON_COMPONENTS, KindIcon } from './kindIcons'
|
||||
|
||||
function render(block: Pick<BlockView, 'kind' | 'badge'>, name: string): string {
|
||||
return renderToStaticMarkup(<KindIcon block={block} name={name} />)
|
||||
function render(concepts: readonly BlockConcept[]): string {
|
||||
return renderToStaticMarkup(<KindIcon concepts={concepts} />)
|
||||
}
|
||||
|
||||
function concept(id: string, label: string, icon: KindIconName, color?: BlockConcept['color']): BlockConcept {
|
||||
return { id, label, icon, ...(color && { color }) }
|
||||
}
|
||||
|
||||
/** 标签之间的字:图标(svg)没有字 */
|
||||
@@ -49,23 +54,44 @@ describe('块的种类图标', () => {
|
||||
|
||||
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)
|
||||
const html = render([concept(name, name, name)])
|
||||
expect(html, name).toContain(`class="lucide lucide-${name}"`)
|
||||
}
|
||||
})
|
||||
|
||||
it('只画图标不带字,名字在悬停提示(data-tooltip,不用原生 title)和 aria-label 里;20px 的框、16px 的图形', () => {
|
||||
const html = render({ kind: 'function' }, '函数')
|
||||
it('只画图标不带字,名字在悬停提示(data-tooltip,不用原生 title)里;读屏念这个名字;20px 的框、16px 的图形', () => {
|
||||
const html = render([concept('function', '函数', 'square-function')])
|
||||
expect(visibleText(html)).toBe('')
|
||||
expect(html).toMatch(/^<span class="cr-kind [^"]*size-5[^"]*" role="img" data-tooltip="函数" aria-label="函数"><svg/)
|
||||
expect(html).toContain('role="img" aria-label="函数"')
|
||||
expect(html).toContain('data-tooltip="函数"')
|
||||
expect(html).toContain('data-kind="function"')
|
||||
expect(html).not.toContain(' title=')
|
||||
expect(html).toContain('lucide-square-function')
|
||||
expect(html).toContain('width="16" height="16"')
|
||||
expect(html).toContain('size-5')
|
||||
})
|
||||
|
||||
it('库规则的卡片用规则给的图标,名字是规则给的', () => {
|
||||
const html = render({ kind: 'sqlalchemy.table', badge: { label: '数据表', color: { dark: '#e8a860', light: '#a3590d' }, icon: 'database' } }, '数据表')
|
||||
expect(html).toContain('lucide-database')
|
||||
expect(html).toContain('data-tooltip="数据表" aria-label="数据表"')
|
||||
it('认到几个就横排几个:悬停各自的名字,读屏把名字连起来念;规则的图标用规则给的颜色', () => {
|
||||
const html = render([
|
||||
concept('class', '类', 'box'),
|
||||
concept('dataclasses.dataclass', '数据类', 'package', { dark: '#ef7fb0', light: '#b3306c' }),
|
||||
concept('dataclasses.frozen', '不可修改', 'lock', { dark: '#9cdcfe', light: '#001080' }),
|
||||
])
|
||||
expect(visibleText(html)).toBe('')
|
||||
expect(html).toContain('aria-label="类,数据类,不可修改"')
|
||||
expect(html).toContain('data-tooltip="类"')
|
||||
expect(html).toContain('data-tooltip="数据类"')
|
||||
expect(html).toContain('data-tooltip="不可修改"')
|
||||
expect(html).toContain('data-kind="class"')
|
||||
expect(html).toContain('lucide-box')
|
||||
expect(html).toContain('lucide-package')
|
||||
expect(html).toContain('lucide-lock')
|
||||
expect(html).toContain('--kind-dark:#ef7fb0')
|
||||
expect(html).toContain('--kind-light:#b3306c')
|
||||
expect(html).toContain('--kind-dark:#9cdcfe')
|
||||
expect(html).toContain('--kind-light:#001080')
|
||||
expect(html).not.toContain(' title=')
|
||||
expect(html.match(/size-5/g)).toHaveLength(3)
|
||||
expect(html).toContain('gap-0.5')
|
||||
})
|
||||
})
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { LucideIcon } from 'lucide-react'
|
||||
import type { ReactElement } from 'react'
|
||||
import type { BlockKind, BlockView, KindIconName } from './types'
|
||||
import type { CSSProperties, ReactElement } from 'react'
|
||||
import type { BlockConcept, KindIconName } from './types'
|
||||
import {
|
||||
Activity,
|
||||
Atom,
|
||||
@@ -79,53 +79,24 @@ export const KIND_ICON_COMPONENTS: Record<KindIconName, LucideIcon> = {
|
||||
}
|
||||
|
||||
/**
|
||||
* 内置种类的图标:块左边只画图标,名字是悬停提示。
|
||||
* 同一类语义共用一个颜色(codeReader.css 的 --cr-kind-*:常量 / 变量 / 字段都是数据,尝试 / 捕获都是异常处理),图标区分具体是哪种。
|
||||
* 卡片左边的概念图标,横排,各 20px,间距紧。图标名由宿主放进概念列表,这里只按名字画。
|
||||
* 没有颜色的是语言的种类,按 id 用 CSS 的种类色;有颜色的只给这个图标上色。
|
||||
* 每个图标悬停自己的名字(data-tooltip,见 components/ui/tooltip.tsx);读屏把名字连起来念。
|
||||
*/
|
||||
export const KIND_ICONS: Record<BlockKind, KindIconName> = {
|
||||
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',
|
||||
interpreter: 'terminal',
|
||||
options: 'sliders-horizontal',
|
||||
case: 'signpost',
|
||||
command: 'square-terminal',
|
||||
pipeline: 'square-terminal',
|
||||
directive: 'terminal',
|
||||
stage: 'layers',
|
||||
run: 'square-terminal',
|
||||
copy: 'copy',
|
||||
setting: 'sliders-horizontal',
|
||||
label: 'tag',
|
||||
health: 'activity',
|
||||
start: 'play',
|
||||
}
|
||||
|
||||
/**
|
||||
* 块左边的种类:只画图标,和一行字等高(20px 的框、16px 的图形),颜色是种类色(--kind)。图标是库规则给的(badge.icon),
|
||||
* 没有就是内置种类的(KIND_ICONS,没带 badge 的块 kind 是 BlockKind);名字(翻译插件给种类起的,库规则的种类是规则给的)
|
||||
* 写在悬停提示(data-tooltip,见 components/ui/tooltip.tsx)和读屏念的 aria-label 里。
|
||||
*/
|
||||
export function KindIcon({ block, name }: { block: Pick<BlockView, 'kind' | 'badge'>, name: string }): ReactElement {
|
||||
const Icon = KIND_ICON_COMPONENTS[block.badge?.icon ?? KIND_ICONS[block.kind as BlockKind]]
|
||||
export function KindIcon({ concepts }: { concepts: readonly BlockConcept[] }): ReactElement {
|
||||
return (
|
||||
<span className="cr-kind flex size-5 shrink-0 items-center justify-center" role="img" data-tooltip={name} aria-label={name}>
|
||||
<Icon size={16} strokeWidth={2.2} aria-hidden />
|
||||
<span className="flex shrink-0 items-center gap-0.5" role="img" aria-label={concepts.map(concept => concept.label).join(',')}>
|
||||
{concepts.map((concept) => {
|
||||
const Icon = KIND_ICON_COMPONENTS[concept.icon]
|
||||
const tint = concept.color
|
||||
? { 'data-badge': '' as const, 'style': { '--kind-dark': concept.color.dark, '--kind-light': concept.color.light } as CSSProperties }
|
||||
: { 'data-kind': concept.id }
|
||||
return (
|
||||
<span key={concept.id} className="cr-kind flex size-5 shrink-0 items-center justify-center" data-tooltip={concept.label} aria-hidden {...tint}>
|
||||
<Icon size={16} strokeWidth={2.2} aria-hidden />
|
||||
</span>
|
||||
)
|
||||
})}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
@@ -21,11 +21,11 @@ function word(text: string): Segment {
|
||||
}
|
||||
|
||||
function textBlock(id: string, kind: BlockKind, startLine: number, translation: Inline[], children: BlockView[] = []): BlockView {
|
||||
return { id, kind, startLine, endLine: startLine, code: '', codeTokens: [], attributes: { kind }, children, translation }
|
||||
return { id, kind, startLine, endLine: startLine, code: '', codeTokens: [], attributes: { kind }, concepts: [{ id: kind, label: kind, icon: 'box' }], children, translation }
|
||||
}
|
||||
|
||||
function factBlock(id: string, kind: BlockKind, startLine: number, properties: PropertyRow[], children: BlockView[] = []): BlockView {
|
||||
return { id, kind, startLine, endLine: startLine, code: '', codeTokens: [], attributes: { kind }, children, properties }
|
||||
return { id, kind, startLine, endLine: startLine, code: '', codeTokens: [], attributes: { kind }, concepts: [{ id: kind, label: kind, icon: 'box' }], children, properties }
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -127,7 +127,7 @@ describe('跳到定义:滚到哪块、展开哪些', () => {
|
||||
|
||||
it('注释块、函数「说明」的 Markdown 里的名字不带下标,不算显示着定义', () => {
|
||||
const blocks: BlockView[] = [
|
||||
{ id: '0', kind: 'comment', startLine: 0, endLine: 0, code: '', codeTokens: [], attributes: { kind: 'comment' }, children: [], translation: { markdown: '见 `get_db`' } },
|
||||
{ id: '0', kind: 'comment', startLine: 0, endLine: 0, code: '', codeTokens: [], attributes: { kind: 'comment' }, concepts: [{ id: 'comment', label: '注释', icon: 'message-square-text' }], children: [], translation: { markdown: '见 `get_db`' } },
|
||||
factBlock('1', 'function', 1, [
|
||||
{ label: '说明', value: { markdown: '同 `get_db`' } },
|
||||
{ label: '输入参数', value: [{ text: '1 个', role: 'hint' }], items: [{ cells: [[name('get_db', 30)]] }] },
|
||||
|
||||
@@ -101,7 +101,18 @@ export type KindIconName
|
||||
| 'message-square-text' | 'terminal' | 'sliders-horizontal' | 'signpost' | 'square-terminal' | 'layers' | 'copy' | 'activity' | 'play'
|
||||
| 'database' | 'package' | 'server-cog' | 'calendar-clock' | 'globe' | 'network' | 'atom' | 'zap' | 'mouse-pointer-click'
|
||||
|
||||
/** 库规则的种类:名字(悬停提示、读屏念的文字)、颜色和图标(从 KindIconName 里挑);块左边只画图标 */
|
||||
/**
|
||||
* 一块上认出的一个概念。宿主放进预览数据:稳定 id、悬停名、图标名。
|
||||
* 颜色不填的是语言的种类,按 id 用 CSS 的种类色;填了的只给这个图标上色。
|
||||
*/
|
||||
export interface BlockConcept {
|
||||
id: string
|
||||
label: string
|
||||
icon: KindIconName
|
||||
color?: ThemeColor
|
||||
}
|
||||
|
||||
/** 库规则的种类:筛选栏里叫 label,卡片边框用 color。图标在 concepts 里,不单画这一个。 */
|
||||
export interface KindBadge { label: string, color: ThemeColor, icon: KindIconName }
|
||||
|
||||
interface BlockViewBase {
|
||||
@@ -109,7 +120,7 @@ interface BlockViewBase {
|
||||
id: string
|
||||
/** 块的位置记号:问答挂在它上面(QaThreadView.anchor),文件别处改了照样对得上;webview 只拿它比相等,不拆开看 */
|
||||
anchor: string
|
||||
/** BlockKind,或库规则认领的块的种类 id(带点,例如 sqlalchemy.table);webview 不认识后者,只按 badge 画 */
|
||||
/** BlockKind,或库规则认领的块的种类 id(带点,例如 sqlalchemy.table)。筛选用它;图标不看它,看 concepts */
|
||||
kind: string
|
||||
/** 编辑范围的起止行,0 起:块的完整源码,头部连同各子孙块、结尾(块编辑时子孙都不画,见 coveredByEditing) */
|
||||
startLine: number
|
||||
@@ -122,7 +133,9 @@ interface BlockViewBase {
|
||||
defines?: SourceRange
|
||||
/** 筛选属性:属性 id → 值 id(含 kind),都是和语言无关的英文 id,显示名在 DocumentMessage.attributes 里 */
|
||||
attributes: Record<string, string>
|
||||
/** 库规则认领的块才有:种类的名字、颜色和图标,由规则给;和类块一样默认收起子块 */
|
||||
/** 这块认出来的概念,从左到右。webview 按图标名画,不自己用种类查 */
|
||||
concepts: BlockConcept[]
|
||||
/** 库规则的卡片才有:卡片边框的颜色,筛选栏里这个种类的名字。和类块一样默认收起子块 */
|
||||
badge?: KindBadge
|
||||
/** 库规则放进父卡片里的子块才有:画在父卡片属性表第 row 行下面,不排在父卡片的子块里;item 见 BlockSlot */
|
||||
slot?: BlockSlot
|
||||
|
||||
@@ -15,7 +15,7 @@ function memoryStorage(initial?: unknown): ViewStateStorage {
|
||||
}
|
||||
|
||||
function block(id: string, kind: BlockKind, children: BlockView[] = []): BlockView {
|
||||
return { id, kind, startLine: 0, endLine: 0, code: '', codeTokens: [], attributes: { kind }, children, translation: [] }
|
||||
return { id, kind, startLine: 0, endLine: 0, code: '', codeTokens: [], attributes: { kind }, concepts: [{ id: kind, label: kind, icon: 'box' }], children, translation: [] }
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in new issue
Block a user