⚡ perf(vscode): 代码阅读预览按卡片订阅状态:中文名、调用树棵数、单击高亮、问答一条消息只重画受影响的卡片和文字,新文档里内容没变的块沿用原来的对象不重画,没展开又没说过的块不画问答区

This commit is contained in:
cruldra committed 2026-10-13 14:40:53 +11:00
1 parent 6e2c541558
commit 0ad4748cc6
27 files changed
+1359 -620

No files matched your search

+287 -160
View File
@@ -2,11 +2,12 @@ import type { CSSProperties, ReactElement, ReactNode } from 'react'
import type { RewriteTool } from './BlockTools'
import type { DiffTree, FoldedRuns, PlacedRemoved } from './diffView'
import type { GridSlots } from './PropertyGrid'
import type { ChatState, QaState } from './qa'
import type { ChatState, ChatView, QaState } from './qa'
import type { PendingImage } from './qaImages'
import type { BlockMarks, BlockView } from './types'
import type { ReadableStore } from './store'
import type { BlockMarks, BlockView, ChangedBlock } from './types'
import { ChevronDown, ChevronRight, ChevronsUpDown, EyeOff } from 'lucide-react'
import { Fragment, useState } from 'react'
import { createContext, Fragment, memo, useContext, useMemo, useState } from 'react'
import { AskThread, REWRITE_THREAD } from './AskThread'
import { BlockTools } from './BlockTools'
import { CodeEditor } from './CodeEditor'
@@ -14,62 +15,74 @@ import { DiffText } from './DiffText'
import { removedTitle } from './diffView'
import { KindIcon } from './kindIcons'
import { PropertyGrid, PropertyItems, PropertyTable } from './PropertyGrid'
import { asksOf, historyOf, unfinished } from './qa'
import { chatOf, EMPTY_CHAT, sameChat, unfinished } from './qa'
import { shallowEqual, staticStore, useStore } from './store'
import { TextPreview } from './TextPreview'
import { listId, tableId } from './viewState'
import { foldsByCard, listId, tableId } from './viewState'
/**
* 块树里改代码的那部分:哪些块在编辑、点「编辑」。页面上的块树才有;定义卡片浮窗里的块树没有,
* 卡片只读,不画右上角的按钮
*/
export interface EditTree {
/** 块树里改代码的那部分状态:哪些块在编辑。页面上的块树才有;定义卡片浮窗里的块树没有,卡片只读,不画右上角的按钮 */
export interface EditState {
version: number
editing: ReadonlySet<string>
/** 刚点了「编辑」、要把光标放进去的块 */
focusId: string | null
onFocused: () => void
onToggleEdit: (id: string) => void
}
/** 函数、类落在几棵调用树里。定义浮窗不传,跟问 AI 一样不画这排按钮。 */
export interface CallTreeTools {
/** 函数、类落在几棵调用树里,开着哪一块的弹框。定义浮窗没有,跟问 AI 一样不画这排按钮。 */
export interface CallTreeState {
counts: ReadonlyMap<string, number>
openAnchor?: string
onOpen: (anchor: string) => void
}
/**
* 块树要画的、会变的状态:一份不可变的快照,放在 BlockTreeHandle.state 里。每张卡片只订阅自己用到的那部分(useStore),
* 一处变了只重画受影响的卡片。
*/
export interface BlockTreeState {
/** 能编辑(页面上的块树);定义卡片浮窗里没有 */
edit?: EditTree
/** 调用树。只读的树不传 */
callTree?: CallTreeTools
edit?: EditState
/** 调用树。只读的树没有 */
callTree?: CallTreeState
/** 眼下折叠着的块 id、属性行 id 和子行 id(propertyRowId、propertyItemId):用户的选择叠在默认上,见 collapsedIds */
collapsed: ReadonlySet<string>
/** collapsed 按卡片分好的(见 foldsByCard):卡片和它的属性表只看自己那份 */
folds: ReadonlyMap<string, ReadonlySet<string>>
/** 按筛选不画的块(连同子孙),见 applyFilter */
hidden: ReadonlySet<string>
/** 本来被筛掉、跳到定义时临时显示的块 */
temporary: ReadonlySet<string>
/** 被正在编辑的祖先盖住、不画的块(它们的代码在祖先的编辑框里),见 coveredByEditing */
covered: ReadonlyMap<string, string>
/** 种类叫什么(种类图标的悬停提示和 aria-label):翻译插件给种类起的名字(库规则的种类是规则给的 badge.label),没给就是 id */
kindName: (kind: string) => string
/** 代码差异:新增、修改的块和删掉的块;侧栏开着「未提交的更改」或审查工单、基准里有这个文件时才有 */
diff?: DiffTree
/** 「只看改动」开着时折起来的连着没变的块(见 foldUnchanged) */
folded?: FoldedRuns
}
/**
* 一棵块树:会变的状态(state,卡片按自己那部分订阅)和动作。整页一份、不跟着状态变,卡片拿到的回调一直是同一个,
* 卡片照 memo 跳过重画。
*/
export interface BlockTreeHandle {
state: ReadableStore<BlockTreeState>
onToggleFold: (id: string) => void
/** 一次记下几块、几行折叠还是展开(点子行展开一块、同一行里别的收起) */
onSetFolds: (folds: Record<string, boolean>) => void
/** 问 AI;文件不在工作区文件夹里(问答没地方存)时没有 */
qa?: QaTree
/** 按注释规范重写;同问 AI,文件不在工作区文件夹里时没有 */
rewrite?: RewriteTree
/** 代码差异:新增、修改的块和删掉的块;侧栏开着「未提交的更改」或审查工单、基准里有这个文件时才有 */
diff?: DiffTree
/** 「只看改动」开着时折起来的连着没变的块(见 foldUnchanged);onOpenRun 点开一段(按第一块的 id) */
folded?: FoldedRuns
/** 「只看改动」时点开一段折起来的块(按第一块的 id) */
onOpenRun: (id: string) => void
/** 编辑:点「编辑 / 预览」、光标放进去之后。和 state.edit 同时有 */
edit?: { onToggleEdit: (id: string) => void, onFocused: () => void }
/** 打开一块的调用树弹框。和 state.callTree 同时有 */
onOpenCall?: (anchor: string) => void
/** 问 AI;文件不在工作区文件夹里(问答没地方存)时 state 里 available 是 false */
qa?: QaTree
/** 按注释规范重写;同问 AI */
rewrite?: RewriteTree
}
/** 重写注释的状态和动作,块按自己的记号(anchor)取;profile 下拉是自己的,和问 AI 的互不影响 */
/** 重写注释的状态(块按自己的记号订阅)和动作;profile 下拉是自己的,和问 AI 的互不影响 */
export interface RewriteTree {
state: ChatState
store: ReadableStore<ChatState>
/** 点了块右上角的按钮:按技能命令重写这块(开新会话),展开它的重写记录 */
onRewrite: (anchor: string) => void
/** 在输入框里说的修改意见,接着这块最近一次重写的会话说;images 是贴的图片 */
@@ -82,9 +95,9 @@ export interface RewriteTree {
onSelectProfile: (id: string) => void
}
/** 问 AI 的状态和动作,块按自己的记号(anchor)取 */
/** 问 AI 的状态(块按自己的记号订阅)和动作 */
export interface QaTree {
state: QaState
store: ReadableStore<QaState>
onToggle: (anchor: string) => void
onClose: (anchor: string) => void
/** images 是贴进输入框的图片,随问题一起发 */
@@ -95,19 +108,47 @@ export interface QaTree {
onSelectProfile: (id: string) => void
}
const BlockTreeContext = createContext<BlockTreeHandle | null>(null)
function useBlockTree(): BlockTreeHandle {
const tree = useContext(BlockTreeContext)
if (!tree)
throw new Error('块树外面没有 BlockTree')
return tree
}
/** 一棵块树:blocks 是顶层的块,tree 是它的状态和动作(见 BlockTreeHandle)。 */
export function BlockTree({ blocks, tree }: { blocks: BlockView[], tree: BlockTreeHandle }): ReactElement {
return (
<BlockTreeContext value={tree}>
<BlockLevel blocks={blocks} />
</BlockTreeContext>
)
}
/** 块树的状态:folds 由 collapsed 按卡片分好(见 foldsByCard),和 previous 里一样的那几份沿用。 */
export function treeState(state: Omit<BlockTreeState, 'folds'>, previous?: BlockTreeState): BlockTreeState {
return { ...state, folds: foldsByCard(state.collapsed, previous?.folds) }
}
const NO_FOLDS: ReadonlySet<string> = new Set()
const NO_REMOVED: readonly PlacedRemoved[] = []
/** 没有问 AI、重写注释的块树(定义卡片浮窗):一直不能用 */
const NO_CHAT: ReadableStore<ChatState> = staticStore(EMPTY_CHAT)
/**
* 块下面有没有画出来的子块(删掉的子块也算):子块都被筛掉、被盖住时不留空的一层、不给折叠箭头。
* 挂在卡片子行上的子块(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)
function hasShownChildren(block: BlockView, state: BlockTreeState): boolean {
return block.children.some(child => !child.slot && shown(child, state)) || (!state.covered.has(block.id) && state.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)
function ChangeRow({ diff, change, children }: { diff: boolean, change: keyof typeof CHANGE_NAMES | undefined, children: ReactNode }): ReactElement {
if (!diff)
return <>{children}</>
return (
<div className="cr-change-row flex gap-[5px]">
@@ -118,9 +159,9 @@ function ChangeRow({ tree, change, children }: { tree: BlockTreeState, change: k
}
/** 「只看改动」时连着没变的几块折成的一条「… N 块没变」,点开恢复这几块。 */
function UnchangedStrip({ count, tree, onOpen }: { count: number, tree: BlockTreeState, onOpen: () => void }): ReactElement {
function UnchangedStrip({ count, onOpen }: { count: number, onOpen: () => void }): ReactElement {
return (
<ChangeRow tree={tree} change={undefined}>
<ChangeRow diff change={undefined}>
<button type="button" className="cr-unchanged flex h-[22px] w-full items-center gap-1.5 rounded px-2.5 text-xs" onClick={onOpen}>
<ChevronsUpDown size={12} strokeWidth={1.8} className="shrink-0" aria-hidden />
{`… ${count} 块没变`}
@@ -131,8 +172,8 @@ function UnchangedStrip({ count, tree, onOpen }: { count: number, tree: BlockTre
}
/** 块画不画:被筛掉的、被正在编辑的祖先盖住的都不画。 */
function shown(block: BlockView, tree: BlockTreeState): boolean {
return !tree.hidden.has(block.id) && !tree.covered.has(block.id)
function shown(block: BlockView, state: BlockTreeState): boolean {
return !state.hidden.has(block.id) && !state.covered.has(block.id)
}
/**
@@ -148,24 +189,184 @@ export function kindProps(block: Pick<BlockView, 'kind' | 'badge'> | undefined):
return { 'data-kind': block.kind, 'data-badge': '', 'style': { '--kind-dark': dark, '--kind-light': light } as CSSProperties }
}
/** 一块画成什么样要的块树状态:只选这一块自己的,别的块变了它不重画。 */
interface BlockShape {
/** 画不画:被筛掉的、被正在编辑的祖先盖住的、「只看改动」折起来的不画 */
shown: boolean
/** 「只看改动」时从这块起折起来的一段有几块 */
run?: number
/** 子块那一层画不画 */
open: boolean
/** 标差异 */
diff: boolean
change?: ChangedBlock
/** 页面上正是差异算的那一版:卡片里的标记这时才画 */
current: boolean
editing: boolean
/** 这张卡片里折叠着的(见 foldsByCard) */
folds: ReadonlySet<string>
hasChildren: boolean
temporary: boolean
focus: boolean
/** 编辑框每来一份新文档都要对一次内容;不在编辑的卡片不跟着版本重画 */
version?: number
/** 调用树:扩展端标了能看调用树(BlockView.callTree)的块才有棵数 */
calls?: number
callOpen: boolean
}
function shapeOf(block: BlockView, state: BlockTreeState): BlockShape {
const editing = state.edit?.editing.has(block.id) ?? false
return {
shown: shown(block, state) && !state.folded?.folded.has(block.id),
run: state.folded?.runs.get(block.id),
open: hasShownChildren(block, state) && !state.collapsed.has(block.id),
diff: state.diff !== undefined,
change: state.diff?.changes.get(block.id),
current: state.diff?.current ?? false,
editing,
folds: state.folds.get(block.id) ?? NO_FOLDS,
// 点父卡片子行才展开的子块:画出来就是展开着的,箭头收起整张
hasChildren: block.slot !== undefined || hasShownChildren(block, state),
temporary: state.temporary.has(block.id),
focus: state.edit?.focusId === block.id,
version: editing ? state.edit?.version : undefined,
calls: state.callTree && block.callTree ? state.callTree.counts.get(block.anchor) ?? 0 : undefined,
callOpen: state.callTree?.openAnchor === block.anchor,
}
}
/** 一块的问 AI:能问(文件在工作区文件夹里)时按钮展开没有;对话区展开着或说过时才有,没展开又没说过的块不画。 */
interface AskShape {
asking?: boolean
chat?: ChatView
}
const NO_ASK: AskShape = {}
function askOf(anchor: string, state: ChatState): AskShape {
if (!state.available)
return NO_ASK
const chat = chatOf(state, anchor)
return { asking: chat.open, ...((chat.open || chat.history.length > 0) && { chat }) }
}
/**
* 一块的重写注释:能写注释的块才有按钮。重写过、记录收着时点了先展开记录(好接着说);展开着、或还没重写过时点了按技能命令重写。
* 展开着又正在写、排着队时不能再点。重写记录同问 AI 的对话区。
*/
interface RewriteShape {
tool?: Omit<RewriteTool, 'onRewrite'>
chat?: ChatView
}
const NO_REWRITE: RewriteShape = {}
function rewriteOf(block: BlockView, state: ChatState): RewriteShape {
if (!state.available)
return NO_REWRITE
const chat = chatOf(state, block.anchor)
return {
...(block.commentTarget && {
tool: {
fresh: block.commentTarget.kind === 'insert',
rewriting: chat.open && chat.asks.some(unfinished),
reopen: !chat.open && (chat.asks.length > 0 || chat.history.length > 0),
},
}),
...((chat.open || chat.history.length > 0) && { chat }),
}
}
function sameChatIn(a: ChatView | undefined, b: ChatView | undefined): boolean {
return a === b || (a !== undefined && b !== undefined && sameChat(a, b))
}
function sameAsk(a: AskShape, b: AskShape): boolean {
return a.asking === b.asking && sameChatIn(a.chat, b.chat)
}
function sameRewrite(a: RewriteShape, b: RewriteShape): boolean {
return shallowEqual(a.tool, b.tool) && sameChatIn(a.chat, b.chat)
}
/**
* 一块:「只看改动」折起来的一段、卡片、下面的问答区和重写记录、子块那一层。块树、问 AI、重写注释三份状态里只订阅这一块自己的,
* 都没变就不重画;子块那一层的各块、卡片的内容各自 memo,这一块重画了它们也不跟着重画。
*/
const BlockItem = memo(({ block }: { block: BlockView }): ReactElement => {
const tree = useBlockTree()
const { qa, rewrite } = tree
const shape = useStore(tree.state, state => shapeOf(block, state), shallowEqual)
const ask = useStore(qa?.store ?? NO_CHAT, state => askOf(block.anchor, state), sameAsk)
const rewriting = useStore(rewrite?.store ?? NO_CHAT, state => rewriteOf(block, state), sameRewrite)
const { anchor } = block
return (
<>
{shape.run !== undefined && <UnchangedStrip count={shape.run} onOpen={() => tree.onOpenRun(block.id)} />}
{shape.shown && (
<>
<ChangeRow diff={shape.diff} change={shape.change?.change}>
<BlockCard block={block} shape={shape} ask={ask} rewrite={rewriting} />
</ChangeRow>
{/* 输入框、展开状态跟着块的记号走,文档刷新块 id 变了也不丢 */}
{qa && ask.chat && (
<AskThread
key={anchor}
history={ask.chat.history}
asks={ask.chat.asks}
open={ask.chat.open}
profiles={ask.chat.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)}
/>
)}
{/* 重写记录紧跟在问答区后面;样子和问答区一样,输入框里说的是对这段注释的修改意见 */}
{rewrite && rewriting.chat && (
<AskThread
key={`rewrite ${anchor}`}
wording={REWRITE_THREAD}
history={rewriting.chat.history}
asks={rewriting.chat.asks}
open={rewriting.chat.open}
profiles={rewriting.chat.profiles}
onSelectProfile={rewrite.onSelectProfile}
onAsk={(request, images) => rewrite.onAsk(anchor, request, images)}
onStop={rewrite.onStop}
onDelete={entryId => rewrite.onDelete(anchor, entryId)}
onClose={() => rewrite.onClose(anchor)}
/>
)}
{shape.open && <BlockLevel blocks={block.children} owner={block} />}
</>
)}
</>
)
})
BlockItem.displayName = 'BlockItem'
/**
* 一层代码块:左边一条竖线标出这一层,每块的子块紧跟在它下面、再往右缩进一层。被筛掉、被盖住的块不画,不留空位,竖线跟着变短。
* 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 {
export function BlockLevel({ blocks, owner, slotted = false }: { blocks: BlockView[], owner?: BlockView, slotted?: boolean }): ReactElement {
const tree = useBlockTree()
// 挂在卡片子行上的子块画在卡片属性表里(slotted 的那一层),这一层不画;删掉的块都放回卡片后面这一层
const listed = slotted ? blocks : blocks.filter(block => !block.slot)
const removed = slotted ? [] : tree.diff?.removed.get(owner?.id ?? '') ?? []
const listed = useMemo(() => slotted ? blocks : blocks.filter(block => !block.slot), [blocks, slotted])
const removed = useStore(tree.state, state => slotted ? NO_REMOVED : state.diff?.removed.get(owner?.id ?? '') ?? NO_REMOVED)
const listedIds = new Set(listed.map(block => block.id))
// 删掉的块排在它原来前面那块后面;那块被筛掉了也照排,不跟着消失;那块画在卡片里时排到这一层最后
const removedAfter = (id: string | undefined): ReactElement[] => removed
.filter(item => item.after === id)
.map(item => <RemovedCard key={item.id} placed={item} tree={tree} />)
.map(item => <RemovedCard key={item.id} placed={item} />)
const removedElsewhere = removed
.filter(item => item.after !== undefined && !listedIds.has(item.after))
.map(item => <RemovedCard key={item.id} placed={item} tree={tree} />)
.map(item => <RemovedCard key={item.id} placed={item} />)
return (
<div className="cr-level flex gap-2" {...kindProps(owner)} data-owner={owner?.id}>
<div className="flex w-3 shrink-0 justify-center">
@@ -175,19 +376,7 @@ export function BlockLevel({ blocks, owner, tree, slotted = false }: { blocks: B
{removedAfter(undefined)}
{listed.map(block => (
<Fragment key={block.id}>
{tree.folded?.runs.has(block.id) && <UnchangedStrip count={tree.folded.runs.get(block.id) ?? 0} tree={tree} onOpen={() => tree.onOpenRun(block.id)} />}
{shown(block, tree) && !tree.folded?.folded.has(block.id) && (
<>
<ChangeRow tree={tree} change={tree.diff?.changes.get(block.id)?.change}>
<BlockCard block={block} tree={tree} />
</ChangeRow>
{tree.qa && <BlockQa key={block.anchor} anchor={block.anchor} qa={tree.qa} />}
{tree.rewrite && <BlockRewrite key={`rewrite ${block.anchor}`} anchor={block.anchor} rewrite={tree.rewrite} />}
{hasShownChildren(block, tree) && !tree.collapsed.has(block.id) && (
<BlockLevel blocks={block.children} owner={block} tree={tree} />
)}
</>
)}
<BlockItem block={block} />
{removedAfter(block.id)}
</Fragment>
))}
@@ -197,50 +386,17 @@ export function BlockLevel({ blocks, owner, tree, slotted = false }: { blocks: B
)
}
/** 块下面的问答区,紧跟在块后面、子块前面;输入框、展开状态跟着块的记号走,文档刷新块 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 BlockRewrite({ anchor, rewrite }: { anchor: string, rewrite: RewriteTree }): ReactElement | null {
return (
<AskThread
wording={REWRITE_THREAD}
history={historyOf(rewrite.state, anchor)}
asks={asksOf(rewrite.state, anchor)}
open={rewrite.state.open.includes(anchor)}
profiles={rewrite.state.profiles}
onSelectProfile={rewrite.onSelectProfile}
onAsk={(request, images) => rewrite.onAsk(anchor, request, images)}
onStop={rewrite.onStop}
onDelete={entryId => rewrite.onDelete(anchor, entryId)}
onClose={() => rewrite.onClose(anchor)}
/>
)
}
/**
* 预览状态的内容:函数、类、枚举、导入、常量、变量块和库规则的卡片是属性表,其余是一行译文;
* 译文下面依次接着拆开的各项(跨几行写的字面量,排法同变量初始值下面的)、摊开实参的表(跨几行写的调用)、说明、几行属性(「注释」一行)。
* 标差异时 marks 是这块自己的内容改了哪些(页面上正是差异算的那一版才给)。
* folds 是这张卡片里折叠着的(见 foldsByCard);标差异时 marks 是这块自己的内容改了哪些(页面上正是差异算的那一版才给)。
*/
function BlockPreview({ block, tree, marks }: { block: BlockView, tree: BlockTreeState, marks?: BlockMarks }): ReactElement {
const BlockPreview = memo(({ block, folds, marks }: { block: BlockView, folds: ReadonlySet<string>, marks?: BlockMarks }): ReactElement => {
const tree = useBlockTree()
const slots = useMemo(() => cardSlots(block, folds, tree), [block, folds, tree])
if ('properties' in block)
return <PropertyGrid blockId={block.id} rows={block.properties} collapsed={tree.collapsed} onToggleFold={tree.onToggleFold} marks={marks?.rows} slots={cardSlots(block, tree)} />
const folding = { collapsed: tree.collapsed, onToggleFold: tree.onToggleFold }
return <PropertyGrid blockId={block.id} rows={block.properties} collapsed={folds} onToggleFold={tree.onToggleFold} marks={marks?.rows} slots={slots} />
const folding = { collapsed: folds, onToggleFold: tree.onToggleFold }
return (
<>
{marks?.translation ? <DiffText diff={marks.translation} /> : <TextPreview text={block.translation} />}
@@ -257,59 +413,27 @@ function BlockPreview({ block, tree, marks }: { block: BlockView, tree: BlockTre
{marks?.doc ? <DiffText diff={marks.doc} className="mt-0.5" /> : block.doc && <TextPreview text={block.doc} className="mt-0.5" />}
{block.rows && (
<div className="mt-[3px]">
<PropertyGrid blockId={block.id} rows={block.rows} collapsed={tree.collapsed} onToggleFold={tree.onToggleFold} marks={marks?.rows} />
<PropertyGrid blockId={block.id} rows={block.rows} collapsed={folds} onToggleFold={tree.onToggleFold} marks={marks?.rows} />
</div>
)}
</>
)
}
/** 这块的调用树按钮:扩展端标了能看调用树(BlockView.callTree)的块才有。 */
function callTreeTool(block: BlockView, tree: BlockTreeState) {
const callTree = tree.callTree
if (!callTree || !block.callTree)
return undefined
return {
anchor: block.anchor,
count: callTree.counts.get(block.anchor) ?? 0,
open: callTree.openAnchor === block.anchor,
onOpen: () => callTree.onOpen(block.anchor),
}
}
/**
* 这块的「重写注释」按钮:能写注释的块才有。重写过、记录收着时点了先展开记录(好接着说);展开着、或还没重写过时点了按技能命令重写。
* 展开着又正在写、排着队时不能再点。
*/
function rewriteTool(block: BlockView, tree: BlockTreeState): RewriteTool | undefined {
const { edit, rewrite } = tree
if (!edit || !rewrite || !block.commentTarget)
return undefined
const { anchor } = block
const open = rewrite.state.open.includes(anchor)
const asks = asksOf(rewrite.state, anchor)
const reopen = !open && (asks.length > 0 || historyOf(rewrite.state, anchor).length > 0)
return {
fresh: block.commentTarget.kind === 'insert',
rewriting: open && asks.some(unfinished),
reopen,
onRewrite: () => reopen ? rewrite.onOpen(anchor) : rewrite.onRewrite(anchor),
}
}
})
BlockPreview.displayName = 'BlockPreview'
/**
* 挂在卡片子行上的子块(BlockSlot)怎么画:一行下面展开着的几块画成一层块(同块树);点那个子行的第一格展开它、
* 同一行里别的收起,展开着的再点收起。卡片没有挂在子行上的子块时没有。
*/
function cardSlots(block: BlockView, tree: BlockTreeState): GridSlots | undefined {
function cardSlots(block: BlockView, folds: ReadonlySet<string>, tree: BlockTreeHandle): GridSlots | undefined {
const blocks = block.children.filter(child => child.slot)
if (blocks.length === 0)
return undefined
return {
blocks,
render: list => <BlockLevel blocks={list} owner={block} tree={tree} slotted />,
render: list => <BlockLevel blocks={list} owner={block} slotted />,
onToggle: (child) => {
if (!tree.collapsed.has(child.id)) {
if (!folds.has(child.id)) {
tree.onSetFolds({ [child.id]: true })
return
}
@@ -320,30 +444,30 @@ function cardSlots(block: BlockView, tree: BlockTreeState): GridSlots | undefine
}
/**
* 一块:折叠箭头、种类图标、预览内容或源码、右上角的一排图标按钮(BlockTools,只读的块树没有)。
* 一块的卡片:折叠箭头、种类图标、预览内容或源码、右上角的一排图标按钮(BlockTools,只读的块树没有)。
* 卡片上记着块记号(data-anchor),说明里不带路径的引用靠它认自己写在哪块(见 commentLinkOf)。
*/
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)
// 点父卡片子行才展开的子块:画出来就是展开着的,箭头收起整张
function BlockCard({ block, shape, ask, rewrite }: { block: BlockView, shape: BlockShape, ask: AskShape, rewrite: RewriteShape }): ReactElement {
const tree = useBlockTree()
const { edit, onOpenCall } = tree
const { anchor } = block
const collapsed = shape.folds.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)
const { change } = shape
const { tool } = rewrite
return (
<div
className="cr-block relative flex items-start gap-2.5 rounded-lg py-2 pr-2.5 pl-2"
{...kindProps(block)}
data-id={block.id}
data-anchor={block.anchor}
data-anchor={anchor}
data-line={block.startLine}
data-editing={editing ? '' : undefined}
data-editing={shape.editing ? '' : undefined}
data-change={change?.change}
>
{change?.outermost && <span className="cr-added-badge">新增</span>}
{hasChildren
{shape.hasChildren
? (
<button
type="button"
@@ -358,19 +482,19 @@ function BlockCard({ block, tree }: { block: BlockView, tree: BlockTreeState }):
: <span className="w-3.5 shrink-0" />}
<KindIcon concepts={block.concepts} />
<div className="min-w-0 flex-1">
{edit && editing
{edit && shape.version !== undefined
? (
<CodeEditor
block={block}
version={edit.version}
autoFocus={edit.focusId === block.id}
version={shape.version}
autoFocus={shape.focus}
onAutoFocused={edit.onFocused}
onExit={() => edit.onToggleEdit(block.id)}
/>
)
: <BlockPreview block={block} tree={tree} marks={tree.diff?.current ? change?.marks : undefined} />}
: <BlockPreview block={block} folds={shape.folds} marks={shape.current ? change?.marks : undefined} />}
</div>
{tree.temporary.has(block.id) && (
{shape.temporary && (
<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 />
已被筛选隐藏,临时显示
@@ -378,12 +502,12 @@ function BlockCard({ block, tree }: { block: BlockView, tree: BlockTreeState }):
)}
{edit && (
<BlockTools
editing={editing}
editing={shape.editing}
onToggleEdit={() => edit.onToggleEdit(block.id)}
asking={tree.qa?.state.open.includes(block.anchor)}
onToggleAsk={tree.qa && (() => tree.qa?.onToggle(block.anchor))}
rewrite={rewriteTool(block, tree)}
callTree={callTreeTool(block, tree)}
asking={ask.asking}
onToggleAsk={tree.qa && ask.asking !== undefined ? () => tree.qa?.onToggle(anchor) : undefined}
rewrite={tree.rewrite && tool && { ...tool, onRewrite: () => tool.reopen ? tree.rewrite?.onOpen(anchor) : tree.rewrite?.onRewrite(anchor) }}
callTree={onOpenCall && shape.calls !== undefined ? { anchor, count: shape.calls, open: shape.callOpen, onOpen: () => onOpenCall(anchor) } : undefined}
/>
)}
</div>
@@ -394,14 +518,18 @@ function BlockCard({ block, tree }: { block: BlockView, tree: BlockTreeState }):
* 删掉的一块,放在原来的位置:红色虚线边、半透明,没有编辑、问 AI 按钮。默认收成一行「已删除 · 种类 名字」,
* 点箭头或这一行展开,下面是旧内容(整张划掉,名字不能跳定义);子块不画。
*/
function RemovedCard({ placed, tree }: { placed: PlacedRemoved, tree: BlockTreeState }): ReactElement {
function RemovedCard({ placed }: { placed: PlacedRemoved }): ReactElement {
const tree = useBlockTree()
const [open, setOpen] = useState(false)
const folds = useStore(tree.state, state => state.folds.get(placed.id) ?? NO_FOLDS)
const { block, kindName } = placed.removed
const title = removedTitle(block)
const Chevron = open ? ChevronDown : ChevronRight
const toggle = (): void => setOpen(value => !value)
// 旧块的 id 是旧版本里的位置,换成页面上认删掉的块用的 id,属性行的 id 才不和现在的块撞上
const old = useMemo(() => ({ ...block, id: placed.id }), [block, placed.id])
return (
<ChangeRow tree={tree} change="del">
<ChangeRow diff change="del">
<div className="cr-block cr-removed flex items-start gap-2.5 rounded-lg py-2 pr-2.5 pl-2" {...kindProps(block)} data-id={placed.id}>
<button
type="button"
@@ -421,8 +549,7 @@ function RemovedCard({ placed, tree }: { placed: PlacedRemoved, tree: BlockTreeS
</div>
{open && (
<div className="cr-removed-old mt-1">
{/* 旧块的 id 是旧版本里的位置,换成页面上认删掉的块用的 id,属性行的 id 才不和现在的块撞上 */}
<BlockPreview block={{ ...block, id: placed.id }} tree={tree} />
<BlockPreview block={old} folds={folds} />
</div>
)}
</div>