192 lines
8.1 KiB
TypeScript
192 lines
8.1 KiB
TypeScript
import type { CSSProperties, ReactElement } from 'react'
|
||
import type { QaState } from './qa'
|
||
import type { PendingImage } from './qaImages'
|
||
import type { BlockView } from './types'
|
||
import { ChevronDown, ChevronRight, EyeOff } from 'lucide-react'
|
||
import { Fragment } from 'react'
|
||
import { AskThread } from './AskThread'
|
||
import { BlockTools } from './BlockTools'
|
||
import { CodeEditor } from './CodeEditor'
|
||
import { KindIcon } from './kindIcons'
|
||
import { PropertyGrid, PropertyTable } from './PropertyGrid'
|
||
import { asksOf, historyOf } from './qa'
|
||
import { TextPreview } from './TextPreview'
|
||
import { tableId } from './viewState'
|
||
|
||
export interface BlockTreeState {
|
||
version: number
|
||
editing: ReadonlySet<string>
|
||
/** 眼下折叠着的块 id、属性行 id 和子行 id(propertyRowId、propertyItemId):用户的选择叠在默认上,见 collapsedIds */
|
||
collapsed: ReadonlySet<string>
|
||
/** 按筛选不画的块(连同子孙),见 applyFilter */
|
||
hidden: ReadonlySet<string>
|
||
/** 本来被筛掉、跳到定义时临时显示的块 */
|
||
temporary: ReadonlySet<string>
|
||
/** 被正在编辑的祖先盖住、不画的块(它们的行在祖先的编辑范围里),见 coveredByEditing */
|
||
covered: ReadonlyMap<string, string>
|
||
/** 种类叫什么(种类图标的悬停提示和 aria-label):翻译插件给种类起的名字(库规则的种类是规则给的 badge.label),没给就是 id */
|
||
kindName: (kind: string) => string
|
||
/** 刚点了「编辑」、要把光标放进去的块 */
|
||
focusId: string | null
|
||
onFocused: () => void
|
||
onToggleEdit: (id: string) => void
|
||
onToggleFold: (id: string) => void
|
||
/** 问 AI;文件不在工作区文件夹里(问答没地方存)时没有 */
|
||
qa?: QaTree
|
||
}
|
||
|
||
/** 问 AI 的状态和动作,块按自己的记号(anchor)取 */
|
||
export interface QaTree {
|
||
state: QaState
|
||
onToggle: (anchor: string) => void
|
||
onClose: (anchor: string) => void
|
||
/** images 是贴进输入框的图片,随问题一起发 */
|
||
onAsk: (anchor: string, question: string, images: PendingImage[]) => void
|
||
onStop: (askId: string) => void
|
||
onDelete: (anchor: string, entryId: string) => void
|
||
/** 换「问 AI」用的 profile(所有块共用),id 同 ProfileOption.id */
|
||
onSelectProfile: (id: string) => void
|
||
}
|
||
|
||
/** 块下面有没有画出来的子块:子块都被筛掉、被盖住时不留空的一层、不给折叠箭头。 */
|
||
function hasShownChildren(block: BlockView, tree: BlockTreeState): boolean {
|
||
return block.children.some(child => shown(child, tree))
|
||
}
|
||
|
||
/** 块画不画:被筛掉的、被正在编辑的祖先盖住的都不画。 */
|
||
function shown(block: BlockView, tree: BlockTreeState): boolean {
|
||
return !tree.hidden.has(block.id) && !tree.covered.has(block.id)
|
||
}
|
||
|
||
/**
|
||
* 块的种类色写在哪:内置种类按 data-kind 取 CSS 里的配色;库规则认领的块(带 badge)按规则给的颜色,
|
||
* 暗色、亮色两个都写在 style 上,CSS 按主题挑一个(见 [data-badge])。
|
||
*/
|
||
function kindProps(block: BlockView | undefined): { 'data-kind'?: string, 'data-badge'?: '', 'style'?: CSSProperties } {
|
||
if (!block)
|
||
return {}
|
||
if (!block.badge)
|
||
return { 'data-kind': block.kind }
|
||
const { dark, light } = block.badge.color
|
||
return { 'data-kind': block.kind, 'data-badge': '', 'style': { '--kind-dark': dark, '--kind-light': light } as CSSProperties }
|
||
}
|
||
|
||
/**
|
||
* 一层代码块:左边一条竖线标出这一层,每块的子块紧跟在它下面、再往右缩进一层。被筛掉、被盖住的块不画,不留空位,竖线跟着变短。
|
||
* owner 是这一层所属的块(顶层没有),悬停它时竖线换成它的种类色。
|
||
*/
|
||
export function BlockLevel({ blocks, owner, tree }: { blocks: BlockView[], owner?: BlockView, tree: BlockTreeState }): ReactElement {
|
||
return (
|
||
<div className="cr-level flex gap-2" {...kindProps(owner)}>
|
||
<div className="flex w-3 shrink-0 justify-center">
|
||
<div className="cr-guide w-px" />
|
||
</div>
|
||
<div className="flex min-w-0 flex-1 flex-col gap-2">
|
||
{blocks.filter(block => shown(block, tree)).map(block => (
|
||
<Fragment key={block.id}>
|
||
<BlockCard block={block} tree={tree} />
|
||
{tree.qa && <BlockQa key={block.anchor} anchor={block.anchor} qa={tree.qa} />}
|
||
{hasShownChildren(block, tree) && !tree.collapsed.has(block.id) && (
|
||
<BlockLevel blocks={block.children} owner={block} tree={tree} />
|
||
)}
|
||
</Fragment>
|
||
))}
|
||
</div>
|
||
</div>
|
||
)
|
||
}
|
||
|
||
/** 块下面的问答区,紧跟在块后面、子块前面;输入框、展开状态跟着块的记号走,文档刷新块 id 变了也不丢 */
|
||
function BlockQa({ anchor, qa }: { anchor: string, qa: QaTree }): ReactElement {
|
||
return (
|
||
<AskThread
|
||
history={historyOf(qa.state, anchor)}
|
||
asks={asksOf(qa.state, anchor)}
|
||
open={qa.state.open.includes(anchor)}
|
||
profiles={qa.state.profiles}
|
||
onSelectProfile={qa.onSelectProfile}
|
||
onAsk={(question, images) => qa.onAsk(anchor, question, images)}
|
||
onStop={qa.onStop}
|
||
onDelete={entryId => qa.onDelete(anchor, entryId)}
|
||
onClose={() => qa.onClose(anchor)}
|
||
/>
|
||
)
|
||
}
|
||
|
||
/**
|
||
* 预览状态的内容:函数、类、枚举、导入、常量、变量块和库规则的卡片是属性表,其余是一行译文;
|
||
* 译文下面依次接着摊开实参的表(跨几行写的调用)、说明。
|
||
*/
|
||
function BlockPreview({ block, tree }: { block: BlockView, tree: BlockTreeState }): ReactElement {
|
||
if ('properties' in block)
|
||
return <PropertyGrid blockId={block.id} rows={block.properties} collapsed={tree.collapsed} onToggleFold={tree.onToggleFold} />
|
||
return (
|
||
<>
|
||
<TextPreview text={block.translation} />
|
||
{block.table && (
|
||
<div className="mt-[3px]">
|
||
<PropertyTable parentId={tableId(block.id)} columns={block.table.columns} items={block.table.items} />
|
||
</div>
|
||
)}
|
||
{block.doc && <TextPreview text={block.doc} className="mt-0.5" />}
|
||
</>
|
||
)
|
||
}
|
||
|
||
/** 一块:折叠箭头、种类图标、预览内容或源码、右上角的一排图标按钮(BlockTools)。 */
|
||
function BlockCard({ block, tree }: { block: BlockView, tree: BlockTreeState }): ReactElement {
|
||
const editing = tree.editing.has(block.id)
|
||
const collapsed = tree.collapsed.has(block.id)
|
||
const hasChildren = hasShownChildren(block, tree)
|
||
const Chevron = collapsed ? ChevronRight : ChevronDown
|
||
return (
|
||
<div
|
||
className="cr-block flex items-start gap-2.5 rounded-lg py-2 pr-2.5 pl-2"
|
||
{...kindProps(block)}
|
||
data-id={block.id}
|
||
data-line={block.startLine}
|
||
data-editing={editing ? '' : undefined}
|
||
>
|
||
{hasChildren
|
||
? (
|
||
<button
|
||
type="button"
|
||
className="flex h-5 w-3.5 shrink-0 items-center justify-center rounded-sm text-[var(--vscode-descriptionForeground)] hover:text-[var(--vscode-foreground)]"
|
||
aria-label={collapsed ? '展开子块' : '折叠子块'}
|
||
aria-expanded={!collapsed}
|
||
onClick={() => tree.onToggleFold(block.id)}
|
||
>
|
||
<Chevron size={14} strokeWidth={1.8} />
|
||
</button>
|
||
)
|
||
: <span className="w-3.5 shrink-0" />}
|
||
<KindIcon block={block} name={tree.kindName(block.kind)} />
|
||
<div className="min-w-0 flex-1">
|
||
{editing
|
||
? (
|
||
<CodeEditor
|
||
block={block}
|
||
version={tree.version}
|
||
autoFocus={tree.focusId === block.id}
|
||
onAutoFocused={tree.onFocused}
|
||
onExit={() => tree.onToggleEdit(block.id)}
|
||
/>
|
||
)
|
||
: <BlockPreview block={block} tree={tree} />}
|
||
</div>
|
||
{tree.temporary.has(block.id) && (
|
||
<span className="cr-temporary flex h-5 shrink-0 items-center gap-1 rounded px-1.5 text-[11px] whitespace-nowrap">
|
||
<EyeOff size={12} strokeWidth={1.8} aria-hidden />
|
||
已被筛选隐藏,临时显示
|
||
</span>
|
||
)}
|
||
<BlockTools
|
||
editing={editing}
|
||
onToggleEdit={() => tree.onToggleEdit(block.id)}
|
||
asking={tree.qa?.state.open.includes(block.anchor)}
|
||
onToggleAsk={tree.qa && (() => tree.qa?.onToggle(block.anchor))}
|
||
/>
|
||
</div>
|
||
)
|
||
}
|