From f4851caac642c8873a3481606a1985a7bfaf076f Mon Sep 17 00:00:00 2001 From: Sean McManus Date: Fri, 14 Aug 2026 04:16:34 +0000 Subject: [PATCH 1/6] Add select translation unit command --- Extension/package.json | 9 +++ Extension/package.nls.json | 1 + Extension/src/LanguageServer/client.ts | 32 +++++++++++ Extension/src/LanguageServer/extension.ts | 67 +++++++++++++++++++---- 4 files changed, 99 insertions(+), 10 deletions(-) diff --git a/Extension/package.json b/Extension/package.json index e4f44941d..2b515f160 100644 --- a/Extension/package.json +++ b/Extension/package.json @@ -3989,6 +3989,11 @@ "title": "%c_cpp.command.switchHeaderSource.title%", "category": "C/C++" }, + { + "command": "C_Cpp.SelectTranslationUnit", + "title": "%c_cpp.command.selectTranslationUnit.title%", + "category": "C/C++" + }, { "command": "C_Cpp.EnableErrorSquiggles", "title": "%c_cpp.command.enableErrorSquiggles.title%", @@ -6536,6 +6541,10 @@ "command": "C_Cpp.SwitchHeaderSource", "when": "editorLangId =~ /^(c|(cuda-)?cpp)$/ && !(config.C_Cpp.intelliSenseEngine =~ /^[dD]isabled$/)" }, + { + "command": "C_Cpp.SelectTranslationUnit", + "when": "editorLangId =~ /^(c|(cuda-)?cpp)$/ && config.C_Cpp.intelliSenseEngine =~ /^[dD]efault$/" + }, { "command": "C_Cpp.EnableErrorSquiggles", "when": "config.C_Cpp.intelliSenseEngine =~ /^[dD]efault$/" diff --git a/Extension/package.nls.json b/Extension/package.nls.json index 686234a1f..ea6352bef 100644 --- a/Extension/package.nls.json +++ b/Extension/package.nls.json @@ -15,6 +15,7 @@ "c_cpp.command.installCompiler.title": "Install a C++ Compiler", "c_cpp.command.rescanCompilers.title": "Rescan for Compilers", "c_cpp.command.switchHeaderSource.title": "Switch Header/Source", + "c_cpp.command.selectTranslationUnit.title": "Select a Translation Unit...", "c_cpp.command.enableErrorSquiggles.title": "Enable Error Squiggles", "c_cpp.command.disableErrorSquiggles.title": "Disable Error Squiggles", "c_cpp.command.toggleDimInactiveRegions.title": "Toggle Inactive Region Colorization", diff --git a/Extension/src/LanguageServer/client.ts b/Extension/src/LanguageServer/client.ts index 4cfa24c0f..a43c72413 100644 --- a/Extension/src/LanguageServer/client.ts +++ b/Extension/src/LanguageServer/client.ts @@ -217,6 +217,16 @@ interface SwitchHeaderSourceParams extends WorkspaceFolderParams { switchHeaderSourceFileName: string; } +interface GetTranslationUnitSourceCandidatesResult { + candidates: string[]; + currentTranslationUnit: string; +} + +interface SelectTranslationUnitParams { + uri: string; + translationUnit: string; +} + interface FileChangedParams extends WorkspaceFolderParams { uri: string; } @@ -625,6 +635,7 @@ const PreInitializationRequest: RequestType = new RequestTyp const InitializationRequest: RequestType = new RequestType('cpptools/initialize'); const QueryCompilerDefaultsRequest: RequestType = new RequestType('cpptools/queryCompilerDefaults'); const SwitchHeaderSourceRequest: RequestType = new RequestType('cpptools/didSwitchHeaderSource'); +const GetTranslationUnitSourceCandidatesRequest: RequestType = new RequestType('cpptools/getTranslationUnitSourceCandidates'); const GetDiagnosticsRequest: RequestType = new RequestType('cpptools/getDiagnostics'); export const GetDocumentSymbolRequest: RequestType = new RequestType('cpptools/getDocumentSymbols'); export const GetSymbolInfoRequest: RequestType = new RequestType('cpptools/getWorkspaceSymbols'); @@ -653,6 +664,7 @@ const PauseParsingNotification: NotificationType = new NotificationType = new NotificationType('cpptools/resumeParsing'); const DidChangeActiveEditorNotification: NotificationType = new NotificationType('cpptools/didChangeActiveEditor'); const RestartIntelliSenseForFileNotification: NotificationType = new NotificationType('cpptools/restartIntelliSenseForFile'); +const SelectTranslationUnitNotification: NotificationType = new NotificationType('cpptools/selectTranslationUnit'); const DidChangeTextEditorSelectionNotification: NotificationType = new NotificationType('cpptools/didChangeTextEditorSelection'); const ChangeCompileCommandsNotification: NotificationType = new NotificationType('cpptools/didChangeCompileCommands'); const ChangeSelectedSettingNotification: NotificationType = new NotificationType('cpptools/didChangeSelectedSetting'); @@ -832,6 +844,8 @@ export interface Client { takeOwnership(document: vscode.TextDocument): void; sendDidOpen(document: vscode.TextDocument): Promise; requestSwitchHeaderSource(rootUri: vscode.Uri, fileName: string, token: vscode.CancellationToken): Thenable; + getTranslationUnitSourceCandidates(uri: vscode.Uri): Promise; + selectTranslationUnit(uri: vscode.Uri, translationUnit: string): Promise; updateActiveDocumentTextOptions(): void; didChangeActiveEditor(editor?: vscode.TextEditor, selection?: Range): Promise; restartIntelliSenseForFile(document: vscode.TextDocument): Promise; @@ -3164,6 +3178,20 @@ export class DefaultClient implements Client { }); } + public async getTranslationUnitSourceCandidates(uri: vscode.Uri): Promise { + const params: TextDocumentIdentifier = { uri: uri.toString() }; + await this.ready; + return this.languageClient.sendRequest( + GetTranslationUnitSourceCandidatesRequest, + params); + } + + public async selectTranslationUnit(uri: vscode.Uri, translationUnit: string): Promise { + const params: SelectTranslationUnitParams = { uri: uri.toString(), translationUnit }; + await this.ready; + return this.languageClient.sendNotification(SelectTranslationUnitNotification, params).catch(logAndReturn.undefined); + } + public async requestCompiler(newCompilerPath?: string): Promise { const params: QueryDefaultCompilerParams = { newTrustedCompilerPath: newCompilerPath ?? "" @@ -4560,6 +4588,10 @@ class NullClient implements Client { takeOwnership(document: vscode.TextDocument): void { } sendDidOpen(document: vscode.TextDocument): Promise { return Promise.resolve(); } requestSwitchHeaderSource(rootUri: vscode.Uri, fileName: string, token: vscode.CancellationToken): Thenable { return Promise.resolve(""); } + getTranslationUnitSourceCandidates(uri: vscode.Uri): Promise { + return Promise.resolve({ candidates: [], currentTranslationUnit: "" }); + } + selectTranslationUnit(uri: vscode.Uri, translationUnit: string): Promise { 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 e1857ba9e..05bac77b3 100644 --- a/Extension/src/LanguageServer/extension.ts +++ b/Extension/src/LanguageServer/extension.ts @@ -383,6 +383,7 @@ export async function registerCommands(enabled: boolean): Promise { commandDisposables.forEach(d => d.dispose()); commandDisposables.length = 0; commandDisposables.push(vscode.commands.registerCommand('C_Cpp.SwitchHeaderSource', enabled ? onSwitchHeaderSource : onDisabledCommand)); + commandDisposables.push(vscode.commands.registerCommand('C_Cpp.SelectTranslationUnit', enabled ? onSelectTranslationUnit : onDisabledCommand)); commandDisposables.push(vscode.commands.registerCommand('C_Cpp.ResetDatabase', enabled ? onResetDatabase : onDisabledCommand)); commandDisposables.push(vscode.commands.registerCommand('C_Cpp.SelectIntelliSenseConfiguration', enabled ? selectIntelliSenseConfiguration : onDisabledCommand)); commandDisposables.push(vscode.commands.registerCommand('C_Cpp.InstallCompiler', enabled ? installCompiler : onDisabledCommand)); @@ -468,6 +469,61 @@ async function onRestartIntelliSenseForFile() { return clients.ActiveClient.restartIntelliSenseForFile(activeEditor.document); } +function getEditorPath(fileName: string): string { + let bestRealRoot: string | undefined; + let bestRootPath: string | undefined; + clients.forEach(client => { + if (!client.RootRealPath) { + return; + } + const relativePath: string = path.relative(client.RootRealPath, fileName); + const isUnderRoot: boolean = relativePath === "" || + (relativePath !== ".." && !relativePath.startsWith(`..${path.sep}`) && !path.isAbsolute(relativePath)); + if (isUnderRoot && (!bestRealRoot || client.RootRealPath.length > bestRealRoot.length)) { + bestRealRoot = client.RootRealPath; + bestRootPath = client.RootPath; + } + }); + return bestRealRoot && bestRootPath ? path.join(bestRootPath, path.relative(bestRealRoot, fileName)) : fileName; +} + +interface TranslationUnitQuickPickItem extends vscode.QuickPickItem { + translationUnit: string; +} + +async function onSelectTranslationUnit(): Promise { + const activeEditor: vscode.TextEditor | undefined = vscode.window.activeTextEditor; + if (!activeEditor || !util.isCpp(activeEditor.document)) { + return; + } + + const client: Client = clients.ActiveClient; + const result = await client.getTranslationUnitSourceCandidates(activeEditor.document.uri); + const currentTranslationUnit: string = getEditorPath(result.currentTranslationUnit); + const items: TranslationUnitQuickPickItem[] = result.candidates.map(candidate => { + const editorPath: string = getEditorPath(candidate); + const relativePath: string = vscode.workspace.asRelativePath(vscode.Uri.file(editorPath)); + const directory: string = path.dirname(relativePath); + const isCurrent: boolean = editorPath === currentTranslationUnit; + return { + label: `${isCurrent ? "$(check) " : ""}${path.basename(relativePath)}`, + description: isCurrent ? localize('current.translation.unit', 'Current translation unit') : + directory === "." ? undefined : directory, + detail: editorPath, + translationUnit: editorPath + }; + }); + const selection: TranslationUnitQuickPickItem | undefined = await vscode.window.showQuickPick(items, { + title: localize('select.translation.unit', 'Select a Translation Unit'), + placeHolder: localize('select.translation.unit.placeholder', 'Select a source file to use as the translation unit'), + matchOnDescription: true, + matchOnDetail: true + }); + if (selection !== undefined) { + await client.selectTranslationUnit(activeEditor.document.uri, selection.translationUnit); + } +} + async function onSwitchHeaderSource(): Promise { const activeEditor: vscode.TextEditor | undefined = vscode.window.activeTextEditor; if (!activeEditor || !util.isCpp(activeEditor.document)) { @@ -487,16 +543,7 @@ async function onSwitchHeaderSource(): Promise { 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; - } - }); + targetFileName = getEditorPath(targetFileName); const document: vscode.TextDocument = await vscode.workspace.openTextDocument(targetFileName); await vscode.window.showTextDocument(document).then(undefined, logAndReturn.undefined); } catch (e) { From 951bb646a52fb2522a0a5e86411093e72f62129a Mon Sep 17 00:00:00 2001 From: Sean McManus Date: Sun, 23 Aug 2026 15:04:07 -0700 Subject: [PATCH 2/6] Add translation unit fallback warnings --- Extension/src/nativeStrings.json | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/Extension/src/nativeStrings.json b/Extension/src/nativeStrings.json index f4e66092a..a39083390 100644 --- a/Extension/src/nativeStrings.json +++ b/Extension/src/nativeStrings.json @@ -773,5 +773,13 @@ "failed_to_lock_browse_db_lock_file": { "text": "Failed to lock browse database lock file: {0} (errno={1})", "hint": "{0} is the lock file path. {1} is the errno value. {Locked=\"{0}\"} {Locked=\"errno\"} {Locked=\"{1}\"}" + }, + "selected_translation_unit_not_include_file_previous": { + "text": "The selected translation unit '{0}' does not include '{1}'. IntelliSense will restore the previous translation unit if it is still available; otherwise, it will use '{1}' as a header-only translation unit.", + "hint": "{0} is the selected translation unit path. {1} is the active file path. {Locked=\"IntelliSense\"} {Locked=\"{0}\"} {Locked=\"{1}\"}" + }, + "selected_translation_unit_not_include_file_header_only": { + "text": "The selected translation unit '{0}' does not include '{1}'. No previous translation unit is available, so IntelliSense will use '{1}' as a header-only translation unit.", + "hint": "{0} is the selected translation unit path. {1} is the active file path. {Locked=\"IntelliSense\"} {Locked=\"{0}\"} {Locked=\"{1}\"}" } } From 360056d518c4119a6123318499c3db2352e7abe1 Mon Sep 17 00:00:00 2001 From: Sean McManus Date: Mon, 31 Aug 2026 20:39:43 -0700 Subject: [PATCH 3/6] Improve translation unit selection feedback --- Extension/src/LanguageServer/client.ts | 13 +++--- Extension/src/LanguageServer/extension.ts | 48 ++++++++++++++++++++++- 2 files changed, 53 insertions(+), 8 deletions(-) diff --git a/Extension/src/LanguageServer/client.ts b/Extension/src/LanguageServer/client.ts index ddca43dc3..9e37b1424 100644 --- a/Extension/src/LanguageServer/client.ts +++ b/Extension/src/LanguageServer/client.ts @@ -844,7 +844,7 @@ export interface Client { takeOwnership(document: vscode.TextDocument): void; sendDidOpen(document: vscode.TextDocument): Promise; requestSwitchHeaderSource(rootUri: vscode.Uri, fileName: string, token: vscode.CancellationToken): Thenable; - getTranslationUnitSourceCandidates(uri: vscode.Uri): Promise; + getTranslationUnitSourceCandidates(uri: vscode.Uri, token: vscode.CancellationToken): Promise; selectTranslationUnit(uri: vscode.Uri, translationUnit: string): Promise; updateActiveDocumentTextOptions(): void; didChangeActiveEditor(editor?: vscode.TextEditor, selection?: Range): Promise; @@ -3097,12 +3097,11 @@ export class DefaultClient implements Client { } } - public async getTranslationUnitSourceCandidates(uri: vscode.Uri): Promise { + public async getTranslationUnitSourceCandidates(uri: vscode.Uri, token: vscode.CancellationToken): Promise { const params: TextDocumentIdentifier = { uri: uri.toString() }; - await this.ready; - return this.languageClient.sendRequest( - GetTranslationUnitSourceCandidatesRequest, - params); + await withCancellation(this.ready, token); + return DefaultClient.withLspCancellationHandling( + () => this.languageClient.sendRequest(GetTranslationUnitSourceCandidatesRequest, params, token), token); } public async selectTranslationUnit(uri: vscode.Uri, translationUnit: string): Promise { @@ -4483,7 +4482,7 @@ class NullClient implements Client { takeOwnership(document: vscode.TextDocument): void { } sendDidOpen(document: vscode.TextDocument): Promise { return Promise.resolve(); } requestSwitchHeaderSource(rootUri: vscode.Uri, fileName: string, token: vscode.CancellationToken): Thenable { return Promise.resolve(""); } - getTranslationUnitSourceCandidates(uri: vscode.Uri): Promise { + getTranslationUnitSourceCandidates(uri: vscode.Uri, token: vscode.CancellationToken): Promise { return Promise.resolve({ candidates: [], currentTranslationUnit: "" }); } selectTranslationUnit(uri: vscode.Uri, translationUnit: string): Promise { return Promise.resolve(); } diff --git a/Extension/src/LanguageServer/extension.ts b/Extension/src/LanguageServer/extension.ts index 4bd48d651..74c1f3051 100644 --- a/Extension/src/LanguageServer/extension.ts +++ b/Extension/src/LanguageServer/extension.ts @@ -498,7 +498,53 @@ async function onSelectTranslationUnit(): Promise { } const client: Client = clients.ActiveClient; - const result = await client.getTranslationUnitSourceCandidates(activeEditor.document.uri); + const result = await (async () => { + const tokenSource: vscode.CancellationTokenSource = new vscode.CancellationTokenSource(); + try { + const candidatesPromise = client.getTranslationUnitSourceCandidates(activeEditor.document.uri, tokenSource.token); + const showProgress: boolean = await new Promise((resolve, reject) => { + const timer: NodeJS.Timeout = global.setTimeout(() => resolve(true), 2000); + void candidatesPromise.then(() => { + clearTimeout(timer); + resolve(false); + }, (e) => { + clearTimeout(timer); + reject(e); + }); + }); + + if (!showProgress) { + return await candidatesPromise; + } + + return await vscode.window.withProgress({ + location: vscode.ProgressLocation.Notification, + title: localize('find.translation.units', 'Finding Translation Units...'), + cancellable: true + }, async (_progress, token) => { + const cancellationListener: vscode.Disposable = token.onCancellationRequested(() => tokenSource.cancel()); + try { + return await candidatesPromise; + } finally { + cancellationListener.dispose(); + } + }); + } catch (e) { + if (e instanceof vscode.CancellationError) { + return undefined; + } + throw e; + } finally { + tokenSource.dispose(); + } + })(); + if (result === undefined) { + return; + } + if (result.candidates.length === 0) { + void vscode.window.showInformationMessage(localize('no.translation.units.found', 'No translation units were found for the active file.')); + return; + } const currentTranslationUnit: string = getEditorPath(result.currentTranslationUnit); const items: TranslationUnitQuickPickItem[] = result.candidates.map(candidate => { const editorPath: string = getEditorPath(candidate); From ececda78e58adb8cbdb2ef7c3ac1715e09756b3e Mon Sep 17 00:00:00 2001 From: Sean McManus Date: Tue, 1 Sep 2026 16:58:37 -0700 Subject: [PATCH 4/6] Apply TypeScript formatting --- Extension/src/LanguageServer/client.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Extension/src/LanguageServer/client.ts b/Extension/src/LanguageServer/client.ts index 9e37b1424..573ae1d36 100644 --- a/Extension/src/LanguageServer/client.ts +++ b/Extension/src/LanguageServer/client.ts @@ -2773,7 +2773,7 @@ export class DefaultClient implements Client { const isTrackedFile: boolean = hasNativeFileTypeMappings() ? isTagParsableFile(uri.fsPath) : isTagParsableFile(uri.fsPath) || - (ext !== undefined && this.associations_for_did_change?.has(ext.toLowerCase()) === true); + (ext !== undefined && this.associations_for_did_change?.has(ext.toLowerCase()) === true); if (isTrackedFile) { // VS Code has a bug that causes onDidChange events to happen to files that aren't changed, // which causes a large backlog of "files to parse" to accumulate. From a4501795d28e9f2693a2092714fb412412de6d02 Mon Sep 17 00:00:00 2001 From: Sean McManus Date: Tue, 1 Sep 2026 18:00:22 -0700 Subject: [PATCH 5/6] Revert unrelated TypeScript formatting --- Extension/src/LanguageServer/client.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Extension/src/LanguageServer/client.ts b/Extension/src/LanguageServer/client.ts index 573ae1d36..9e37b1424 100644 --- a/Extension/src/LanguageServer/client.ts +++ b/Extension/src/LanguageServer/client.ts @@ -2773,7 +2773,7 @@ export class DefaultClient implements Client { const isTrackedFile: boolean = hasNativeFileTypeMappings() ? isTagParsableFile(uri.fsPath) : isTagParsableFile(uri.fsPath) || - (ext !== undefined && this.associations_for_did_change?.has(ext.toLowerCase()) === true); + (ext !== undefined && this.associations_for_did_change?.has(ext.toLowerCase()) === true); if (isTrackedFile) { // VS Code has a bug that causes onDidChange events to happen to files that aren't changed, // which causes a large backlog of "files to parse" to accumulate. From fc18aa57cd51974f1370b93ea747a82ff4e9ed92 Mon Sep 17 00:00:00 2001 From: Sean McManus Date: Tue, 1 Sep 2026 18:37:48 -0700 Subject: [PATCH 6/6] Clean up unrelated TypeScript formatting --- Extension/src/LanguageServer/client.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Extension/src/LanguageServer/client.ts b/Extension/src/LanguageServer/client.ts index 9e37b1424..573ae1d36 100644 --- a/Extension/src/LanguageServer/client.ts +++ b/Extension/src/LanguageServer/client.ts @@ -2773,7 +2773,7 @@ export class DefaultClient implements Client { const isTrackedFile: boolean = hasNativeFileTypeMappings() ? isTagParsableFile(uri.fsPath) : isTagParsableFile(uri.fsPath) || - (ext !== undefined && this.associations_for_did_change?.has(ext.toLowerCase()) === true); + (ext !== undefined && this.associations_for_did_change?.has(ext.toLowerCase()) === true); if (isTrackedFile) { // VS Code has a bug that causes onDidChange events to happen to files that aren't changed, // which causes a large backlog of "files to parse" to accumulate.