From 8d8bd90fb0892d54a151b2cacf11d2dafc4d6e99 Mon Sep 17 00:00:00 2001 From: Sergio Ramirez Date: Fri, 24 Apr 2026 12:01:15 -0700 Subject: [PATCH 1/2] Adding additional parse checking logic to determine if idle state has no pending calls and finished workspace parsing, file parsing, and intellisense updates. --- Extension/src/LanguageServer/client.ts | 62 +++++++++++++++++++++++ Extension/src/LanguageServer/extension.ts | 5 ++ 2 files changed, 67 insertions(+) diff --git a/Extension/src/LanguageServer/client.ts b/Extension/src/LanguageServer/client.ts index 680c14749..db78cdc78 100644 --- a/Extension/src/LanguageServer/client.ts +++ b/Extension/src/LanguageServer/client.ts @@ -805,6 +805,7 @@ export interface Client { setCurrentConfigName(configurationName: string): Thenable; getCurrentConfigName(): Thenable; getCurrentConfigCustomVariable(variableName: string): Thenable; + waitForIdle(timeout?: number): Promise; getVcpkgInstalled(): Thenable; getVcpkgEnabled(): Thenable; getCurrentCompilerPathAndArgs(): Thenable; @@ -919,6 +920,7 @@ export class DefaultClient implements Client { private configStateReceived: ConfigStateReceived = { compilers: false, compileCommands: false, configProviders: undefined, timeout: false }; private showConfigureIntelliSenseButton: boolean = false; + private pendingIdleCalls: { promise: ManualPromise; timer?: NodeJS.Timeout }[] = []; /** A queue of asynchronous tasks that need to be processed befofe ready is considered active. */ private static queue = new Array<[ManualPromise, () => Promise] | [ManualPromise]>(); @@ -1008,6 +1010,56 @@ export class DefaultClient implements Client { }; } + private isIdle(): boolean { + return !this.model.isInitializingWorkspace.Value + && !this.model.isIndexingWorkspace.Value + && !this.model.isParsingWorkspace.Value + && !this.model.isParsingFiles.Value + && !this.model.isUpdatingIntelliSense.Value; + } + + private resolvePendingIdleCallsIfReady(): void { + if (!this.pendingIdleCalls.length || !this.isIdle()) { + return; + } + + const pendingCalls: { promise: ManualPromise; timer?: NodeJS.Timeout }[] = this.pendingIdleCalls; + this.pendingIdleCalls = []; + pendingCalls.forEach(pendingCall => { + if (pendingCall.timer) { + clearTimeout(pendingCall.timer); + } + pendingCall.promise.resolve(true); + }); + } + + public async waitForIdle(timeout?: number): Promise { + if (this.isIdle()) { + return true; + } + + if (timeout !== undefined && timeout <= 0) { + return false; + } + + const pendingCall: { promise: ManualPromise; timer?: NodeJS.Timeout } = { + promise: new ManualPromise() + }; + + if (timeout !== undefined) { + pendingCall.timer = global.setTimeout(() => { + const index: number = this.pendingIdleCalls.indexOf(pendingCall); + if (index !== -1) { + this.pendingIdleCalls.splice(index, 1); + } + pendingCall.promise.resolve(false); + }, timeout); + } + + this.pendingIdleCalls.push(pendingCall); + return pendingCall.promise; + } + private getName(workspaceFolder?: vscode.WorkspaceFolder): string { return workspaceFolder ? workspaceFolder.name : "untitled"; } @@ -2893,6 +2945,8 @@ export class DefaultClient implements Client { } else if (message.includes("/")) { this.lastInvokedLspMessage = message; } + + this.resolvePendingIdleCallsIfReady(); } private updateTagParseStatus(tagParseStatus: TagParseStatus): void { @@ -4208,6 +4262,13 @@ export class DefaultClient implements Client { } public dispose(): void { + this.pendingIdleCalls.forEach(pendingCall => { + if (pendingCall.timer) { + clearTimeout(pendingCall.timer); + } + pendingCall.promise.resolve(false); + }); + this.pendingIdleCalls = []; this.disposables.forEach((d) => d.dispose()); this.disposables = []; if (this.documentFormattingProviderDisposable) { @@ -4360,6 +4421,7 @@ class NullClient implements Client { setCurrentConfigName(configurationName: string): Thenable { return Promise.resolve(); } getCurrentConfigName(): Thenable { return Promise.resolve(""); } getCurrentConfigCustomVariable(variableName: string): Thenable { return Promise.resolve(""); } + waitForIdle(timeout?: number): Promise { return Promise.resolve(true); } getVcpkgInstalled(): Thenable { return Promise.resolve(false); } getVcpkgEnabled(): Thenable { return Promise.resolve(false); } getCurrentCompilerPathAndArgs(): Thenable { return Promise.resolve(undefined); } diff --git a/Extension/src/LanguageServer/extension.ts b/Extension/src/LanguageServer/extension.ts index 79f89f9d6..331ddeb42 100644 --- a/Extension/src/LanguageServer/extension.ts +++ b/Extension/src/LanguageServer/extension.ts @@ -430,6 +430,7 @@ export async function registerCommands(enabled: boolean): Promise { commandDisposables.push(vscode.commands.registerCommand('cpptools.activeConfigName', enabled ? onGetActiveConfigName : onDisabledCommand)); commandDisposables.push(vscode.commands.registerCommand('cpptools.activeConfigCustomVariable', enabled ? onGetActiveConfigCustomVariable : onDisabledCommand)); commandDisposables.push(vscode.commands.registerCommand('cpptools.setActiveConfigName', enabled ? onSetActiveConfigName : onDisabledCommand)); + commandDisposables.push(vscode.commands.registerCommand('C_Cpp.waitForIdle', enabled ? onWaitForIdle : onDisabledCommand)); commandDisposables.push(vscode.commands.registerCommand('C_Cpp.RestartIntelliSenseForFile', enabled ? onRestartIntelliSenseForFile : onDisabledCommand)); commandDisposables.push(vscode.commands.registerCommand('C_Cpp.GenerateDoxygenComment', enabled ? onGenerateDoxygenComment : onDisabledCommand)); commandDisposables.push(vscode.commands.registerCommand('C_Cpp.CreateDeclarationOrDefinition', enabled ? onCreateDeclarationOrDefinition : onDisabledCommand)); @@ -977,6 +978,10 @@ function onGetActiveConfigCustomVariable(variableName: string): Thenable return clients.ActiveClient.getCurrentConfigCustomVariable(variableName); } +async function onWaitForIdle(timeout?: number): Promise { + return clients.getDefaultClient().waitForIdle(timeout); +} + function onLogDiagnostics(): Promise { return clients.ActiveClient.logDiagnostics(); } From 1782057bbcc3e6a5794d61b975204d2afc60849b Mon Sep 17 00:00:00 2001 From: Sergio Ramirez Date: Sat, 25 Apr 2026 00:55:09 -0700 Subject: [PATCH 2/2] Only checking for tag parsing now. Added cancellation token for propagation back to the caller. --- Extension/src/LanguageServer/client.ts | 75 +++++++++++++---------- Extension/src/LanguageServer/extension.ts | 6 +- 2 files changed, 45 insertions(+), 36 deletions(-) diff --git a/Extension/src/LanguageServer/client.ts b/Extension/src/LanguageServer/client.ts index db78cdc78..30873ee1a 100644 --- a/Extension/src/LanguageServer/client.ts +++ b/Extension/src/LanguageServer/client.ts @@ -805,7 +805,7 @@ export interface Client { setCurrentConfigName(configurationName: string): Thenable; getCurrentConfigName(): Thenable; getCurrentConfigCustomVariable(variableName: string): Thenable; - waitForIdle(timeout?: number): Promise; + waitForTagParsing(timeout: number, token: vscode.CancellationToken): Promise; getVcpkgInstalled(): Thenable; getVcpkgEnabled(): Thenable; getCurrentCompilerPathAndArgs(): Thenable; @@ -920,7 +920,7 @@ export class DefaultClient implements Client { private configStateReceived: ConfigStateReceived = { compilers: false, compileCommands: false, configProviders: undefined, timeout: false }; private showConfigureIntelliSenseButton: boolean = false; - private pendingIdleCalls: { promise: ManualPromise; timer?: NodeJS.Timeout }[] = []; + private pendingTagParsingCalls: { promise: ManualPromise; timer?: NodeJS.Timeout; cancellationListener?: vscode.Disposable }[] = []; /** A queue of asynchronous tasks that need to be processed befofe ready is considered active. */ private static queue = new Array<[ManualPromise, () => Promise] | [ManualPromise]>(); @@ -1010,53 +1010,61 @@ export class DefaultClient implements Client { }; } - private isIdle(): boolean { - return !this.model.isInitializingWorkspace.Value - && !this.model.isIndexingWorkspace.Value - && !this.model.isParsingWorkspace.Value - && !this.model.isParsingFiles.Value - && !this.model.isUpdatingIntelliSense.Value; - } - - private resolvePendingIdleCallsIfReady(): void { - if (!this.pendingIdleCalls.length || !this.isIdle()) { + // If there are any pending calls that were waiting for tag parsing to complete, we can resolve them since it's finished. If there are no pending calls, this does nothing. + private resolvePendingTagParsingCallsIfReady(): void { + if (!this.pendingTagParsingCalls.length || this.IsTagParsing) { return; } - const pendingCalls: { promise: ManualPromise; timer?: NodeJS.Timeout }[] = this.pendingIdleCalls; - this.pendingIdleCalls = []; + const pendingCalls: { promise: ManualPromise; timer?: NodeJS.Timeout; cancellationListener?: vscode.Disposable }[] = this.pendingTagParsingCalls; + this.pendingTagParsingCalls = []; pendingCalls.forEach(pendingCall => { if (pendingCall.timer) { clearTimeout(pendingCall.timer); } + pendingCall.cancellationListener?.dispose(); pendingCall.promise.resolve(true); }); } - public async waitForIdle(timeout?: number): Promise { - if (this.isIdle()) { + public async waitForTagParsing(timeout: number, token: vscode.CancellationToken): Promise { + // On initialization, the client has UI bools all set to false which could cause an early return. We want to ensure it's ready first. + await this.ready; + + if (!this.IsTagParsing) { return true; } - if (timeout !== undefined && timeout <= 0) { - return false; + if (token.isCancellationRequested) { + throw new vscode.CancellationError(); } - const pendingCall: { promise: ManualPromise; timer?: NodeJS.Timeout } = { + const pendingCall: { promise: ManualPromise; timer?: NodeJS.Timeout; cancellationListener?: vscode.Disposable } = { promise: new ManualPromise() }; - if (timeout !== undefined) { - pendingCall.timer = global.setTimeout(() => { - const index: number = this.pendingIdleCalls.indexOf(pendingCall); - if (index !== -1) { - this.pendingIdleCalls.splice(index, 1); - } - pendingCall.promise.resolve(false); - }, timeout); - } + pendingCall.timer = global.setTimeout(() => { + const index: number = this.pendingTagParsingCalls.indexOf(pendingCall); + if (index !== -1) { + this.pendingTagParsingCalls.splice(index, 1); + } + pendingCall.cancellationListener?.dispose(); + pendingCall.promise.resolve(false); + }, timeout); + + pendingCall.cancellationListener = token.onCancellationRequested(() => { + const index: number = this.pendingTagParsingCalls.indexOf(pendingCall); + if (index !== -1) { + this.pendingTagParsingCalls.splice(index, 1); + } + if (pendingCall.timer) { + clearTimeout(pendingCall.timer); + } + pendingCall.cancellationListener?.dispose(); + pendingCall.promise.reject(new vscode.CancellationError()); + }); - this.pendingIdleCalls.push(pendingCall); + this.pendingTagParsingCalls.push(pendingCall); return pendingCall.promise; } @@ -2946,7 +2954,7 @@ export class DefaultClient implements Client { this.lastInvokedLspMessage = message; } - this.resolvePendingIdleCallsIfReady(); + this.resolvePendingTagParsingCallsIfReady(); } private updateTagParseStatus(tagParseStatus: TagParseStatus): void { @@ -4262,13 +4270,14 @@ export class DefaultClient implements Client { } public dispose(): void { - this.pendingIdleCalls.forEach(pendingCall => { + this.pendingTagParsingCalls.forEach(pendingCall => { if (pendingCall.timer) { clearTimeout(pendingCall.timer); } + pendingCall.cancellationListener?.dispose(); pendingCall.promise.resolve(false); }); - this.pendingIdleCalls = []; + this.pendingTagParsingCalls = []; this.disposables.forEach((d) => d.dispose()); this.disposables = []; if (this.documentFormattingProviderDisposable) { @@ -4421,7 +4430,7 @@ class NullClient implements Client { setCurrentConfigName(configurationName: string): Thenable { return Promise.resolve(); } getCurrentConfigName(): Thenable { return Promise.resolve(""); } getCurrentConfigCustomVariable(variableName: string): Thenable { return Promise.resolve(""); } - waitForIdle(timeout?: number): Promise { return Promise.resolve(true); } + waitForTagParsing(timeout: number, token: vscode.CancellationToken): Promise { return token.isCancellationRequested ? Promise.reject(new vscode.CancellationError()) : Promise.resolve(true); } getVcpkgInstalled(): Thenable { return Promise.resolve(false); } getVcpkgEnabled(): Thenable { return Promise.resolve(false); } getCurrentCompilerPathAndArgs(): Thenable { return Promise.resolve(undefined); } diff --git a/Extension/src/LanguageServer/extension.ts b/Extension/src/LanguageServer/extension.ts index 331ddeb42..6af3223e5 100644 --- a/Extension/src/LanguageServer/extension.ts +++ b/Extension/src/LanguageServer/extension.ts @@ -430,7 +430,7 @@ export async function registerCommands(enabled: boolean): Promise { commandDisposables.push(vscode.commands.registerCommand('cpptools.activeConfigName', enabled ? onGetActiveConfigName : onDisabledCommand)); commandDisposables.push(vscode.commands.registerCommand('cpptools.activeConfigCustomVariable', enabled ? onGetActiveConfigCustomVariable : onDisabledCommand)); commandDisposables.push(vscode.commands.registerCommand('cpptools.setActiveConfigName', enabled ? onSetActiveConfigName : onDisabledCommand)); - commandDisposables.push(vscode.commands.registerCommand('C_Cpp.waitForIdle', enabled ? onWaitForIdle : onDisabledCommand)); + commandDisposables.push(vscode.commands.registerCommand('C_Cpp.waitForTagParsing', enabled ? onWaitForTagParsing : onDisabledCommand)); commandDisposables.push(vscode.commands.registerCommand('C_Cpp.RestartIntelliSenseForFile', enabled ? onRestartIntelliSenseForFile : onDisabledCommand)); commandDisposables.push(vscode.commands.registerCommand('C_Cpp.GenerateDoxygenComment', enabled ? onGenerateDoxygenComment : onDisabledCommand)); commandDisposables.push(vscode.commands.registerCommand('C_Cpp.CreateDeclarationOrDefinition', enabled ? onCreateDeclarationOrDefinition : onDisabledCommand)); @@ -978,8 +978,8 @@ function onGetActiveConfigCustomVariable(variableName: string): Thenable return clients.ActiveClient.getCurrentConfigCustomVariable(variableName); } -async function onWaitForIdle(timeout?: number): Promise { - return clients.getDefaultClient().waitForIdle(timeout); +async function onWaitForTagParsing(timeout: number, token: vscode.CancellationToken): Promise { + return clients.getDefaultClient().waitForTagParsing(timeout, token); } function onLogDiagnostics(): Promise {