⚡ 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

@@ -9,6 +9,58 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { App } from './App'
import { createViewStateStore } from './viewState'
/**
* 一次次提交里哪些组件真的画了(新挂上的不算):赶在 react-dom 加载之前挂上 React 开发者工具的钩子,每次提交顺着 fiber 树数。
* 跳过重画的整棵子树(fiber 和上一次的共用子节点)不往下数。
*/
const renders = vi.hoisted(() => {
interface Fiber { type: unknown, elementType: unknown, alternate: Fiber | null, flags: number, child: Fiber | null, sibling: Fiber | null }
const counts = new Map<string, number>()
let counting = false
/** 组件叫什么:memo 包着的看它的 displayName,别的看函数名 */
const nameOf = (fiber: Fiber): string | undefined => {
const memoName = (fiber.elementType as { displayName?: string } | null)?.displayName
return memoName ?? (typeof fiber.type === 'function' ? fiber.type.name : undefined)
}
const walk = (root: { current: Fiber }): void => {
const stack = [root.current]
while (stack.length > 0) {
const fiber = stack.pop()!
const name = nameOf(fiber)
// flags 的第 1 位(PerformedWork):这次真的调用了组件函数
if (name && fiber.alternate && (fiber.flags & 1))
counts.set(name, (counts.get(name) ?? 0) + 1)
if (fiber.alternate && fiber.child === fiber.alternate.child)
continue
for (let child = fiber.child; child; child = child.sibling)
stack.push(child)
}
}
Object.assign(globalThis, {
__REACT_DEVTOOLS_GLOBAL_HOOK__: {
supportsFiber: true,
isDisabled: false,
renderers: new Map(),
inject: () => 1,
checkDCE: () => {},
onScheduleFiberRoot: () => {},
onCommitFiberRoot: (_id: number, root: { current: Fiber }) => counting && walk(root),
onCommitFiberUnmount: () => {},
onPostCommitFiberRoot: () => {},
},
})
return {
start: () => {
counts.clear()
counting = true
},
stop: (): Record<string, number> => {
counting = false
return Object.fromEntries(counts)
},
}
})
/** 扩展端那头:页面发出去的消息、页面挂着的收消息回调,以及 webview 自己存的状态(页面重新加载时还在) */
const host = vi.hoisted(() => ({
posted: [] as unknown[],
@@ -171,3 +223,70 @@ describe('代码配色跟着当前颜色主题', () => {
expect(style.getPropertyValue('--cr-kind-loop')).toBe('#AF00DB')
})
})
describe('一条消息只重画受影响的卡片', () => {
/** 三张卡片,译文里各有一个带下标的名字(下标 10、20、30) */
function named(id: string, name: string, offset: number, text = '调用'): BlockView {
return { ...block(id, name), translation: [{ text, role: 'keyword' }, { text: name, role: 'function', offset }] }
}
const DOC = documentOf(3, [named('0', 'load', 10), named('1', 'save', 20), named('2', 'drop', 30)])
/** 扩展端每次发来的都是新对象:深拷一份 */
const copy = <T,>(value: T): T => JSON.parse(JSON.stringify(value)) as T
async function rendered(msg: ExtensionToWebview): Promise<Record<string, number>> {
renders.start()
await deliver(msg)
return renders.stop()
}
it('中文名到了:只重画换了字的那段文字,卡片不动', async () => {
await load()
await deliver(copy(DOC))
const counts = await rendered({ type: 'displayNames', version: 3, names: [{ offset: 20, length: 4, name: '保存' }] })
expect(counts).toEqual({ SegmentText: 1 })
expect(container.querySelector('[data-offset="20"]')?.textContent).toBe('保存')
})
it('调用树棵数到了:只重画棵数变了的那几块', async () => {
await load()
await deliver(copy(DOC))
const counts = await rendered({ type: 'callTreeCounts', version: 3, counts: { save: 2 } })
expect(counts.BlockItem).toBe(1)
expect(counts.SegmentText).toBeUndefined()
expect(container.querySelector('[data-call-tree="save"]')?.textContent).toBe('2')
})
it('同一版文档重发、只改了一块的新版本:内容没变的卡片不重画', async () => {
await load()
await deliver(copy(DOC))
expect(await rendered(copy(DOC))).not.toHaveProperty('BlockItem')
const edited = documentOf(4, [named('0', 'load', 10), named('1', 'save', 20, '调用了'), named('2', 'drop', 30)])
const counts = await rendered(copy(edited))
expect(counts.BlockItem).toBe(1)
expect(container.querySelector('[data-id="1"]')?.textContent).toContain('调用了')
})
it('单击名字高亮:只重画高亮落到的那段文字', async () => {
await load()
await deliver(copy(DOC))
await act(async () => container.querySelector<HTMLElement>('[data-offset="30"]')!.click())
const asked = [...host.posted].reverse().find((msg): msg is Extract<WebviewToExtension, { type: 'findOccurrences' }> => (msg as WebviewToExtension).type === 'findOccurrences')!
const counts = await rendered({ type: 'occurrencesFound', id: asked.id, occurrences: [{ start: 30, end: 34, kind: 'read' }] })
expect(counts.SegmentText).toBe(1)
expect(counts.BlockItem).toBeUndefined()
expect(container.querySelector('[data-offset="30"]')?.getAttribute('data-occurrence')).toBe('normal')
})
it('没展开、没说过的块下面不画问答区;展开一块只重画那一块', async () => {
await load()
await deliver({ type: 'qa', available: true, threads: [], path: [], subject: 'file', current: [], rewrites: [], rewriteCurrent: [] })
await deliver(copy(DOC))
// 顶部那条一直摊开
expect(container.querySelectorAll('.cr-qa')).toHaveLength(1)
renders.start()
await act(async () => container.querySelector<HTMLButtonElement>('[data-id="1"] [aria-label="问 AI"]')!.click())
const counts = renders.stop()
expect(counts.BlockItem).toBe(1)
expect(container.querySelectorAll('.cr-qa')).toHaveLength(2)
})
})