diff --git a/Extension/src/LanguageServer/client.ts b/Extension/src/LanguageServer/client.ts index 680c14749..f4d6eaa87 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,23 @@ 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)); + return this.enqueue(async () => { + // 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; + } + }); } public async requestCompiler(newCompilerPath?: string): Promise { @@ -4366,7 +4377,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..d6f557649 100644 --- a/Extension/src/LanguageServer/extension.ts +++ b/Extension/src/LanguageServer/extension.ts @@ -478,19 +478,66 @@ 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; + const switchHeaderSource: (token: vscode.CancellationToken) => Promise = async (token: vscode.CancellationToken) => { + try { + let targetFileName: string = await clients.ActiveClient.requestSwitchHeaderSource(rootUri, fileName, token); + if (!targetFileName) { + 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); + } catch (e) { + if (e instanceof vscode.CancellationError) { + return; + } + throw e; } - }); - const document: vscode.TextDocument = await vscode.workspace.openTextDocument(targetFileName); - void vscode.window.showTextDocument(document).then(undefined, logAndReturn.undefined); + }; + + 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(); + } } /**