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

192 lines
8.2 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 { translationTableId } 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={translationTableId(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>
)
}