Skip to content

Ensure the language client is always ready before using it - #14617

Open
Bob Brown (bobbrow) wants to merge 10 commits into
mainfrom
bobbrow/languageClientRefactor
Open

Ensure the language client is always ready before using it#14617
Bob Brown (bobbrow) wants to merge 10 commits into
mainfrom
bobbrow/languageClientRefactor

Conversation

@bobbrow

@bobbrow Bob Brown (bobbrow) commented Jul 29, 2026

Copy link
Copy Markdown
Member

There are some crash reports indicating that some events attempting to communicate with cpptools are firing before the language client is ready to service them. While investigating, I noticed that several events are not awaiting the ready event.

This PR wraps the LanguageClient interface in a class that ensures the client is ready before making any RPC calls into cpptools. Some other minor cleanup in the provider classes is included. I did not root out all places where DefaultClient.ready is being unnecessarily awaited as it will require more testing, but those remaining are harmless.

Note: I did not see a need to keep the enqueue functionality based on my findings. On the native side, we already defer all messages in a queue until configuration happens so we can let the messages come in the order they are queued on the TypeScript side. Historically, I remember there being a need for "blocking" the language client "Middleware" (protocolFilter.ts), but that was already removed a while ago. enqueue is incompatible with this change as it results in deadlocked awaiting (an enqueued task that makes an RPC call to cpptools).

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR introduces a TypeScript wrapper around the VS Code LanguageClient to ensure RPC calls into cpptools are gated on a single “ready” signal, aiming to prevent crashes caused by early event/RPC usage before the language client is usable.

Changes:

  • Added a LanguageClient wrapper that awaits readiness for sendRequest/sendNotification, and updated the core client startup flow to use it.
  • Removed many explicit await client.ready call sites across providers/commands, relying on the wrapper to enforce readiness.
  • Removed the enqueue-based sequencing from client/extension event wiring and performed minor provider cleanup.

Reviewed changes

