import type { CSSProperties, ReactElement, ReactNode } from 'react' import type { DiffTree, FoldedRuns, PlacedRemoved } from './diffView' import type { GridSlots } from './PropertyGrid' import type { QaState } from './qa' import type { PendingImage } from './qaImages' import type { RewriteRun } from './rewriteRun' import type { BlockMarks, BlockView } from './types' import { ChevronDown, ChevronRight, ChevronsUpDown, EyeOff } from 'lucide-react' import { Fragment, useState } from 'react' import { AskThread, RewriteThread } from './AskThread' import { BlockTools } from './BlockTools' import { CodeEditor } from './CodeEditor' import { DiffText } from './DiffText' import { removedTitle } from './diffView' import { KindIcon } from './kindIcons' import { PropertyGrid, PropertyTable } from './PropertyGrid' import { asksOf, historyOf } from './qa' import { TextPreview } from './TextPreview' import { tableId } from './viewState' /** * 块树里改代码的那部分:哪些块在编辑、点「编辑」、按注释规范重写。页面上的块树才有;定义卡片浮窗里的块树没有, * 卡片只读,不画右上角的按钮 */ export interface EditTree { version: number editing: ReadonlySet /** 刚点了「编辑」、要把光标放进去的块 */ focusId: string | null onFocused: () => void onToggleEdit: (id: string) => void /** 按注释规范重写的过程,按块的记号。只在这一页上 */ rewrites: ReadonlyMap onRewriteComment: (anchor: string) => void /** 叫停这块正在进行的重写,不写文件 */ onStopRewrite: (anchor: string) => void } export interface BlockTreeState { /** 能编辑(页面上的块树);定义卡片浮窗里没有 */ edit?: EditTree /** 眼下折叠着的块 id、属性行 id 和子行 id(propertyRowId、propertyItemId):用户的选择叠在默认上,见 collapsedIds */ collapsed: ReadonlySet /** 按筛选不画的块(连同子孙),见 applyFilter */ hidden: ReadonlySet /** 本来被筛掉、跳到定义时临时显示的块 */ temporary: ReadonlySet /** 被正在编辑的祖先盖住、不画的块(它们的代码在祖先的编辑框里),见 coveredByEditing */ covered: ReadonlyMap /** 种类叫什么(种类图标的悬停提示和 aria-label):翻译插件给种类起的名字(库规则的种类是规则给的 badge.label),没给就是 id */ kindName: (kind: string) => string onToggleFold: (id: string) => void /** 一次记下几块、几行折叠还是展开(点子行展开一块、同一行里别的收起) */ onSetFolds: (folds: Record) => void /** 问 AI;文件不在工作区文件夹里(问答没地方存)时没有 */ qa?: QaTree /** 代码差异:新增、修改的块和删掉的块;侧栏开着「未提交的更改」或审查工单、基准里有这个文件时才有 */ diff?: DiffTree /** 「只看改动」开着时折起来的连着没变的块(见 foldUnchanged);onOpenRun 点开一段(按第一块的 id) */ folded?: FoldedRuns onOpenRun: (id: string) => void } /** 问 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 } /** * 块下面有没有画出来的子块(删掉的子块也算):子块都被筛掉、被盖住时不留空的一层、不给折叠箭头。 * 挂在卡片子行上的子块(BlockSlot)画在卡片属性表里,不算。 */ function hasShownChildren(block: BlockView, tree: BlockTreeState): boolean { return block.children.some(child => !child.slot && shown(child, tree)) || (!tree.covered.has(block.id) && tree.diff?.removed.has(block.id) === true) } const CHANGE_NAMES = { add: '新增', mod: '修改', del: '删除' } as const /** 标差异时卡片左边外侧的改动色条(没变的留一条透明的,同一层照样对齐);不标差异时原样。 */ function ChangeRow({ tree, change, children }: { tree: BlockTreeState, change: keyof typeof CHANGE_NAMES | undefined, children: ReactNode }): ReactElement { if (!tree.diff) return <>{children} return (
{children}
) } /** 「只看改动」时连着没变的几块折成的一条「… N 块没变」,点开恢复这几块。 */ function UnchangedStrip({ count, tree, onOpen }: { count: number, tree: BlockTreeState, onOpen: () => void }): ReactElement { return ( ) } /** 块画不画:被筛掉的、被正在编辑的祖先盖住的都不画。 */ 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 是这一层所属的块(顶层没有,记在 data-owner 上,钉住外层卡片时按它认哪张卡片的子块画出来了),悬停它时竖线换成它的种类色。 * 标差异时每张卡片左边外侧一条改动色条,删掉的块收成一行放回原来的位置(见 RemovedCard);「只看改动」时连着没变的块折成一条(UnchangedStrip)。 * slotted:这一层是画在卡片属性表某一行下面的子块(BlockSlot),blocks 只有那几块。 */ export function BlockLevel({ blocks, owner, tree, slotted = false }: { blocks: BlockView[], owner?: BlockView, tree: BlockTreeState, slotted?: boolean }): ReactElement { // 挂在卡片子行上的子块画在卡片属性表里(slotted 的那一层),这一层不画;删掉的块都放回卡片后面这一层 const listed = slotted ? blocks : blocks.filter(block => !block.slot) const removed = slotted ? [] : tree.diff?.removed.get(owner?.id ?? '') ?? [] const listedIds = new Set(listed.map(block => block.id)) // 删掉的块排在它原来前面那块后面;那块被筛掉了也照排,不跟着消失;那块画在卡片里时排到这一层最后 const removedAfter = (id: string | undefined): ReactElement[] => removed .filter(item => item.after === id) .map(item => ) const removedElsewhere = removed .filter(item => item.after !== undefined && !listedIds.has(item.after)) .map(item => ) return (
{removedAfter(undefined)} {listed.map(block => ( {tree.folded?.runs.has(block.id) && tree.onOpenRun(block.id)} />} {shown(block, tree) && !tree.folded?.folded.has(block.id) && ( <> {tree.qa && } {tree.edit && } {hasShownChildren(block, tree) && !tree.collapsed.has(block.id) && ( )} )} {removedAfter(block.id)} ))} {removedElsewhere}
) } /** 块下面的问答区,紧跟在块后面、子块前面;输入框、展开状态跟着块的记号走,文档刷新块 id 变了也不丢 */ function BlockQa({ anchor, qa }: { anchor: string, qa: QaTree }): ReactElement { return ( qa.onAsk(anchor, question, images)} onStop={qa.onStop} onDelete={entryId => qa.onDelete(anchor, entryId)} onClose={() => qa.onClose(anchor)} /> ) } function BlockRewrite({ anchor, edit }: { anchor: string, edit: EditTree }): ReactElement | null { const run = edit.rewrites.get(anchor) if (!run) return null return edit.onStopRewrite(anchor)} /> } /** * 预览状态的内容:函数、类、枚举、导入、常量、变量块和库规则的卡片是属性表,其余是一行译文; * 译文下面依次接着摊开实参的表(跨几行写的调用)、说明。标差异时 marks 是这块自己的内容改了哪些(页面上正是差异算的那一版才给)。 */ function BlockPreview({ block, tree, marks }: { block: BlockView, tree: BlockTreeState, marks?: BlockMarks }): ReactElement { if ('properties' in block) return return ( <> {marks?.translation ? : } {block.table && (
)} {marks?.doc ? : block.doc && } ) } /** * 挂在卡片子行上的子块(BlockSlot)怎么画:一行下面展开着的几块画成一层块(同块树);点那个子行的第一格展开它、 * 同一行里别的收起,展开着的再点收起。卡片没有挂在子行上的子块时没有。 */ function cardSlots(block: BlockView, tree: BlockTreeState): GridSlots | undefined { const blocks = block.children.filter(child => child.slot) if (blocks.length === 0) return undefined return { blocks, render: list => , onToggle: (child) => { if (!tree.collapsed.has(child.id)) { tree.onSetFolds({ [child.id]: true }) return } const others = blocks.filter(other => other !== child && other.slot?.row === child.slot?.row) tree.onSetFolds({ ...Object.fromEntries(others.map(other => [other.id, true])), [child.id]: false }) }, } } /** 一块:折叠箭头、种类图标、预览内容或源码、右上角的一排图标按钮(BlockTools,只读的块树没有)。 */ function BlockCard({ block, tree }: { block: BlockView, tree: BlockTreeState }): ReactElement { const { edit } = tree const editing = edit?.editing.has(block.id) ?? false const collapsed = tree.collapsed.has(block.id) // 点父卡片子行才展开的子块:画出来就是展开着的,箭头收起整张 const toggled = block.slot !== undefined const hasChildren = toggled || hasShownChildren(block, tree) const Chevron = collapsed && !toggled ? ChevronRight : ChevronDown const change = tree.diff?.changes.get(block.id) return (
{change?.outermost && 新增} {hasChildren ? ( ) : }
{edit && editing ? ( edit.onToggleEdit(block.id)} /> ) : }
{tree.temporary.has(block.id) && ( 已被筛选隐藏,临时显示 )} {edit && ( edit.onToggleEdit(block.id)} asking={tree.qa?.state.open.includes(block.anchor)} onToggleAsk={tree.qa && (() => tree.qa?.onToggle(block.anchor))} rewriting={edit.rewrites.get(block.anchor)?.status === 'running'} onRewrite={block.commentStartLine !== undefined ? () => edit.onRewriteComment(block.anchor) : undefined} /> )}
) } /** * 删掉的一块,放在原来的位置:红色虚线边、半透明,没有编辑、问 AI 按钮。默认收成一行「已删除 · 种类 名字」, * 点箭头或这一行展开,下面是旧内容(整张划掉,名字不能跳定义);子块不画。 */ function RemovedCard({ placed, tree }: { placed: PlacedRemoved, tree: BlockTreeState }): ReactElement { const [open, setOpen] = useState(false) const { block, kindName } = placed.removed const title = removedTitle(block) const Chevron = open ? ChevronDown : ChevronRight const toggle = (): void => setOpen(value => !value) return (
已删除 · {kindName} {title && {` ${title}`}}
{open && (
{/* 旧块的 id 是旧版本里的位置,换成页面上认删掉的块用的 id,属性行的 id 才不和现在的块撞上 */}
)}
) }