import type { LucideIcon } from 'lucide-react' import type { CSSProperties, ReactElement } from 'react' import type { NameMap } from './displayNames' import type { Highlight, OccurrenceStyle } from './occurrences' import type { Placement } from './placement' import type { ReadableStore } from './store' import type { GroupStyle, Inline, Segment, SegmentGroup, ShownName } from './types' import { ChevronDown, ChevronRight } from 'lucide-react' import { createContext, memo, useContext, useState } from 'react' import { ChainFoldContext } from './chainFold' import { sameShownName, shownName } from './displayNames' import { KIND_ICON_COMPONENTS } from './kindIcons' import { isCurrent, segmentOccurrence, sourceOf } from './occurrences' import { NO_PLACEMENT } from './placement' import { staticStore, useStore } from './store' /** * 页面上文字的标记:中文名(文件里的下标 → 显示成什么),眼下高亮着的名字(没有高亮时没有),和各块在文件里的位置 * (文字里的下标从所在那块的开头算,按它换成文件里的下标再去查)。 */ export interface TextMarks { names: NameMap highlight?: Highlight placement: Placement } /** * 文字的标记从哪份 store 读:页面上是 App 的那份(中文名、单击高亮都记在里面);定义卡片浮窗是那个文件的中文名,钉住的行不画高亮。 * 每段文字只订阅自己那几段的标记。 */ export const TextMarksContext = createContext>(staticStore({ names: new Map(), placement: NO_PLACEMENT })) /** 文字在哪块的卡片里(块 id):卡片、钉住的卡片头按它给里面的文字;卡片外面的文字(编辑框的着色)不带下标,没有 */ export const BlockIdContext = createContext(undefined) /** * 按词性给每一段上色,颜色在 codeReader.css 的 .cr-role-* 里按主题切换,带自己颜色(color)的段用它(.cr-colored);带下标的名字把块里的下标记在 data-offset 上 * (讲别处名字的段记那个名字的下标,见 sourceOf;换成文件里的见 nameOffset),跳到定义、高亮各处出现用;是眼下高亮着的名字的一处出现时 data-occurrence 标出底色,是当前那一处(点中的、用键盘跳到的)时 * 再标 data-current。起了中文名的名字显示中文名(data-name),悬停看原名,定义处本身在后面淡淡写原名;下标照旧,跳到定义、高亮照常。 * 带悬停提示(tooltip)的段悬停看它。 * 框起来的一组词画成小框,框里的词同样处理。 * 中文名和高亮只订阅自己这几段的(TextMarksContext),按所在那块(BlockIdContext)在文件里的位置查:中文名到了、换了高亮的名字,只有字或底色变了的那几串文字重画; * 块只是挪了位置时查出来的还是那些,不重画。 */ export const SegmentText = memo(({ segments }: { segments: Inline[] }): ReactElement => { const block = useContext(BlockIdContext) const marks = useStore(useContext(TextMarksContext), (current) => { const base = block === undefined ? undefined : current.placement.of(block)?.offset return segments.map(piece => 'role' in piece && base !== undefined ? markOf(piece, base, current) : undefined) }, sameMarks) return ( <> {segments.map((piece, index) => { if (!('role' in piece)) return const mark = marks[index] const shown = mark?.shown return ( {shown ? shown.name : piece.text} {shown?.definition && {piece.text}} ) })} ) }) SegmentText.displayName = 'SegmentText' /** 一段的标记:显示成的中文名,高亮里画哪种底色、是不是当前那一处;都没有时没有。 */ interface SegmentMark { shown?: ShownName occurrence?: OccurrenceStyle current: boolean } /** 一段的标记;段里的下标从所在那块的开头算,那块的开头在文件里的下标是 base。 */ function markOf(piece: Segment, base: number, { names, highlight }: TextMarks): SegmentMark | undefined { const shown = shownName(piece, base, names) const occurrence = highlight && segmentOccurrence(piece, base, highlight.occurrences) const current = highlight !== undefined && isCurrent(piece, base, highlight) return shown || occurrence || current ? { shown, occurrence, current } : undefined } function sameMarks(a: (SegmentMark | undefined)[], b: (SegmentMark | undefined)[]): boolean { return a.length === b.length && a.every((mark, index) => { const other = b[index] return mark === other || (mark !== undefined && other !== undefined && sameShownName(mark.shown, other.shown) && mark.occurrence === other.occurrence && mark.current === other.current) }) } /** * 框的样式 → 框头图标:遍历同「循环」块、按条件同「条件」块的图标;条件的一支、小标签、类名不带图标。 * 颜色在 codeReader.css 的 .cr-group[data-style] 里,框自己给了颜色(color)时用它。 */ const GROUP_ICONS: Record = { iteration: KIND_ICON_COMPONENTS.repeat, condition: KIND_ICON_COMPONENTS.split, branch: undefined, tag: undefined, classes: undefined, fold: 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 if (group.style === 'fold') return const Icon = GROUP_ICONS[group.style] return ( {(Icon || group.label) && ( {Icon && } {group.label} )} ) } /** * 链式调用卡片里收起来的闭包、async 块(「闭包 |app| · 37 句」):前面一个折叠箭头,点它展开、收起这一步下面挂着的子块 * (开关是所在那一步给的,见 [ChainFoldContext]);不在链的某一步里时只是一个小框。 */ function FoldChip({ group }: { group: SegmentGroup }): ReactElement { const fold = useContext(ChainFoldContext) const Chevron = fold?.open ? ChevronDown : ChevronRight return ( ) } /** 收起来的一串类名(「样式 N 个类」):悬停看全部类名,点一下在它下面弹出、盖住后面的字,再点收起。 */ function ClassesChip({ group }: { group: SegmentGroup }): ReactElement { const [open, setOpen] = useState(false) return ( // 点小标、点弹层里的字都不往外传:大纲里点这一行的字会折叠它的子行,点小标只开关类名 event.stopPropagation()}> {open && {group.detail}} ) }