From 7aeb44fb32f775ee484ece20eb02ac7e727b4542 Mon Sep 17 00:00:00 2001 From: Sean McManus Date: Thu, 16 Apr 2026 10:57:47 -0700 Subject: [PATCH 1/5] Add a progress indicator with Cancel for Switch Header/Source. --- Extension/src/LanguageServer/client.ts | 10 +++++--- Extension/src/LanguageServer/extension.ts | 31 +++++++++++++++-------- 2 files changed, 26 insertions(+), 15 deletions(-) diff --git a/Extension/src/LanguageServer/client.ts b/Extension/src/LanguageServer/client.ts index 680c14749..316ec54ce 100644 --- a/Extension/src/LanguageServer/client.ts +++ b/Extension/src/LanguageServer/client.ts @@ -811,7 +811,7 @@ export interface Client { getKnownCompilers(): Thenable; takeOwnership(document: vscode.TextDocument): void; sendDidOpen(document: vscode.TextDocument): Promise; - requestSwitchHeaderSource(rootUri: vscode.Uri, fileName: string): Thenable; + requestSwitchHeaderSource(rootUri: vscode.Uri, fileName: string, token: vscode.CancellationToken): Thenable; updateActiveDocumentTextOptions(): void; didChangeActiveEditor(editor?: vscode.TextEditor, selection?: Range): Promise; restartIntelliSenseForFile(document: vscode.TextDocument): Promise; @@ -3019,12 +3019,14 @@ export class DefaultClient implements Client { /** * requests to the language server */ - public async requestSwitchHeaderSource(rootUri: vscode.Uri, fileName: string): Promise { + public async requestSwitchHeaderSource(rootUri: vscode.Uri, fileName: string, token: vscode.CancellationToken): Promise { const params: SwitchHeaderSourceParams = { switchHeaderSourceFileName: fileName, workspaceFolderUri: rootUri.toString() }; - return this.enqueue(async () => this.languageClient.sendRequest(SwitchHeaderSourceRequest, params)); + await withCancellation(this.ready, token); + return DefaultClient.withLspCancellationHandling( + () => this.languageClient.sendRequest(SwitchHeaderSourceRequest, params, token), token); } public async requestCompiler(newCompilerPath?: string): Promise { @@ -4366,7 +4368,7 @@ class NullClient implements Client { getKnownCompilers(): Thenable { return Promise.resolve([]); } takeOwnership(document: vscode.TextDocument): void { } sendDidOpen(document: vscode.TextDocument): Promise { return Promise.resolve(); } - requestSwitchHeaderSource(rootUri: vscode.Uri, fileName: string): Thenable { return Promise.resolve(""); } + requestSwitchHeaderSource(rootUri: vscode.Uri, fileName: string, token: vscode.CancellationToken): Thenable { return Promise.resolve(""); } updateActiveDocumentTextOptions(): void { } didChangeActiveEditor(editor?: vscode.TextEditor): Promise { return Promise.resolve(); } restartIntelliSenseForFile(document: vscode.TextDocument): Promise { return Promise.resolve(); } diff --git a/Extension/src/LanguageServer/extension.ts b/Extension/src/LanguageServer/extension.ts index 79f89f9d6..6e81fdce3 100644 --- a/Extension/src/LanguageServer/extension.ts +++ b/Extension/src/LanguageServer/extension.ts @@ -478,19 +478,28 @@ async function onSwitchHeaderSource(): Promise { rootUri = vscode.Uri.file(path.dirname(fileName)); // When switching without a folder open. } - let targetFileName: string = await clients.ActiveClient.requestSwitchHeaderSource(rootUri, fileName); - // If the targetFileName has a path that is a symlink target of a workspace folder, - // then replace the RootRealPath with the RootPath (the symlink path). - let targetFileNameReplaced: boolean = false; - clients.forEach(client => { - if (!targetFileNameReplaced && client.RootRealPath && client.RootPath !== client.RootRealPath - && targetFileName.startsWith(client.RootRealPath)) { - targetFileName = client.RootPath + targetFileName.substring(client.RootRealPath.length); - targetFileNameReplaced = true; + await vscode.window.withProgress({ + location: vscode.ProgressLocation.Notification, + title: localize('switch.header.source', 'Switching Header/Source...'), + cancellable: true + }, async (_progress, token) => { + let targetFileName: string = await clients.ActiveClient.requestSwitchHeaderSource(rootUri, fileName, token); + if (token.isCancellationRequested) { + return; } + // If the targetFileName has a path that is a symlink target of a workspace folder, + // then replace the RootRealPath with the RootPath (the symlink path). + let targetFileNameReplaced: boolean = false; + clients.forEach(client => { + if (!targetFileNameReplaced && client.RootRealPath && client.RootPath !== client.RootRealPath + && targetFileName.startsWith(client.RootRealPath)) { + targetFileName = client.RootPath + targetFileName.substring(client.RootRealPath.length); + targetFileNameReplaced = true; + } + }); + const document: vscode.TextDocument = await vscode.workspace.openTextDocument(targetFileName); + await vscode.window.showTextDocument(document).then(undefined, logAndReturn.undefined); }); - const document: vscode.TextDocument = await vscode.workspace.openTextDocument(targetFileName); - void vscode.window.showTextDocument(document).then(undefined, logAndReturn.undefined); } /** From 6c9fca55b01dca9c123882169c86a465770778e6 Mon Sep 17 00:00:00 2001 From: Sean McManus Date: Thu, 16 Apr 2026 11:15:42 -0700 Subject: [PATCH 2/5] Fixes. Co-authored-by: Copilot --- Extension/src/LanguageServer/client.ts | 11 +++++-- Extension/src/LanguageServer/extension.ts | 37 ++++++++++++++--------- 2 files changed, 30 insertions(+), 18 deletions(-) diff --git a/Extension/src/LanguageServer/client.ts b/Extension/src/LanguageServer/client.ts index 316ec54ce..90b7b550e 100644 --- a/Extension/src/LanguageServer/client.ts +++ b/Extension/src/LanguageServer/client.ts @@ -3024,9 +3024,14 @@ export class DefaultClient implements Client { switchHeaderSourceFileName: fileName, workspaceFolderUri: rootUri.toString() }; - await withCancellation(this.ready, token); - return DefaultClient.withLspCancellationHandling( - () => this.languageClient.sendRequest(SwitchHeaderSourceRequest, params, token), token); + const request: Promise = this.enqueue(async () => { + if (token.isCancellationRequested) { + throw new vscode.CancellationError(); + } + return DefaultClient.withLspCancellationHandling( + () => this.languageClient.sendRequest(SwitchHeaderSourceRequest, params, token), token); + }); + return withCancellation(request, token); } public async requestCompiler(newCompilerPath?: string): Promise { diff --git a/Extension/src/LanguageServer/extension.ts b/Extension/src/LanguageServer/extension.ts index 6e81fdce3..993dd60da 100644 --- a/Extension/src/LanguageServer/extension.ts +++ b/Extension/src/LanguageServer/extension.ts @@ -483,22 +483,29 @@ async function onSwitchHeaderSource(): Promise { title: localize('switch.header.source', 'Switching Header/Source...'), cancellable: true }, async (_progress, token) => { - let targetFileName: string = await clients.ActiveClient.requestSwitchHeaderSource(rootUri, fileName, token); - if (token.isCancellationRequested) { - return; - } - // If the targetFileName has a path that is a symlink target of a workspace folder, - // then replace the RootRealPath with the RootPath (the symlink path). - let targetFileNameReplaced: boolean = false; - clients.forEach(client => { - if (!targetFileNameReplaced && client.RootRealPath && client.RootPath !== client.RootRealPath - && targetFileName.startsWith(client.RootRealPath)) { - targetFileName = client.RootPath + targetFileName.substring(client.RootRealPath.length); - targetFileNameReplaced = true; + try { + let targetFileName: string = await clients.ActiveClient.requestSwitchHeaderSource(rootUri, fileName, token); + if (token.isCancellationRequested || !targetFileName) { + return; } - }); - const document: vscode.TextDocument = await vscode.workspace.openTextDocument(targetFileName); - await vscode.window.showTextDocument(document).then(undefined, logAndReturn.undefined); + // If the targetFileName has a path that is a symlink target of a workspace folder, + // then replace the RootRealPath with the RootPath (the symlink path). + let targetFileNameReplaced: boolean = false; + clients.forEach(client => { + if (!targetFileNameReplaced && client.RootRealPath && client.RootPath !== client.RootRealPath + && targetFileName.startsWith(client.RootRealPath)) { + targetFileName = client.RootPath + targetFileName.substring(client.RootRealPath.length); + targetFileNameReplaced = true; + } + }); + const document: vscode.TextDocument = await vscode.workspace.openTextDocument(targetFileName); + await vscode.window.showTextDocument(document).then(undefined, logAndReturn.undefined); + } catch (e) { + if (e instanceof vscode.CancellationError) { + return; + } + throw e; + } }); } From 1af74434b18f7f42f236610d7140ef8a02a51a1c Mon Sep 17 00:00:00 2001 From: Sean McManus Date: Thu, 16 Apr 2026 11:28:39 -0700 Subject: [PATCH 3/5] Add a 2 second delay before showing the progress notification. Co-authored-by: Copilot --- Extension/src/LanguageServer/extension.ts | 43 +++++++++++++++++++---- 1 file changed, 37 insertions(+), 6 deletions(-) diff --git a/Extension/src/LanguageServer/extension.ts b/Extension/src/LanguageServer/extension.ts index 993dd60da..ebfae931b 100644 --- a/Extension/src/LanguageServer/extension.ts +++ b/Extension/src/LanguageServer/extension.ts @@ -478,11 +478,7 @@ async function onSwitchHeaderSource(): Promise { rootUri = vscode.Uri.file(path.dirname(fileName)); // When switching without a folder open. } - await vscode.window.withProgress({ - location: vscode.ProgressLocation.Notification, - title: localize('switch.header.source', 'Switching Header/Source...'), - cancellable: true - }, async (_progress, token) => { + const switchHeaderSource: (token: vscode.CancellationToken) => Promise = async (token: vscode.CancellationToken) => { try { let targetFileName: string = await clients.ActiveClient.requestSwitchHeaderSource(rootUri, fileName, token); if (token.isCancellationRequested || !targetFileName) { @@ -506,7 +502,42 @@ async function onSwitchHeaderSource(): Promise { } throw e; } - }); + }; + + const tokenSource: vscode.CancellationTokenSource = new vscode.CancellationTokenSource(); + try { + const switchHeaderSourcePromise: Promise = switchHeaderSource(tokenSource.token); + const showProgress: boolean = await new Promise((resolve, reject) => { + const timer: NodeJS.Timeout = global.setTimeout(() => resolve(true), 2000); + void switchHeaderSourcePromise.then(() => { + clearTimeout(timer); + resolve(false); + }, (e) => { + clearTimeout(timer); + reject(e); + }); + }); + + if (!showProgress) { + await switchHeaderSourcePromise; + return; + } + + await vscode.window.withProgress({ + location: vscode.ProgressLocation.Notification, + title: localize('switch.header.source', 'Switching Header/Source...'), + cancellable: true + }, async (_progress, token) => { + const cancellationListener: vscode.Disposable = token.onCancellationRequested(() => tokenSource.cancel()); + try { + await switchHeaderSourcePromise; + } finally { + cancellationListener.dispose(); + } + }); + } finally { + tokenSource.dispose(); + } } /** From fcbd02cca8aecd87dd7bf75fb1587c3fc45f0ff1 Mon Sep 17 00:00:00 2001 From: Sean McManus Date: Thu, 16 Apr 2026 12:47:28 -0700 Subject: [PATCH 4/5] Update. Co-authored-by: Copilot --- Extension/src/LanguageServer/client.ts | 16 ++++++++++++---- Extension/src/LanguageServer/extension.ts | 2 +- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/Extension/src/LanguageServer/client.ts b/Extension/src/LanguageServer/client.ts index 90b7b550e..9e90e3859 100644 --- a/Extension/src/LanguageServer/client.ts +++ b/Extension/src/LanguageServer/client.ts @@ -3024,14 +3024,22 @@ export class DefaultClient implements Client { switchHeaderSourceFileName: fileName, workspaceFolderUri: rootUri.toString() }; - const request: Promise = this.enqueue(async () => { + return this.enqueue(async () => { if (token.isCancellationRequested) { throw new vscode.CancellationError(); } - return DefaultClient.withLspCancellationHandling( - () => this.languageClient.sendRequest(SwitchHeaderSourceRequest, params, token), token); + + // Don't use withLspCancellationHandling() or withCancellation() here. If the switch target is already known, + // the caller should still be able to use it even if the progress notification was just cancelled. + try { + return await this.languageClient.sendRequest(SwitchHeaderSourceRequest, params, token); + } catch (e: any) { + if (e instanceof ResponseError && (e.code === RequestCancelled || e.code === ServerCancelled)) { + throw new vscode.CancellationError(); + } + throw e; + } }); - return withCancellation(request, token); } public async requestCompiler(newCompilerPath?: string): Promise { diff --git a/Extension/src/LanguageServer/extension.ts b/Extension/src/LanguageServer/extension.ts index ebfae931b..d6f557649 100644 --- a/Extension/src/LanguageServer/extension.ts +++ b/Extension/src/LanguageServer/extension.ts @@ -481,7 +481,7 @@ async function onSwitchHeaderSource(): Promise { const switchHeaderSource: (token: vscode.CancellationToken) => Promise = async (token: vscode.CancellationToken) => { try { let targetFileName: string = await clients.ActiveClient.requestSwitchHeaderSource(rootUri, fileName, token); - if (token.isCancellationRequested || !targetFileName) { + if (!targetFileName) { return; } // If the targetFileName has a path that is a symlink target of a workspace folder, From 22fcfc9c25ffb85add106621a111fd58f12a806e Mon Sep 17 00:00:00 2001 From: Sean McManus Date: Thu, 16 Apr 2026 12:51:32 -0700 Subject: [PATCH 5/5] Cleanup. Co-authored-by: Copilot --- Extension/src/LanguageServer/client.ts | 4 ---- 1 file changed, 4 deletions(-) diff --git a/Extension/src/LanguageServer/client.ts b/Extension/src/LanguageServer/client.ts index 9e90e3859..f4d6eaa87 100644 --- a/Extension/src/LanguageServer/client.ts +++ b/Extension/src/LanguageServer/client.ts @@ -3025,10 +3025,6 @@ export class DefaultClient implements Client { workspaceFolderUri: rootUri.toString() }; return this.enqueue(async () => { - if (token.isCancellationRequested) { - throw new vscode.CancellationError(); - } - // Don't use withLspCancellationHandling() or withCancellation() here. If the switch target is already known, // the caller should still be able to use it even if the progress notification was just cancelled. try {