From 7d6e7592c99267aecd9973a165910ff3507d1022 Mon Sep 17 00:00:00 2001 From: cruldra Date: Tue, 13 Oct 2026 13:31:20 +1100 Subject: [PATCH] =?UTF-8?q?=E2=9A=A1=20perf(vscode):=20=E5=88=87=E8=B5=B0?= =?UTF-8?q?=E7=9A=84=E9=A2=84=E8=A7=88=E6=96=87=E4=BB=B6=E6=94=B9=E4=BA=86?= =?UTF-8?q?=E4=B8=8D=E5=9C=A8=E5=90=8E=E5=8F=B0=E7=94=9F=E6=88=90=E6=96=87?= =?UTF-8?q?=E6=A1=A3=EF=BC=8C=E5=88=87=E5=9B=9E=E6=9D=A5=E5=86=8D=E8=A1=A5?= =?UTF-8?q?=E4=B8=80=E6=AC=A1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../codeReader/editor/previewEditor.test.ts | 71 +++++++++++++++++-- vscode/src/codeReader/editor/previewEditor.ts | 26 +++++-- 2 files changed, 88 insertions(+), 9 deletions(-) diff --git a/vscode/src/codeReader/editor/previewEditor.test.ts b/vscode/src/codeReader/editor/previewEditor.test.ts index a1b206f..9da09f6 100644 --- a/vscode/src/codeReader/editor/previewEditor.test.ts +++ b/vscode/src/codeReader/editor/previewEditor.test.ts @@ -253,6 +253,13 @@ function tab() { } } +/** 别处改了这份文件,文档变成 version 版:交给最近打开的预览听着的那个回调 */ +function edit(document: { version: number }, version: number): void { + document.version = version + const changed = vi.mocked(workspace.onDidChangeTextDocument).mock.calls.at(-1)![0] + changed({ document, contentChanges: [{ rangeOffset: 0, rangeLength: 0, text: '// ' }] } as never) +} + const comparedWithHead = expect.objectContaining({ type: 'diff', diff: expect.objectContaining({ base: '上一次提交' }) }) describe('预览对比什么看这个标签页是从哪儿打开的', () => { @@ -365,7 +372,7 @@ describe('标签页切走时页面留着', () => { expect(commands.executeCommand).not.toHaveBeenCalledWith('setContext', OCCURRENCES_CONTEXT, false) }) - it('切走期间文件在别处改了:照常把新的文档发给留着的页面', async () => { + it('切走期间文件在别处改了:不生成、不发文档,切回来生成一次发出去', async () => { const { provider, document } = opening() const page = tab() provider.resolveCustomTextEditor(document as never, page.panel) @@ -373,9 +380,65 @@ describe('标签页切走时页面留着', () => { page.hide() page.clear() - document.version = 2 - const edited = vi.mocked(workspace.onDidChangeTextDocument).mock.calls.at(-1)![0] - edited({ document, contentChanges: [{ rangeOffset: 0, rangeLength: 0, text: '// ' }] } as never) + vi.mocked(documentMessage).mockClear() + edit(document, 2) + edit(document, 3) + await new Promise(resolve => setTimeout(resolve, 200)) + expect(documentMessage).not.toHaveBeenCalled() + expect(page.posted()).toEqual([]) + + page.show() + await vi.waitFor(() => expect(page.posted()).toContainEqual(expect.objectContaining({ type: 'document', version: 3 }))) + expect(documentMessage).toHaveBeenCalledTimes(1) + }) + + it('改完还没来得及生成就切走了:排着的那次不在后台跑,切回来再生成', async () => { + const { provider, document } = opening() + const page = tab() + provider.resolveCustomTextEditor(document as never, page.panel) + await page.send({ type: 'ready' }) + + page.clear() + vi.mocked(documentMessage).mockClear() + edit(document, 2) + page.hide() + await new Promise(resolve => setTimeout(resolve, 200)) + expect(documentMessage).not.toHaveBeenCalled() + + page.show() + await vi.waitFor(() => expect(page.posted()).toContainEqual(expect.objectContaining({ type: 'document', version: 2 }))) + expect(documentMessage).toHaveBeenCalledTimes(1) + }) + + it('切回来时还要滚到别处(跳定义打到它):等补发的文档一起带过去,不先滚旧的', async () => { + const { provider, document } = opening() + const page = tab() + provider.resolveCustomTextEditor(document as never, page.panel) + await page.send({ type: 'ready' }) + + page.hide() + edit(document, 2) + await new Promise(resolve => setTimeout(resolve, 200)) + page.clear() + // 补生成要一会儿:跳过来的位置在生成完之前就到了 + const generate = vi.mocked(documentMessage).getMockImplementation()! + vi.mocked(documentMessage).mockImplementationOnce(async (...args) => { + await new Promise(resolve => setTimeout(resolve, 50)) + return generate(...args) + }) + await provider.open(document.uri as never, { reveal: { line: 1 } }, async () => page.show()) + await vi.waitFor(() => expect(page.posted()).toContainEqual(expect.objectContaining({ type: 'document', version: 2, reveal: { line: 1 } }))) + expect(page.posted()).not.toContainEqual(expect.objectContaining({ type: 'reveal' })) + }) + + it('分屏同时看着:文件改了照常马上更新', async () => { + const { provider, document } = opening() + const page = tab() + provider.resolveCustomTextEditor(document as never, page.panel) + await page.send({ type: 'ready' }) + + page.clear() + edit(document, 2) await vi.waitFor(() => expect(page.posted()).toContainEqual(expect.objectContaining({ type: 'document', version: 2 }))) }) diff --git a/vscode/src/codeReader/editor/previewEditor.ts b/vscode/src/codeReader/editor/previewEditor.ts index 18d79ad..2df310d 100644 --- a/vscode/src/codeReader/editor/previewEditor.ts +++ b/vscode/src/codeReader/editor/previewEditor.ts @@ -104,7 +104,7 @@ export class PreviewEditorProvider implements CustomTextEditorProvider { ) {} /** - * 注册成预览编辑器。标签页切走时页面留着(`retainContextWhenHidden`):切回来不用重新生成、重发文档,页面也不用整份重画, + * 注册成预览编辑器。标签页切走时页面留着(`retainContextWhenHidden`):切回来不用重新生成、重发文档(切走期间改过的才补一次),页面也不用整份重画, * 代价是每个开着的预览切走后仍占着它那个页面的内存。 */ register(): Disposable { @@ -183,10 +183,14 @@ export class PreviewEditorProvider implements CustomTextEditorProvider { class Preview { /** - * 页面发过 ready(装好了、能收消息)之后才发消息。标签页切走时页面留着,照常收消息,切回来什么都不用补; + * 页面发过 ready(装好了、能收消息)之后才发消息。标签页切走时页面留着,照常收消息; * 页面真的重新加载了(webview 进程崩了重建、开发者重载 webview)会再发 ready,那时整份重发 */ private ready = false + /** 切走期间该重新生成的文档没生成(文件改了、规则变了):重新看得见时补一次;没过期的切回来什么都不用做 */ + private outdated = false + /** 页面上那份文档的版本:还不是眼下这一版时(正在生成、切走期间改过),要滚到的位置等新的那份一起带过去 */ + private shown: number | undefined /** 侧栏「问 AI」打开的:页面准备好后把光标放进顶部输入框 */ private pendingPathFocus = false /** 解析是异步的:只发最后一次请求的结果,慢的旧结果不能盖掉新的 */ @@ -226,7 +230,7 @@ class Preview { /** 这份文件相比对比基准改了什么:发完文档再算、补发 */ private readonly diff: PreviewDiff /** - * 随下一份真正发出去的文档走、只走一次:新预览第一份文档要滚到的位置,或页面 ready 之前又来的 reveal。 + * 随下一份真正发出去的文档走、只走一次:新预览第一份文档要滚到的位置,或页面上还不是眼下这一版文档时来的 reveal。 * 之后页面真的重新加载了,按 webview 自己记的位置恢复 */ private pendingReveal: Reveal | undefined @@ -271,6 +275,10 @@ class Preview { void this.diff.update(this.sent) }), panel.webview.onDidReceiveMessage((msg: WebviewToExtension) => this.handleMessage(msg)), + panel.onDidChangeViewState(() => { + if (panel.visible && this.outdated) + void this.push() + }), workspace.onDidChangeTextDocument((e) => { if (e.document !== document || e.contentChanges.length === 0) return @@ -366,12 +374,19 @@ class Preview { clearTimeout(this.timer) if (!this.ready) return + // 切走的预览不在后台生成,后面跟着文档的查询也不起;分屏同时看得见的照常马上生成 + if (!this.panel.visible) { + this.outdated = true + return + } + this.outdated = false const request = ++this.latest // 路径页、功能对话页:文档是空的,只在顶部聊天 if (this.document.uri.scheme === PATH_PREVIEW_SCHEME) { this.sent = undefined const reveal = this.pendingReveal this.pendingReveal = undefined + this.shown = this.document.version this.post({ type: 'document', version: this.document.version, @@ -401,6 +416,7 @@ class Preview { this.imports = imports const reveal = this.pendingReveal this.pendingReveal = undefined + this.shown = version this.post({ ...message, names: this.names.known(version), ...(reveal && { reveal }) }) if (this.sent) void this.diff.update(this.sent) @@ -513,9 +529,9 @@ class Preview { this.focusPathAsk() } - /** 滚到 reveal:页面 ready 了就直接发,还没 ready 就等它发 ready 后随文档带过去。 */ + /** 滚到 reveal:页面上已是眼下这一版文档就直接发;还不是(还没 ready、正在生成、切走期间改过)就随新的那份文档带过去。 */ private reveal(reveal: Reveal): void { - if (this.ready) + if (this.shown === this.document.version) this.post({ type: 'reveal', ...reveal }) else this.pendingReveal = reveal