⚡ perf(vscode): 代码阅读预览发给页面的块只记相对位置:块在源码里占的地方记成离前一块多远、自己多长,块里名字的下标从块的开头算,页面从文件开头累加回文件里的位置、和扩展端打交道的地方都在一处换算;靠前改了代码,后面内容没变的块发过去一模一样,扩展端自己的块和发给页面的块分成两种类型

This commit is contained in:
cruldra committed 2026-10-13 16:32:18 +11:00
1 parent 3a2815c19c
commit ad5f559e92
43 files changed
+980 -424

No files matched your search

+73 -64
View File
@@ -2,11 +2,12 @@ import type { CSSProperties, ReactElement, ReactNode } from 'react'
import type { SourceText } from './blockSource'
import type { RewriteTool } from './BlockTools'
import type { DiffTree, FoldedRuns, PlacedRemoved } from './diffView'
import type { Place, Placement } from './placement'
import type { GridSlots } from './PropertyGrid'
import type { ChatState, ChatView, QaState } from './qa'
import type { PendingImage } from './qaImages'
import type { ReadableStore } from './store'
import type { BlockMarks, BlockView, ChangedBlock } from './types'
import type { BlockMarks, BlockView, CardView, ChangedBlock } from './types'
import { ChevronDown, ChevronRight, ChevronsUpDown, EyeOff } from 'lucide-react'
import { createContext, Fragment, memo, useContext, useMemo, useState } from 'react'
import { AskThread, REWRITE_THREAD } from './AskThread'
@@ -18,6 +19,7 @@ import { estimatedHeight } from './itemHeight'
import { KindIcon } from './kindIcons'
import { PropertyGrid, PropertyItems, PropertyTable } from './PropertyGrid'
import { chatOf, EMPTY_CHAT, sameChat, unfinished } from './qa'
import { BlockIdContext } from './SegmentText'
import { shallowEqual, staticStore, useStore } from './store'
import { TextPreview } from './TextPreview'
import { foldsByCard, listId, tableId } from './viewState'
@@ -27,6 +29,8 @@ export interface EditState {
version: number
/** 这一版文档的源码和着色:编辑框按块的编辑范围从里面取 */
sourceText: SourceText
/** 这一版文档里各块在文件里的位置:编辑范围、发回改动时的行号按它算 */
placement: Placement
editing: ReadonlySet<string>
/** 刚点了「编辑」、要把光标放进去的块 */
focusId: string | null
@@ -139,6 +143,7 @@ export function treeState(state: Omit<BlockTreeState, 'folds'>, previous?: Block
}
const NO_FOLDS: ReadonlySet<string> = new Set()
const NO_CHILDREN: BlockView[] = []
const NO_REMOVED: readonly PlacedRemoved[] = []
/** 没有问 AI、重写注释的块树(定义卡片浮窗):一直不能用 */
const NO_CHAT: ReadableStore<ChatState> = staticStore(EMPTY_CHAT)
@@ -211,8 +216,9 @@ interface BlockShape {
focus: boolean
/** 编辑框每来一份新文档都要对一次内容;不在编辑的卡片不跟着版本重画 */
version?: number
/** 编辑着的卡片才有:这一版文档的源码和着色 */
/** 编辑着的卡片才有:这一版文档的源码和着色,这块在文件里的位置 */
sourceText?: SourceText
place?: Place
/** 调用树:扩展端标了能看调用树(BlockView.callTree)的块才有棵数 */
calls?: number
callOpen: boolean
@@ -235,6 +241,7 @@ function shapeOf(block: BlockView, state: BlockTreeState): BlockShape {
focus: state.edit?.focusId === block.id,
version: editing ? state.edit?.version : undefined,
sourceText: editing ? state.edit?.sourceText : undefined,
place: editing ? state.edit?.placement.of(block.id) : undefined,
calls: state.callTree && block.callTree ? state.callTree.counts.get(block.anchor) ?? 0 : undefined,
callOpen: state.callTree?.openAnchor === block.anchor,
}
@@ -273,7 +280,7 @@ function rewriteOf(block: BlockView, state: ChatState): RewriteShape {
return {
...(block.commentTarget && {
tool: {
fresh: block.commentTarget.kind === 'insert',
fresh: block.commentTarget === 'insert',
rewriting: chat.open && chat.asks.some(unfinished),
reopen: !chat.open && (chat.asks.length > 0 || chat.history.length > 0),
},
@@ -310,7 +317,7 @@ const BlockItem = memo(({ block, item }: { block: BlockView, item: boolean }): R
<>
{shape.run !== undefined && <UnchangedStrip count={shape.run} onOpen={() => tree.onOpenRun(block.id)} />}
{shape.shown && (
<Item item={item} line={block.startLine} height={item ? estimatedHeight(block, shape.open) : 0}>
<Item item={item} height={item ? estimatedHeight(block, shape.open) : 0}>
<BlockCard block={block} shape={shape} ask={ask} rewrite={rewriting} />
{/* 输入框、展开状态跟着块的记号走,文档刷新块 id 变了也不丢 */}
{qa && ask.chat && (
@@ -355,10 +362,10 @@ BlockItem.displayName = 'BlockItem'
* 顶层一块的外层(见 BlockTree 的 items);不套时原样。离视口远时浏览器跳过它里面的样式、排版和绘制(.cr-item 的 content-visibility),
* 没画过时按估的高度(height,见 estimatedHeight)占位,画过一次就记住真实高度。
*/
function Item({ item, line, height, children }: { item: boolean, line: number, height: number, children: ReactNode }): ReactElement {
function Item({ item, height, children }: { item: boolean, height: number, children: ReactNode }): ReactElement {
if (!item)
return <>{children}</>
return <div className="cr-item flex flex-col gap-2" data-line={line} style={{ containIntrinsicBlockSize: `auto ${height}px` }}>{children}</div>
return <div className="cr-item flex flex-col gap-2" style={{ containIntrinsicBlockSize: `auto ${height}px` }}>{children}</div>
}
/**
@@ -367,7 +374,7 @@ function Item({ item, line, height, children }: { item: boolean, line: number, h
* 标差异时每张卡片左边外侧一条改动色条,删掉的块收成一行放回原来的位置(见 RemovedCard);「只看改动」时连着没变的块折成一条(UnchangedStrip)。
* slotted:这一层是画在卡片属性表某一行下面的子块(BlockSlot),blocks 只有那几块。items:这一层每块套一层 .cr-item(只有页面上的顶层)。
*/
export function BlockLevel({ blocks, owner, slotted = false, items = false }: { blocks: BlockView[], owner?: BlockView, slotted?: boolean, items?: boolean }): ReactElement {
export function BlockLevel({ blocks, owner, slotted = false, items = false }: { blocks: BlockView[], owner?: CardView, slotted?: boolean, items?: boolean }): ReactElement {
const tree = useBlockTree()
// 挂在卡片子行上的子块画在卡片属性表里(slotted 的那一层),这一层不画;删掉的块都放回卡片后面这一层
const listed = useMemo(() => slotted ? blocks : blocks.filter(block => !block.slot), [blocks, slotted])
@@ -406,10 +413,11 @@ export function BlockLevel({ blocks, owner, slotted = false, items = false }: {
* 预览状态的内容:函数、类、枚举、导入、常量、变量块和库规则的卡片是属性表,其余是一行译文;
* 译文下面依次接着拆开的各项(跨几行写的字面量,排法同变量初始值下面的)、摊开实参的表(跨几行写的调用)、说明、几行属性(「注释」一行)。
* folds 是这张卡片里折叠着的(见 foldsByCard);标差异时 marks 是这块自己的内容改了哪些(页面上正是差异算的那一版才给)。
* children 是这块的子块,挂在子行上的画在属性表里(删掉的块不画子块,没有)。
*/
const BlockPreview = memo(({ block, folds, marks }: { block: BlockView, folds: ReadonlySet<string>, marks?: BlockMarks }): ReactElement => {
const BlockPreview = memo(({ block, children = NO_CHILDREN, folds, marks }: { block: CardView, children?: BlockView[], folds: ReadonlySet<string>, marks?: BlockMarks }): ReactElement => {
const tree = useBlockTree()
const slots = useMemo(() => cardSlots(block, folds, tree), [block, folds, tree])
const slots = useMemo(() => cardSlots(block, children, folds, tree), [block, children, folds, tree])
if ('properties' in block)
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 }
@@ -441,8 +449,8 @@ BlockPreview.displayName = 'BlockPreview'
* 挂在卡片子行上的子块(BlockSlot)怎么画:一行下面展开着的几块画成一层块(同块树);点那个子行的第一格展开它、
* 同一行里别的收起,展开着的再点收起。卡片没有挂在子行上的子块时没有。
*/
function cardSlots(block: BlockView, folds: ReadonlySet<string>, tree: BlockTreeHandle): GridSlots | undefined {
const blocks = block.children.filter(child => child.slot)
function cardSlots(block: CardView, children: readonly BlockView[], folds: ReadonlySet<string>, tree: BlockTreeHandle): GridSlots | undefined {
const blocks = children.filter(child => child.slot)
if (blocks.length === 0)
return undefined
return {
@@ -473,62 +481,63 @@ function BlockCard({ block, shape, ask, rewrite }: { block: BlockView, shape: Bl
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={anchor}
data-line={block.startLine}
data-editing={shape.editing ? '' : undefined}
data-change={change?.change}
>
{shape.diff && <ChangeBar change={change?.change} />}
{change?.outermost && <span className="cr-added-badge">新增</span>}
{shape.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={toggled ? '收起' : collapsed ? '展开子块' : '折叠子块'}
aria-expanded={toggled || !collapsed}
onClick={() => tree.onToggleFold(block.id)}
>
<Chevron size={14} strokeWidth={1.8} />
</button>
)
: <span className="w-3.5 shrink-0" />}
<KindIcon concepts={block.concepts} />
<div className="min-w-0 flex-1">
{edit && shape.version !== undefined && shape.sourceText
<BlockIdContext value={block.id}>
<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={anchor}
data-editing={shape.editing ? '' : undefined}
data-change={change?.change}
>
{shape.diff && <ChangeBar change={change?.change} />}
{change?.outermost && <span className="cr-added-badge">新增</span>}
{shape.hasChildren
? (
<CodeEditor
block={block}
sourceText={shape.sourceText}
version={shape.version}
autoFocus={shape.focus}
onAutoFocused={edit.onFocused}
onExit={() => edit.onToggleEdit(block.id)}
/>
<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={toggled ? '收起' : collapsed ? '展开子块' : '折叠子块'}
aria-expanded={toggled || !collapsed}
onClick={() => tree.onToggleFold(block.id)}
>
<Chevron size={14} strokeWidth={1.8} />
</button>
)
: <BlockPreview block={block} folds={shape.folds} marks={shape.current ? change?.marks : undefined} />}
: <span className="w-3.5 shrink-0" />}
<KindIcon concepts={block.concepts} />
<div className="min-w-0 flex-1">
{edit && shape.version !== undefined && shape.sourceText && shape.place
? (
<CodeEditor
sourceText={shape.sourceText}
place={shape.place}
version={shape.version}
autoFocus={shape.focus}
onAutoFocused={edit.onFocused}
onExit={() => edit.onToggleEdit(block.id)}
/>
)
: <BlockPreview block={block} children={block.children} folds={shape.folds} marks={shape.current ? change?.marks : undefined} />}
</div>
{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 />
已被筛选隐藏,临时显示
</span>
)}
{edit && (
<BlockTools
editing={shape.editing}
onToggleEdit={() => edit.onToggleEdit(block.id)}
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>
{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 />
已被筛选隐藏,临时显示
</span>
)}
{edit && (
<BlockTools
editing={shape.editing}
onToggleEdit={() => edit.onToggleEdit(block.id)}
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>
</BlockIdContext>
)
}