diff --git a/Extension/package.json b/Extension/package.json index 5c42a5ecd..45d666a38 100644 --- a/Extension/package.json +++ b/Extension/package.json @@ -3990,6 +3990,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%", @@ -6537,6 +6542,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 67836f6f3..573ae1d36 100644 --- a/Extension/src/LanguageServer/client.ts +++ b/Extension/src/LanguageServer/client.ts @@ -216,6 +216,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, token: vscode.CancellationToken): Promise; + selectTranslationUnit(uri: vscode.Uri, translationUnit: string): Promise; updateActiveDocumentTextOptions(): void; didChangeActiveEditor(editor?: vscode.TextEditor, selection?: Range): Promise; restartIntelliSenseForFile(document: vscode.TextDocument): Promise; @@ -2759,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. @@ -3083,6 +3097,19 @@ export class DefaultClient implements Client { } } + public async getTranslationUnitSourceCandidates(uri: vscode.Uri, token: vscode.CancellationToken): Promise { + const params: TextDocumentIdentifier = { uri: uri.toString() }; + await withCancellation(this.ready, token); + return DefaultClient.withLspCancellationHandling( + () => this.languageClient.sendRequest(GetTranslationUnitSourceCandidatesRequest, params, token), token); + } + + 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 ?? "" @@ -4455,6 +4482,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, token: vscode.CancellationToken): 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 7b20221cf..74c1f3051 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,107 @@ 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 (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); + 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 +589,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) { diff --git a/Extension/src/nativeStrings.json b/Extension/src/nativeStrings.json index 149887245..ffe091980 100644 --- a/Extension/src/nativeStrings.json +++ b/Extension/src/nativeStrings.json @@ -777,5 +777,13 @@ "browse_database_disabled_incompatible_storage": { "text": "The browse database was disabled because its storage location does not support SQLite WAL shared memory. Set browse.databaseFilename to a local path.", "hint": "browse.databaseFilename is a setting name. {Locked=\"SQLite WAL\"} {Locked=\"browse.databaseFilename\"}" + }, + "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}\"}" } }