✨ feat(vscode): 代码阅读的 React 组件卡片:原子图标,名称后面跟具名导出 / 默认导出 / 客户端组件 / 记忆化 / 转发 ref 小标签,属性表按属性一行(类型取字段、默认值、必填、说明,来自 XxxProps 可点,导入的类型经工作区索引查),"use client" 和组件上的 JSDoc 不再单独成块
This commit is contained in:
1 parent
1a32a51e75
commit
15f3a6600d
14 files changed
+666
-34
No files matched your search
@@ -31,3 +31,19 @@ describe('着色的词', () => {
|
||||
expect(html).toBe('<span class="cr-role-string" data-offset="20">/users/</span><span class="cr-role-variable" data-offset="90">user_id</span>')
|
||||
})
|
||||
})
|
||||
|
||||
describe('框起来的一组词', () => {
|
||||
it('小标签只有框头的字,颜色写在 style 上;按条件的前缀只有图标、框里的词照常上色', () => {
|
||||
const tag = renderToStaticMarkup(<SegmentText segments={[{ style: 'tag', label: '具名导出', segments: [], color: { dark: '#c586c0', light: '#af00db' } }]} />)
|
||||
expect(tag).toBe('<span class="cr-group" data-style="tag" data-colored="" style="--group-dark:#c586c0;--group-light:#af00db"><span class="cr-group-head">具名导出</span></span>')
|
||||
const condition = renderToStaticMarkup(<SegmentText segments={[{ style: 'condition', label: '', segments: [{ text: '当', role: 'keyword' }] }]} />)
|
||||
expect(condition).toMatch(/^<span class="cr-group" data-style="condition"><span class="cr-group-head" data-bare=""><svg[^>]*>.*<\/svg><\/span><span class="cr-role-keyword">当<\/span><\/span>$/)
|
||||
const branch = renderToStaticMarkup(<SegmentText segments={[{ style: 'branch', label: '', segments: [{ text: '是', role: 'keyword' }] }]} />)
|
||||
expect(branch).toBe('<span class="cr-group" data-style="branch"><span class="cr-role-keyword">是</span></span>')
|
||||
})
|
||||
|
||||
it('收起来的类名是个按钮:悬停提示是全部类名,收着时不弹层', () => {
|
||||
const html = renderToStaticMarkup(<SegmentText segments={[{ style: 'classes', label: '样式 2 个类', segments: [], detail: 'flex gap-2' }]} />)
|
||||
expect(html).toBe('<span class="cr-classes"><button type="button" class="cr-group" data-style="classes" aria-expanded="false" data-tooltip="flex gap-2"><span class="cr-group-head">样式 2 个类</span></button></span>')
|
||||
})
|
||||
})
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { LucideIcon } from 'lucide-react'
|
||||
import type { CSSProperties, ReactElement } from 'react'
|
||||
import type { GroupStyle, Inline, SegmentGroup } from './types'
|
||||
import { useContext } from 'react'
|
||||
import { useContext, useState } from 'react'
|
||||
import { DisplayNameContext, shownName } from './displayNames'
|
||||
import { KIND_ICON_COMPONENTS, KIND_ICONS } from './kindIcons'
|
||||
import { isCurrent, segmentOccurrence, sourceOf } from './occurrences'
|
||||
@@ -42,19 +42,62 @@ export function SegmentText({ segments }: { segments: Inline[] }): ReactElement
|
||||
)
|
||||
}
|
||||
|
||||
/** 框的样式 → 框头图标:遍历同「循环」块的图标;颜色在 codeReader.css 的 .cr-group[data-style] 里。 */
|
||||
const GROUP_ICONS: Record<GroupStyle, LucideIcon> = { iteration: KIND_ICON_COMPONENTS[KIND_ICONS.loop] }
|
||||
/**
|
||||
* 框的样式 → 框头图标:遍历同「循环」块、按条件同「条件」块、不渲染同「返回」块的图标;条件的一支、小标签、类名不带图标。
|
||||
* 颜色在 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],
|
||||
branch: undefined,
|
||||
skip: KIND_ICON_COMPONENTS[KIND_ICONS.return],
|
||||
tag: undefined,
|
||||
classes: undefined,
|
||||
}
|
||||
|
||||
/** 框起来的一组词:嵌在前后文字里的小框,放不下时整个挪到下一行,比一行还长时在框里换行。 */
|
||||
/** 框自己的颜色,暗色、亮色两个都写在 style 上,CSS 按主题挑一个(见 .cr-group[data-colored])。 */
|
||||
function groupColor(group: SegmentGroup): CSSProperties | undefined {
|
||||
return group.color && { '--group-dark': group.color.dark, '--group-light': group.color.light } as CSSProperties
|
||||
}
|
||||
|
||||
/**
|
||||
* 框起来的一组词:嵌在前后文字里的小框,放不下时整个挪到下一行,比一行还长时在框里换行。框头是图标和 label,只有图标、只有字、都没有都行;
|
||||
* 收起来的类名另见 ClassesChip。
|
||||
*/
|
||||
function GroupBox({ group }: { group: SegmentGroup }): ReactElement {
|
||||
if (group.style === 'classes')
|
||||
return <ClassesChip group={group} />
|
||||
const Icon = GROUP_ICONS[group.style]
|
||||
return (
|
||||
<span className="cr-group" data-style={group.style}>
|
||||
<span className="cr-group-head">
|
||||
<Icon size={12} strokeWidth={2} aria-hidden />
|
||||
{group.label}
|
||||
</span>
|
||||
<span className="cr-group" data-style={group.style} data-colored={group.color ? '' : undefined} style={groupColor(group)}>
|
||||
{(Icon || group.label) && (
|
||||
<span className="cr-group-head" data-bare={group.label ? undefined : ''}>
|
||||
{Icon && <Icon size={12} strokeWidth={2} aria-hidden />}
|
||||
{group.label}
|
||||
</span>
|
||||
)}
|
||||
<SegmentText segments={group.segments} />
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
/** 收起来的一串类名(「样式 N 个类」):悬停看全部类名,点一下在它下面弹出、盖住后面的字,再点收起。 */
|
||||
function ClassesChip({ group }: { group: SegmentGroup }): ReactElement {
|
||||
const [open, setOpen] = useState(false)
|
||||
return (
|
||||
<span className="cr-classes">
|
||||
<button
|
||||
type="button"
|
||||
className="cr-group"
|
||||
data-style="classes"
|
||||
data-open={open ? '' : undefined}
|
||||
aria-expanded={open}
|
||||
data-tooltip={open ? undefined : group.detail}
|
||||
onClick={() => setOpen(value => !value)}
|
||||
>
|
||||
<span className="cr-group-head">{group.label}</span>
|
||||
</button>
|
||||
{open && <span className="cr-classes-popup" role="note">{group.detail}</span>}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
@@ -381,6 +381,73 @@ body:is(.vscode-light, .vscode-high-contrast-light) .cr-colored { color: var(--c
|
||||
color: var(--group);
|
||||
white-space: nowrap;
|
||||
}
|
||||
/* 只有图标的框头(渲染大纲一行前面的「当 … 时」):图标和后面的字贴近一点;框里没有词时(小标签、类名)框头后面不留空 */
|
||||
.cr-group-head[data-bare] {
|
||||
margin-right: 4px;
|
||||
}
|
||||
.cr-group-head:last-child {
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
/*
|
||||
* 渲染大纲一行前面的小框(React 组件卡片):按条件、条件的一支同「条件」块的颜色,不渲染同「返回」块;
|
||||
* 小标签(名称后面的「具名导出」、属性名后面的「回调」)用规则给的颜色(暗色、亮色各一个,按主题挑)
|
||||
*/
|
||||
.cr-group:is([data-style="condition"], [data-style="branch"]) { --group: var(--cr-kind-branch); }
|
||||
.cr-group[data-style="skip"] { --group: var(--cr-kind-return); }
|
||||
.cr-group[data-colored] { --group: var(--group-dark); }
|
||||
body:is(.vscode-light, .vscode-high-contrast-light) .cr-group[data-colored] { --group: var(--group-light); }
|
||||
.cr-group[data-style="tag"] {
|
||||
line-height: 16px;
|
||||
margin-top: 1px;
|
||||
}
|
||||
|
||||
/*
|
||||
* 收起来的一串类名(「样式 N 个类」):中性色的小框,是个按钮;点开时换成焦点色的边、在下面弹出全部类名,
|
||||
* 弹层盖住后面的字,不挤开它们
|
||||
*/
|
||||
.cr-group[data-style="classes"] {
|
||||
margin: 0;
|
||||
font: inherit;
|
||||
color: inherit;
|
||||
cursor: pointer;
|
||||
background: var(--vscode-input-background);
|
||||
border-color: var(--vscode-editorWidget-border, var(--vscode-contrastBorder, transparent));
|
||||
}
|
||||
.cr-group[data-style="classes"] .cr-group-head {
|
||||
font-weight: 400;
|
||||
color: var(--vscode-descriptionForeground);
|
||||
}
|
||||
.cr-group[data-style="classes"]:hover,
|
||||
.cr-group[data-style="classes"][data-open] {
|
||||
border-color: var(--vscode-focusBorder);
|
||||
}
|
||||
.cr-group[data-style="classes"][data-open] .cr-group-head {
|
||||
color: var(--vscode-foreground);
|
||||
}
|
||||
.cr-classes {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
vertical-align: top;
|
||||
}
|
||||
.cr-classes-popup {
|
||||
position: absolute;
|
||||
top: calc(100% + 2px);
|
||||
left: 0;
|
||||
z-index: 20;
|
||||
width: max-content;
|
||||
max-width: min(460px, 70vw);
|
||||
padding: 6px 8px;
|
||||
border: 1px solid var(--vscode-editorHoverWidget-border, var(--vscode-editorWidget-border));
|
||||
border-radius: 6px;
|
||||
background: var(--vscode-editorHoverWidget-background, var(--vscode-editorWidget-background));
|
||||
box-shadow: 0 2px 8px var(--vscode-widget-shadow, rgb(0 0 0 / 36%));
|
||||
color: var(--vscode-editorHoverWidget-foreground, var(--vscode-foreground));
|
||||
font-family: var(--vscode-editor-font-family, monospace);
|
||||
font-size: 12px;
|
||||
line-height: 18px;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
/*
|
||||
* 属性表里带列名的子行(输入参数、数据表的字段)排成的表格:外框圆角,格子之间、行之间一条主题的表格分隔线;
|
||||
|
||||
@@ -3,6 +3,7 @@ import type { ReactElement } from 'react'
|
||||
import type { BlockKind, BlockView, KindIconName } from './types'
|
||||
import {
|
||||
Activity,
|
||||
Atom,
|
||||
Box,
|
||||
Braces,
|
||||
CalendarClock,
|
||||
@@ -17,6 +18,7 @@ import {
|
||||
ListOrdered,
|
||||
Lock,
|
||||
MessageSquareText,
|
||||
MousePointerClick,
|
||||
Network,
|
||||
Package,
|
||||
Play,
|
||||
@@ -34,6 +36,7 @@ import {
|
||||
Terminal,
|
||||
TriangleAlert,
|
||||
Variable,
|
||||
Zap,
|
||||
} from 'lucide-react'
|
||||
|
||||
/** 插件定义的这组种类图标,每个名字一个 lucide 图标(逐个 import,打包只带这些)。 */
|
||||
@@ -70,6 +73,9 @@ export const KIND_ICON_COMPONENTS: Record<KindIconName, LucideIcon> = {
|
||||
'calendar-clock': CalendarClock,
|
||||
'globe': Globe,
|
||||
'network': Network,
|
||||
'atom': Atom,
|
||||
'zap': Zap,
|
||||
'mouse-pointer-click': MousePointerClick,
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -28,14 +28,23 @@ export interface Segment { text: string, role: TokenRole, offset?: number, refer
|
||||
/** 规则自己查得到定义的一个名字:哪个规则、规则自己认的键 */
|
||||
export interface RuleReference { rule: string, key: string }
|
||||
|
||||
/** 框的样式,决定框头的图标和框的颜色:'iteration' 是写在表达式里的遍历,图标、颜色同「循环」块 */
|
||||
export type GroupStyle = 'iteration'
|
||||
/**
|
||||
* 框的样式,决定框头的图标和框的颜色:iteration 写在表达式里的遍历(同「循环」块),condition 按条件(同「条件」块),
|
||||
* branch 条件的一支(同「条件」块的颜色、不带图标),skip 这个条件下不渲染(同「返回」块),tag 小标签(颜色是 SegmentGroup.color),
|
||||
* classes 收起来的一串类名(中性色,点开、悬停看 detail)
|
||||
*/
|
||||
export type GroupStyle = 'iteration' | 'condition' | 'branch' | 'skip' | 'tag' | 'classes'
|
||||
|
||||
/** 框起来的一组词:画成嵌在前后文字里的一个小框,框头是图标和 label;框里的词照样带源码下标,还能再套框 */
|
||||
/**
|
||||
* 框起来的一组词:画成嵌在前后文字里的一个小框,框头是图标和 label(只有图标时 label 是空的);框里的词照样带源码下标,还能再套框。
|
||||
* color 盖过样式的颜色(tag 按标签的意思上色),detail 是点开、悬停时看的全文(classes 的全部类名,一行一条)
|
||||
*/
|
||||
export interface SegmentGroup {
|
||||
style: GroupStyle
|
||||
label: string
|
||||
segments: Inline[]
|
||||
color?: ThemeColor
|
||||
detail?: string
|
||||
}
|
||||
|
||||
/** 译文里的一段:着色的词,或框起来的一组词 */
|
||||
@@ -90,7 +99,7 @@ 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' | 'terminal' | 'sliders-horizontal' | 'signpost' | 'square-terminal' | 'layers' | 'copy' | 'activity' | 'play'
|
||||
| 'database' | 'package' | 'server-cog' | 'calendar-clock' | 'globe' | 'network'
|
||||
| 'database' | 'package' | 'server-cog' | 'calendar-clock' | 'globe' | 'network' | 'atom' | 'zap' | 'mouse-pointer-click'
|
||||
|
||||
/** 库规则的种类:名字(悬停提示、读屏念的文字)、颜色和图标(从 KindIconName 里挑);块左边只画图标 */
|
||||
export interface KindBadge { label: string, color: ThemeColor, icon: KindIconName }
|
||||
|
||||
Reference in new issue
Block a user