From 7b11eaef10452acea2a1b61d5cd865c5ac754eba Mon Sep 17 00:00:00 2001 From: David Wengier Date: Wed, 5 Nov 2025 16:21:01 +1100 Subject: [PATCH 1/5] Wati for Html buffer updates before making requests --- CHANGELOG.md | 1 + src/lsptoolshost/razor/htmlDocument.ts | 16 +++++++++++++++- src/lsptoolshost/razor/htmlDocumentManager.ts | 4 +--- src/lsptoolshost/razor/razorEndpoints.ts | 7 +++++++ 4 files changed, 24 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 564113950d..bea1cd3b8e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -29,6 +29,7 @@ * Fix(ish) formatting of RenderFragments (C# templates) (PR: [#12397](https://github.com/dotnet/razor/pull/12397)) * Drop Html edits that would split a C# literal across multiple lines (PR: [#12396](https://github.com/dotnet/razor/pull/12396)) * Fix completion resolve for provisional completion (PR: [#12403](https://github.com/dotnet/razor/pull/12403)) +* Wait for Html buffer updates before making requests (PR: [#8748](https://github.com/dotnet/vscode-csharp/pull/8748)) # 2.96.x * Update Debugger to v2.95.0 (PR: [#8710](https://github.com/dotnet/vscode-csharp/pull/8710)) diff --git a/src/lsptoolshost/razor/htmlDocument.ts b/src/lsptoolshost/razor/htmlDocument.ts index eecf19d905..927d28c70e 100644 --- a/src/lsptoolshost/razor/htmlDocument.ts +++ b/src/lsptoolshost/razor/htmlDocument.ts @@ -10,6 +10,7 @@ export class HtmlDocument { public readonly path: string; private content = ''; private checksum = ''; + private previousVersion = -1; public constructor(public readonly uri: vscode.Uri, checksum: string) { this.path = getUriPath(uri); @@ -24,8 +25,21 @@ export class HtmlDocument { return this.checksum; } - public setContent(checksum: string, content: string) { + public async setContent(checksum: string, content: string) { + var document = await vscode.workspace.openTextDocument(this.uri); + // Capture the version _before_ the change, so we can know for sure if it's been seen + this.previousVersion = document.version; this.checksum = checksum; this.content = content; } + + public async waitForBufferUpdate() { + var document = await vscode.workspace.openTextDocument(this.uri); + + // Wait for VS Code to process any previous content change. We don't care about finding + // a specific version, just that it's moved on from the previous one. + while (document.version === this.previousVersion) { + await new Promise((resolve) => setTimeout(resolve, 10)); + } + } } diff --git a/src/lsptoolshost/razor/htmlDocumentManager.ts b/src/lsptoolshost/razor/htmlDocumentManager.ts index 8e73120f50..cadae0a929 100644 --- a/src/lsptoolshost/razor/htmlDocumentManager.ts +++ b/src/lsptoolshost/razor/htmlDocumentManager.ts @@ -79,9 +79,7 @@ export class HtmlDocumentManager { this.logger.logTrace(`New content for '${uri}', updating '${document.path}', checksum '${checksum}'.`); - await vscode.workspace.openTextDocument(document.uri); - - document.setContent(checksum, text); + await document.setContent(checksum, text); this.contentProvider.fireDidChange(document.uri); } diff --git a/src/lsptoolshost/razor/razorEndpoints.ts b/src/lsptoolshost/razor/razorEndpoints.ts index ecd5a76646..3852936cd1 100644 --- a/src/lsptoolshost/razor/razorEndpoints.ts +++ b/src/lsptoolshost/razor/razorEndpoints.ts @@ -252,6 +252,13 @@ export function registerRazorEndpoints( return undefined; } + // We know that we've got the right document, and we've been told about the right content by the server, + // but all we can be sure of at this point is that we've fired the change event for it. The event firing + // is async, and the didChange notification that it would generate is a notification, so doesn't necessarily + // block. Before we actually make a call to the Html server, we should at least make sure that the document + // version is not still the same as before we updated the content. + await document.waitForBufferUpdate(); + return invocation(document, params.request); }); } From f973fa22a51127bd610b5b1a8268e17c492b763a Mon Sep 17 00:00:00 2001 From: David Wengier Date: Wed, 5 Nov 2025 16:31:46 +1100 Subject: [PATCH 2/5] =?UTF-8?q?Too=20used=20to=20C#=20=F0=9F=A4=A6?= =?UTF-8?q?=E2=80=8D=E2=99=82=EF=B8=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/lsptoolshost/razor/htmlDocument.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/lsptoolshost/razor/htmlDocument.ts b/src/lsptoolshost/razor/htmlDocument.ts index 927d28c70e..cc63075caf 100644 --- a/src/lsptoolshost/razor/htmlDocument.ts +++ b/src/lsptoolshost/razor/htmlDocument.ts @@ -26,7 +26,7 @@ export class HtmlDocument { } public async setContent(checksum: string, content: string) { - var document = await vscode.workspace.openTextDocument(this.uri); + const document = await vscode.workspace.openTextDocument(this.uri); // Capture the version _before_ the change, so we can know for sure if it's been seen this.previousVersion = document.version; this.checksum = checksum; @@ -34,7 +34,7 @@ export class HtmlDocument { } public async waitForBufferUpdate() { - var document = await vscode.workspace.openTextDocument(this.uri); + const document = await vscode.workspace.openTextDocument(this.uri); // Wait for VS Code to process any previous content change. We don't care about finding // a specific version, just that it's moved on from the previous one. From 82537a498feb80f3cc67bca09962195b61627079 Mon Sep 17 00:00:00 2001 From: David Wengier Date: Wed, 5 Nov 2025 16:31:58 +1100 Subject: [PATCH 3/5] Wait a little less --- src/lsptoolshost/razor/htmlDocument.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lsptoolshost/razor/htmlDocument.ts b/src/lsptoolshost/razor/htmlDocument.ts index cc63075caf..fb8db974f3 100644 --- a/src/lsptoolshost/razor/htmlDocument.ts +++ b/src/lsptoolshost/razor/htmlDocument.ts @@ -39,7 +39,7 @@ export class HtmlDocument { // Wait for VS Code to process any previous content change. We don't care about finding // a specific version, just that it's moved on from the previous one. while (document.version === this.previousVersion) { - await new Promise((resolve) => setTimeout(resolve, 10)); + await new Promise((resolve) => setTimeout(resolve, 5)); } } } From ce999a56f055fae9e436785fb11d4cd1efa8c7ad Mon Sep 17 00:00:00 2001 From: David Wengier Date: Thu, 6 Nov 2025 10:44:36 +1100 Subject: [PATCH 4/5] Wait for a didChange rather than polling --- src/lsptoolshost/razor/htmlDocument.ts | 16 +--- src/lsptoolshost/razor/htmlDocumentManager.ts | 74 ++++++++++++++++++- src/lsptoolshost/razor/razorEndpoints.ts | 7 -- 3 files changed, 71 insertions(+), 26 deletions(-) diff --git a/src/lsptoolshost/razor/htmlDocument.ts b/src/lsptoolshost/razor/htmlDocument.ts index fb8db974f3..eecf19d905 100644 --- a/src/lsptoolshost/razor/htmlDocument.ts +++ b/src/lsptoolshost/razor/htmlDocument.ts @@ -10,7 +10,6 @@ export class HtmlDocument { public readonly path: string; private content = ''; private checksum = ''; - private previousVersion = -1; public constructor(public readonly uri: vscode.Uri, checksum: string) { this.path = getUriPath(uri); @@ -25,21 +24,8 @@ export class HtmlDocument { return this.checksum; } - public async setContent(checksum: string, content: string) { - const document = await vscode.workspace.openTextDocument(this.uri); - // Capture the version _before_ the change, so we can know for sure if it's been seen - this.previousVersion = document.version; + public setContent(checksum: string, content: string) { this.checksum = checksum; this.content = content; } - - public async waitForBufferUpdate() { - const document = await vscode.workspace.openTextDocument(this.uri); - - // Wait for VS Code to process any previous content change. We don't care about finding - // a specific version, just that it's moved on from the previous one. - while (document.version === this.previousVersion) { - await new Promise((resolve) => setTimeout(resolve, 5)); - } - } } diff --git a/src/lsptoolshost/razor/htmlDocumentManager.ts b/src/lsptoolshost/razor/htmlDocumentManager.ts index cadae0a929..f2fe885f98 100644 --- a/src/lsptoolshost/razor/htmlDocumentManager.ts +++ b/src/lsptoolshost/razor/htmlDocumentManager.ts @@ -17,6 +17,13 @@ import { UriConverter } from '../utils/uriConverter'; export class HtmlDocumentManager { private readonly htmlDocuments: { [hostDocumentPath: string]: HtmlDocument } = {}; private readonly contentProvider: HtmlDocumentContentProvider; + private readonly pendingUpdates: { + [documentPath: string]: { + promise: Promise; + resolve: () => void; + reject: (error: any) => void; + }; + } = {}; private readonly razorDocumentClosedRequest: RequestType = new RequestType( 'razor/documentClosed' @@ -35,6 +42,20 @@ export class HtmlDocumentManager { } public register() { + const didChangeRegistration = vscode.workspace.onDidChangeTextDocument((e) => { + // Check if this document is being monitored for updates + if (e.document.uri.scheme === HtmlDocumentContentProvider.scheme) { + const documentPath = getUriPath(e.document.uri); + const pendingUpdate = this.pendingUpdates[documentPath]; + + if (pendingUpdate) { + // Document has been updated, resolve the promise + pendingUpdate.resolve(); + delete this.pendingUpdates[documentPath]; + } + } + }); + const didCloseRegistration = vscode.workspace.onDidCloseTextDocument(async (document) => { // We log when a virtual document is closed just in case it helps track down future bugs if (document.uri.scheme === HtmlDocumentContentProvider.scheme) { @@ -63,7 +84,7 @@ export class HtmlDocumentManager { this.contentProvider ); - return vscode.Disposable.from(didCloseRegistration, providerRegistration); + return vscode.Disposable.from(didChangeRegistration, didCloseRegistration, providerRegistration); } public async updateDocumentText(uri: vscode.Uri, checksum: string, text: string) { @@ -79,7 +100,20 @@ export class HtmlDocumentManager { this.logger.logTrace(`New content for '${uri}', updating '${document.path}', checksum '${checksum}'.`); - await document.setContent(checksum, text); + // Create a promise for this document update + let resolve: () => void; + let reject: (error: any) => void; + const updatePromise = new Promise((res, rej) => { + resolve = res; + reject = rej; + }); + + this.pendingUpdates[document.path] = { promise: updatePromise, resolve: resolve!, reject: reject! }; + + // Now update the document and fire the change event so VS Code will inform the Html language client. + await vscode.workspace.openTextDocument(document.uri); + + document.setContent(checksum, text); this.contentProvider.fireDidChange(document.uri); } @@ -90,6 +124,13 @@ export class HtmlDocumentManager { if (document) { this.logger.logTrace(`Removing '${document.uri}' from the document manager.`); + // Clean up any pending update promises for this document + const pendingUpdate = this.pendingUpdates[document.path]; + if (pendingUpdate) { + pendingUpdate.reject(new Error('Document was closed before update completed')); + delete this.pendingUpdates[document.path]; + } + delete this.htmlDocuments[document.path]; } } @@ -109,10 +150,35 @@ export class HtmlDocumentManager { return undefined; } - // No checksum, just give them the latest document and hope they know what to do with it. - await vscode.workspace.openTextDocument(document.uri); + if (checksum) { + // If checksum is supplied, that means we're getting this document because we're about to call an LSP method + // on it. We know that we've got the right document, and we've been told about the right content by the server, + // but all we can be sure of at this point is that we've fired the change event for it. The event firing + // is async, and the didChange notification that it would generate is a notification, so doesn't necessarily + // block. Before we actually make a call to the Html server, we should at least make sure that the document + // update has been seen by VS Code. We can't get access to the Html language client specifically to check if it + // has seen it, but we can trust that ordering will be preserved at least. + const pendingUpdate = this.pendingUpdates[document.path]; + if (pendingUpdate) { + try { + // Wait for the update promise with a 5 second timeout + await Promise.race([ + pendingUpdate.promise, + new Promise((_, reject) => + setTimeout(() => reject(new Error('Document update timeout')), 5000) + ), + ]); + } catch (error) { + this.logger.logWarning(`Failed to wait for document update: ${error}`); + } finally { + // Clean up the promise reference + delete this.pendingUpdates[document.path]; + } + } + } + return document; } diff --git a/src/lsptoolshost/razor/razorEndpoints.ts b/src/lsptoolshost/razor/razorEndpoints.ts index 3852936cd1..ecd5a76646 100644 --- a/src/lsptoolshost/razor/razorEndpoints.ts +++ b/src/lsptoolshost/razor/razorEndpoints.ts @@ -252,13 +252,6 @@ export function registerRazorEndpoints( return undefined; } - // We know that we've got the right document, and we've been told about the right content by the server, - // but all we can be sure of at this point is that we've fired the change event for it. The event firing - // is async, and the didChange notification that it would generate is a notification, so doesn't necessarily - // block. Before we actually make a call to the Html server, we should at least make sure that the document - // version is not still the same as before we updated the content. - await document.waitForBufferUpdate(); - return invocation(document, params.request); }); } From 648d8ea7bcfa5dd27824fda7babc66839c27a29d Mon Sep 17 00:00:00 2001 From: David Wengier Date: Thu, 6 Nov 2025 14:04:55 +1100 Subject: [PATCH 5/5] Move changelog entry --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 19019b8d87..038e27e796 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ - Debug from .csproj and .sln [#5876](https://github.com/dotnet/vscode-csharp/issues/5876) # 2.101.x +* Wait for Html buffer updates before making requests (PR: [#8748](https://github.com/dotnet/vscode-csharp/pull/8748)) # 2.97.x * Add integration test for restore of file-based programs (PR: [#8470](https://github.com/dotnet/vscode-csharp/pull/8470)) @@ -37,7 +38,6 @@ * Fix(ish) formatting of RenderFragments (C# templates) (PR: [#12397](https://github.com/dotnet/razor/pull/12397)) * Drop Html edits that would split a C# literal across multiple lines (PR: [#12396](https://github.com/dotnet/razor/pull/12396)) * Fix completion resolve for provisional completion (PR: [#12403](https://github.com/dotnet/razor/pull/12403)) -* Wait for Html buffer updates before making requests (PR: [#8748](https://github.com/dotnet/vscode-csharp/pull/8748)) # 2.96.x * Update Debugger to v2.95.0 (PR: [#8710](https://github.com/dotnet/vscode-csharp/pull/8710))