From b2e998665e20c01488e063be69529b6fd523182e Mon Sep 17 00:00:00 2001 From: cruldra Date: Tue, 13 Oct 2026 14:46:57 +1100 Subject: [PATCH] =?UTF-8?q?=E2=9A=A1=20perf(vscode):=20=E4=BB=A3=E7=A0=81?= =?UTF-8?q?=E9=98=85=E8=AF=BB=E9=A2=84=E8=A7=88=E5=B7=AE=E5=BC=82=E5=88=B0?= =?UTF-8?q?=E4=BA=86=E4=B8=8D=E5=86=8D=E7=BB=99=E6=AF=8F=E5=BC=A0=E5=8D=A1?= =?UTF-8?q?=E7=89=87=E5=A4=96=E9=9D=A2=E5=8C=85=E4=B8=80=E5=B1=82=EF=BC=9A?= =?UTF-8?q?=E6=94=B9=E5=8A=A8=E8=89=B2=E6=9D=A1=E7=94=BB=E5=9C=A8=E5=8D=A1?= =?UTF-8?q?=E7=89=87=E9=87=8C=E3=80=81=E6=8C=AA=E5=88=B0=E5=8D=A1=E7=89=87?= =?UTF-8?q?=E5=A4=96=E4=BE=A7=EF=BC=8C=E5=8D=A1=E7=89=87=E4=B8=8D=E6=8B=86?= =?UTF-8?q?=E6=8E=89=E9=87=8D=E5=BB=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- vscode/webview-ui/src/codeReader/App.test.tsx | 31 +++++++ .../webview-ui/src/codeReader/BlockTree.tsx | 89 +++++++++---------- .../webview-ui/src/codeReader/codeReader.css | 36 ++++---- 3 files changed, 93 insertions(+), 63 deletions(-) diff --git a/vscode/webview-ui/src/codeReader/App.test.tsx b/vscode/webview-ui/src/codeReader/App.test.tsx index a7e3894..a33d28c 100644 --- a/vscode/webview-ui/src/codeReader/App.test.tsx +++ b/vscode/webview-ui/src/codeReader/App.test.tsx @@ -277,6 +277,37 @@ describe('一条消息只重画受影响的卡片', () => { expect(container.querySelector('[data-offset="30"]')?.getAttribute('data-occurrence')).toBe('normal') }) + it('差异到了:卡片还是原来那个元素(不拆掉重建),改了的那块标上改动色条,删掉的块放回原处', async () => { + await load() + await deliver(copy(DOC)) + const cards = [...container.querySelectorAll('.cr-block')] + const removed = { ...block('9', 'gone'), translation: [{ text: 'gone', role: 'plain' as const }] } + renders.start() + await deliver({ + type: 'diff', + diff: { + base: 'HEAD', + version: 3, + blocks: { save: { change: 'mod', own: true } }, + removed: [{ parent: '', after: 'save', block: removed, line: 9, kindName: '函数' }], + stats: { add: 0, mod: 1, del: 1 }, + lineCount: 20, + }, + }) + const counts = renders.stop() + expect(counts.SegmentText).toBeUndefined() + const after = [...container.querySelectorAll('.cr-block:not(.cr-removed)')] + expect(after).toHaveLength(3) + after.forEach((card, index) => expect(card).toBe(cards[index])) + expect(container.querySelector('[data-id="1"] > .cr-change-bar')?.getAttribute('data-change')).toBe('mod') + expect(container.querySelector('[data-id="0"] > .cr-change-bar')?.hasAttribute('data-change')).toBe(false) + expect(container.querySelector('.cr-level')?.hasAttribute('data-diff')).toBe(true) + const gone = container.querySelector('.cr-removed')! + expect(gone.querySelector('.cr-change-bar')?.getAttribute('data-change')).toBe('del') + expect(cards[1].compareDocumentPosition(gone) & Node.DOCUMENT_POSITION_FOLLOWING).toBeTruthy() + expect(gone.compareDocumentPosition(cards[2]) & Node.DOCUMENT_POSITION_FOLLOWING).toBeTruthy() + }) + it('没展开、没说过的块下面不画问答区;展开一块只重画那一块', async () => { await load() await deliver({ type: 'qa', available: true, threads: [], path: [], subject: 'file', current: [], rewrites: [], rewriteCurrent: [] }) diff --git a/vscode/webview-ui/src/codeReader/BlockTree.tsx b/vscode/webview-ui/src/codeReader/BlockTree.tsx index dcbd6c1..ce5c2f1 100644 --- a/vscode/webview-ui/src/codeReader/BlockTree.tsx +++ b/vscode/webview-ui/src/codeReader/BlockTree.tsx @@ -1,4 +1,4 @@ -import type { CSSProperties, ReactElement, ReactNode } from 'react' +import type { CSSProperties, ReactElement } from 'react' import type { RewriteTool } from './BlockTools' import type { DiffTree, FoldedRuns, PlacedRemoved } from './diffView' import type { GridSlots } from './PropertyGrid' @@ -146,28 +146,22 @@ function hasShownChildren(block: BlockView, state: BlockTreeState): boolean { const CHANGE_NAMES = { add: '新增', mod: '修改', del: '删除' } as const -/** 标差异时卡片左边外侧的改动色条(没变的留一条透明的,同一层照样对齐);不标差异时原样。 */ -function ChangeRow({ diff, change, children }: { diff: boolean, change: keyof typeof CHANGE_NAMES | undefined, children: ReactNode }): ReactElement { - if (!diff) - return <>{children} - return ( -
- -
{children}
-
- ) +/** + * 标差异时卡片左边外侧的改动色条(没变的是一条透明的):画在卡片里、由 CSS 挪到卡片左边外侧(同一层的卡片让出它的位置,见 .cr-level[data-diff]), + * 差异到了、走了,卡片都还是原来那个元素,不拆掉重建。 + */ +function ChangeBar({ change }: { change: keyof typeof CHANGE_NAMES | undefined }): ReactElement { + return } /** 「只看改动」时连着没变的几块折成的一条「… N 块没变」,点开恢复这几块。 */ function UnchangedStrip({ count, onOpen }: { count: number, onOpen: () => void }): ReactElement { return ( - - - + ) } @@ -306,9 +300,7 @@ const BlockItem = memo(({ block }: { block: BlockView }): ReactElement => { {shape.run !== undefined && tree.onOpenRun(block.id)} />} {shape.shown && ( <> - - - + {/* 输入框、展开状态跟着块的记号走,文档刷新块 id 变了也不丢 */} {qa && ask.chat && ( 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 { removed, diff } = useStore(tree.state, state => ({ + removed: slotted ? NO_REMOVED : state.diff?.removed.get(owner?.id ?? '') ?? NO_REMOVED, + diff: state.diff !== undefined, + }), shallowEqual) const listedIds = new Set(listed.map(block => block.id)) // 删掉的块排在它原来前面那块后面;那块被筛掉了也照排,不跟着消失;那块画在卡片里时排到这一层最后 const removedAfter = (id: string | undefined): ReactElement[] => removed @@ -368,7 +363,7 @@ export function BlockLevel({ blocks, owner, slotted = false }: { blocks: BlockVi .filter(item => item.after !== undefined && !listedIds.has(item.after)) .map(item => ) return ( -
+
@@ -466,6 +461,7 @@ function BlockCard({ block, shape, ask, rewrite }: { block: BlockView, shape: Bl data-editing={shape.editing ? '' : undefined} data-change={change?.change} > + {shape.diff && } {change?.outermost && 新增} {shape.hasChildren ? ( @@ -529,31 +525,30 @@ function RemovedCard({ placed }: { placed: PlacedRemoved }): ReactElement { // 旧块的 id 是旧版本里的位置,换成页面上认删掉的块用的 id,属性行的 id 才不和现在的块撞上 const old = useMemo(() => ({ ...block, id: placed.id }), [block, placed.id]) return ( - -
- - -
-
- 已删除 · - {kindName} - {title && {` ${title}`}} -
- {open && ( -
- -
- )} +
+ + + +
+
+ 已删除 · + {kindName} + {title && {` ${title}`}}
+ {open && ( +
+ +
+ )}
- +
) } diff --git a/vscode/webview-ui/src/codeReader/codeReader.css b/vscode/webview-ui/src/codeReader/codeReader.css index 35501b2..facebc8 100644 --- a/vscode/webview-ui/src/codeReader/codeReader.css +++ b/vscode/webview-ui/src/codeReader/codeReader.css @@ -959,25 +959,24 @@ body { --cr-diff-del: var(--vscode-editorGutter-deletedBackground, #f85149); } -/* 卡片左边外侧的改动色条:3px 宽、和卡片等高;没变的卡片留一条透明的,同一层的卡片照样对齐 */ +/* + * 卡片左边外侧的改动色条:3px 宽、和卡片等高,空 5px 再是卡片;没变的卡片留一条透明的,同一层的卡片照样对齐。 + * 色条画在卡片里(标不标差异卡片都是同一个元素),标差异的那一层(data-diff)卡片、折起来的一段、问答区都往右让出 8px,色条挪进这 8px 里 + */ +.cr-level[data-diff] > div > :is(.cr-block, .cr-unchanged, .cr-qa) { + margin-left: 8px; +} .cr-change-bar { + position: absolute; + top: -1px; + bottom: -1px; + left: -9px; width: 3px; - flex-shrink: 0; border-radius: 1.5px; } .cr-change-bar[data-change="add"] { background: var(--cr-diff-add); } .cr-change-bar[data-change="mod"] { background: var(--cr-diff-mod); } .cr-change-bar[data-change="del"] { background: var(--cr-diff-del); } -/* 卡片包了一层色条以后不再和子层相邻:悬停卡片时子层竖线换色改按外层认;问答区往右缩到和卡片对齐 */ -.cr-change-row:has(> div > .cr-block:hover) + .cr-level > * > .cr-guide, -.cr-change-row:has(> div > .cr-block:hover) + .cr-qa + .cr-level > * > .cr-guide, -.cr-change-row:has(> div > .cr-block:hover) + .cr-qa + .cr-qa + .cr-level > * > .cr-guide { - background: var(--kind); -} -.cr-change-row + .cr-qa, -.cr-change-row + .cr-qa + .cr-qa { - margin-left: 8px; -} /* 新增的卡片(连同它的子卡片):底色淡绿;最外面那张左上角压着「新增」小标 */ .cr-block[data-change="add"] { @@ -1001,14 +1000,19 @@ body { } /* 删掉的块:红色虚线边、淡红底、半透明,没有编辑、问 AI 按钮;展开后下面是旧内容,整张划掉 */ +/* 半透明落在边框、底色和里面的内容上,不落在改动色条上(色条画在卡片里,同别的卡片的一样亮) */ .cr-removed { - border: 1px dashed color-mix(in srgb, var(--cr-diff-del) 80%, transparent); - background: color-mix(in srgb, var(--cr-diff-del) 6%, transparent); + border: 1px dashed color-mix(in srgb, var(--cr-diff-del) 48%, transparent); + background: color-mix(in srgb, var(--cr-diff-del) 3.6%, transparent); +} +.cr-removed > :not(.cr-change-bar) { opacity: 0.6; } .cr-block.cr-removed:hover { - border-color: var(--cr-diff-del); - background: color-mix(in srgb, var(--cr-diff-del) 10%, transparent); + border-color: color-mix(in srgb, var(--cr-diff-del) 80%, transparent); + background: color-mix(in srgb, var(--cr-diff-del) 8%, transparent); +} +.cr-block.cr-removed:hover > :not(.cr-change-bar) { opacity: 0.8; } .cr-removed-head {