Copilot reviewed 19 out of 19 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
Extension/src/LanguageServer/Providers/workspaceSymbolProvider.ts Constructor simplification; relies on centralized readiness behavior.
Extension/src/LanguageServer/Providers/semanticTokensProvider.ts Formatting-only style adjustment.
Extension/src/LanguageServer/Providers/renameProvider.ts Removes explicit readiness await in favor of wrapper.
Extension/src/LanguageServer/Providers/onTypeFormattingEditProvider.ts Removes explicit readiness await in favor of wrapper.
Extension/src/LanguageServer/Providers/HoverProvider.ts Removes explicit readiness await in favor of wrapper.
Extension/src/LanguageServer/Providers/foldingRangeProvider.ts Removes explicit readiness await in favor of wrapper.
Extension/src/LanguageServer/Providers/findAllReferencesProvider.ts Removes explicit readiness await in favor of wrapper.
Extension/src/LanguageServer/Providers/documentSymbolProvider.ts Switches to using client.languageClient.sendRequest without manual readiness await.
Extension/src/LanguageServer/Providers/documentRangeFormattingEditProvider.ts Removes explicit readiness await in favor of wrapper.
Extension/src/LanguageServer/Providers/documentFormattingEditProvider.ts Removes explicit readiness await in favor of wrapper.
Extension/src/LanguageServer/Providers/CopilotHoverProvider.ts Removes explicit readiness await in favor of wrapper.
Extension/src/LanguageServer/Providers/codeActionProvider.ts Removes explicit readiness await in favor of wrapper.
Extension/src/LanguageServer/Providers/callHierarchyProvider.ts Removes explicit readiness awaits in favor of wrapper.
Extension/src/LanguageServer/protocolFilter.ts Stops manually awaiting client.ready before sending visible-editor updates.
Extension/src/LanguageServer/languageClient.ts New wrapper class that gates requests/notifications on a “ready” signal.
Extension/src/LanguageServer/extension.ts Removes enqueue usage from editor event hooks; removes explicit ready awaits for some commands.
Extension/src/LanguageServer/codeAnalysis.ts Swaps to wrapper LanguageClient and adjusts languageclient imports accordingly.
Extension/src/LanguageServer/clientCollection.ts Removes await newClient.ready during crash-recovery ownership transfer.
Extension/src/LanguageServer/client.ts Central refactor to use wrapper, remove enqueue, and shift readiness responsibility to the wrapper.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread Extension/src/LanguageServer/client.ts
Comment on lines 3115 to 3116
public async restartIntelliSenseForFile(document: vscode.TextDocument): Promise<void> {
await this.ready;
return this.languageClient.sendNotification(RestartIntelliSenseForFileNotification, this.languageClient.code2ProtocolConverter.asTextDocumentIdentifier(document)).catch(logAndReturn.undefined);

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ready does not guard against the case you describe. In the unlikely event that a rescan is requested before the language client is ready, the exception should be caught and this will be a no-op. The language server would then proceed to initialize and parse the file for IntelliSense and it will appear as though the rescan happened.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✨Copilot (agent8): This is still reachable on the current head. JavaScript evaluates this.languageClient.code2ProtocolConverter.asTextDocumentIdentifier(...) before sendNotification() is called, so the wrapper's await this.ready is never reached and the method-level .catch(...) is never attached. Because the containing function is async, the command returns a rejected Promise rather than crashing synchronously, but VS Code surfaces that as a command failure rather than a no-op. Restoring await this.ready before constructing the identifier would address it.

@Colengms

Copy link
Copy Markdown
Contributor

With these changes, I'm seeing an unusual number of didChangeSettings being delivered at startup. (Often as many as 6). It may be an existing issue, as I sometimes see up to 3 without your changes.

LSP: (received) cpptools/preinitialize (id: 1)
LSP: (invoked) cpptools/preinitialize (id: 1)
LSP: Sending response (id: 1)
LSP: (received) cpptools/initialize (id: 2)
LSP: (invoked) cpptools/initialize (id: 2)
cpptools version (TypeScript): 1.33.4-main
cpptools version (native): 1.33.5.0
Autocomplete is enabled.
Error squiggles are enabled.
Hover is enabled.
IntelliSense Engine = default.
LSP: Sending response (id: 2)
LSP: (received) cpptools/didChangeSettings
LSP: (invoked) cpptools/didChangeSettings
Autocomplete is enabled.
Error squiggles are enabled.
Hover is enabled.
IntelliSense Engine = default.
Enhanced Colorization is enabled.
LSP: (received) cpptools/didChangeSettings
LSP: (invoked) cpptools/didChangeSettings
Autocomplete is enabled.
Error squiggles are enabled.
Hover is enabled.
IntelliSense Engine = default.
Enhanced Colorization is enabled.
LSP: (received) cpptools/didChangeSettings
LSP: (invoked) cpptools/didChangeSettings
Autocomplete is enabled.
Error squiggles are enabled.
Hover is enabled.
IntelliSense Engine = default.
Enhanced Colorization is enabled.
LSP: (received) cpptools/didChangeSettings
LSP: (invoked) cpptools/didChangeSettings
Autocomplete is enabled.
Error squiggles are enabled.
Hover is enabled.
IntelliSense Engine = default.
Enhanced Colorization is enabled.
LSP: (received) cpptools/didChangeSettings
LSP: (invoked) cpptools/didChangeSettings
Autocomplete is enabled.
Error squiggles are enabled.
Hover is enabled.
IntelliSense Engine = default.
Enhanced Colorization is enabled.
LSP: (received) cpptools/didChangeSettings
LSP: (invoked) cpptools/didChangeSettings
LSP: (received - deferred) cpptools/didChangeVisibleTextEditors
Autocomplete is enabled.
Error squiggles are enabled.
Hover is enabled.
IntelliSense Engine = default.
LSP: (received - deferred) cpptools/didChangeVisibleTextEditors
Enhanced Colorization is enabled.
LSP: (received - deferred) cpptools/didChangeVisibleTextEditors
LSP: (received) cpptools/queryCompilerDefaults (id: 3)
LSP: (invoked) cpptools/queryCompilerDefaults (id: 3)
Compiler info database not connected - skipping load.
Compiler info database not connected - skipping load.
LSP: Sending response (id: 3)
Processed c_cpp_properties.json in 0.0054666s
LSP: (received) cpptools/didChangeCppProperties
LSP: (invoked) cpptools/didChangeCppProperties

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 19 out of 19 changed files in this pull request and generated no new comments.

Comments suppressed due to low confidence (3)

Extension/src/LanguageServer/client.ts:3116

  • restartIntelliSenseForFile reads this.languageClient.code2ProtocolConverter before any readiness await. If the language client wrapper hasn’t been initialized yet, that getter can throw synchronously during argument evaluation, bypassing the .catch(...) and crashing the event path. Await readiness before accessing the converters so the call is reliably deferred until the RPC client exists.
    public async restartIntelliSenseForFile(document: vscode.TextDocument): Promise<void> {
        return this.languageClient.sendNotification(RestartIntelliSenseForFileNotification, this.languageClient.code2ProtocolConverter.asTextDocumentIdentifier(document)).catch(logAndReturn.undefined);

Extension/src/LanguageServer/protocolFilter.ts:49

  • cppEditors is captured before the language client is ready, but onDidChangeVisibleTextEditors builds params synchronously and only awaits readiness inside sendNotification. If initialization is still in progress, the visible editor/selection snapshot may be stale by the time the notification is actually sent. Defer collecting editors until after client.ready resolves (as before) so the server receives a current snapshot.
                    const cppEditors: vscode.TextEditor[] = vscode.window.visibleTextEditors.filter(e => util.isCpp(e.document));
                    void client.onDidChangeVisibleTextEditors(cppEditors).catch(logAndReturn.undefined);

Extension/src/LanguageServer/extension.ts:192

  • The onDidChangeTextEditorVisibleRanges / onDidChangeVisibleTextEditors handlers are async and can reject, but their returned Promises are currently ignored by the event registration callbacks. If either throws/rejects, it can surface as an unhandled promise rejection. Wrap the calls with void ... .catch(logAndReturn.undefined) like other event hooks in this file.
    disposables.push(vscode.window.onDidChangeTextEditorVisibleRanges(e => onDidChangeTextEditorVisibleRanges(e)));
    disposables.push(vscode.window.onDidChangeActiveTextEditor(e => onDidChangeActiveTextEditor(e)));
    ui.didChangeActiveEditor(); // Handle already active documents (for non-cpp files that we don't register didOpen).
    disposables.push(vscode.window.onDidChangeTextEditorSelection(e => onDidChangeTextEditorSelection(e)));
    disposables.push(vscode.window.onDidChangeVisibleTextEditors(e => onDidChangeVisibleTextEditors(e)));

@bobbrow

Bob Brown (bobbrow) commented Jul 29, 2026

Copy link
Copy Markdown
Member Author

With these changes, I'm seeing an unusual number of didChangeSettings being delivered at startup. (Often as many as 6). It may be an existing issue, as I sometimes see up to 3 without your changes.

I can check...

EDIT: I did not reproduce this problem, but put out a separate PR to filter unnecessary settings change events which dropped my count down to 0.

@bobbrow
Bob Brown (bobbrow) marked this pull request as ready for review August 14, 2026 16:02
@bobbrow
Bob Brown (bobbrow) requested a review from a team as a code owner August 14, 2026 16:02
@sean-mcmanus

Copy link
Copy Markdown
Contributor

Bob Brown (@bobbrow) Do you want us to try to check this in?

@bobbrow

Copy link
Copy Markdown
Member Author

Bob Brown (Bob Brown (@bobbrow)) Do you want us to try to check this in?

Yes, I was hoping it would get a review and sign off for 1.34.0.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✨Copilot (agent8): I found one additional startup-ordering issue on the current head.

this.innerLanguageClient = languageClient;
// Ideally this would be set earlier, but the task provider expects it to also mean that `this.innerConfiguration` is set.
this.languageClient.isStarted = true;
compilerDefaults = await this.requestCompiler();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✨Copilot (agent8): [Moderate] This moves the global compiler-defaults request out of the existing "all clients configured" guard. ClientCollection creates one DefaultClient per workspace folder, so every client's init() now sends cpptools/queryCompilerDefaults before setupConfigurations(); previously the last client sent it once after all clients were configured. This multiplies startup work in multiroot workspaces and changes the ordering that the surrounding comment says is required. Could we use a separate RPC-started gate for the wrapper while keeping DefaultClient.ready and this once-only request at the end of initialization? A multiroot test that asserts one compiler-defaults request would also protect this sequencing.

@sean-mcmanus Sean McManus (sean-mcmanus) left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1.34.1

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Pull Request

Development

Successfully merging this pull request may close these issues.

4 participants