⚡ perf(vscode): 代码阅读预览按卡片订阅状态:中文名、调用树棵数、单击高亮、问答一条消息只重画受影响的卡片和文字,新文档里内容没变的块沿用原来的对象不重画,没展开又没说过的块不画问答区
This commit is contained in:
1 parent
6e2c541558
commit
0ad4748cc6
27 files changed
+1359
-620
No files matched your search
@@ -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>
|
||||
|
||||
Reference in new issue
Block a user