Files
superwork/vscode/webview-ui/src/codeReader/SegmentText.tsx
T

104 lines
4.8 KiB
TypeScript

import type { LucideIcon } from 'lucide-react'
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 { isCurrent, segmentOccurrence, sourceOf } from './occurrences'
import { HighlightContext } from './useOccurrences'
/**
* 按词性给每一段上色,颜色在 codeReader.css 的 .cr-role-* 里按主题切换,带自己颜色(color)的段用它(.cr-colored);带源码下标的名字记在 data-offset 上
* (讲别处名字的段记那个名字的下标,见 sourceOf),跳到定义、高亮各处出现用;是眼下高亮着的名字的一处出现时 data-occurrence 标出底色,是当前那一处(点中的、用键盘跳到的)时
* 再标 data-current。起了中文名的名字显示中文名(data-name),悬停看原名,定义处本身在后面淡淡写原名;下标照旧,跳到定义、高亮照常。
* 框起来的一组词画成小框,框里的词同样处理。
*/
export function SegmentText({ segments }: { segments: Inline[] }): ReactElement {
const highlight = useContext(HighlightContext)
const names = useContext(DisplayNameContext)
return (
<>
{segments.map((piece, index) => {
if (!('role' in piece))
return <GroupBox key={index} group={piece} />
const shown = shownName(piece, names)
return (
<span
key={index}
className={piece.color ? `cr-role-${piece.role} cr-colored` : `cr-role-${piece.role}`}
style={piece.color && { '--color-dark': piece.color.dark, '--color-light': piece.color.light } as CSSProperties}
data-offset={sourceOf(piece)?.start}
data-occurrence={highlight && segmentOccurrence(piece, highlight.occurrences)}
data-current={highlight && isCurrent(piece, highlight) ? '' : undefined}
data-name={shown?.name}
data-tooltip={shown && `原名 ${piece.text}`}
>
{shown ? shown.name : piece.text}
{shown?.definition && <span className="cr-original">{piece.text}</span>}
</span>
)
})}
</>
)
}
/**
* 框的样式 → 框头图标:遍历同「循环」块、按条件同「条件」块、不渲染同「返回」块的图标;条件的一支、小标签、类名不带图标。
* 颜色在 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} 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>
)
}