diff --git a/Extension/src/Debugger/configurationProvider.ts b/Extension/src/Debugger/configurationProvider.ts index 067983feac..7e4bf4e896 100644 --- a/Extension/src/Debugger/configurationProvider.ts +++ b/Extension/src/Debugger/configurationProvider.ts @@ -1,1278 +1,1278 @@ -/* -------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All Rights Reserved. - * See 'LICENSE' in the project root for license information. - * ------------------------------------------------------------------------------------------ */ - -import * as jsonc from 'comment-json'; -import * as fs from 'fs'; -import * as glob from 'glob'; -import * as os from 'os'; -import * as path from 'path'; -import { promisify } from 'util'; -import * as vscode from 'vscode'; -import * as nls from 'vscode-nls'; -import * as util from '../common'; -import { isWindows } from '../constants'; -import { expandAllStrings, ExpansionOptions, ExpansionVars } from '../expand'; -import { CppBuildTask, CppBuildTaskDefinition, cppBuildTaskProvider } from '../LanguageServer/cppBuildTaskProvider'; -import { configPrefix } from '../LanguageServer/extension'; -import { CppSettings, OtherSettings } from '../LanguageServer/settings'; -import * as logger from '../logger'; -import { PlatformInformation } from '../platform'; -import { rsync, scp, ssh } from '../SSH/commands'; -import * as Telemetry from '../telemetry'; -import { AttachItemsProvider, AttachPicker, RemoteAttachPicker } from './attachToProcess'; -import { ConfigMenu, ConfigMode, ConfigSource, CppDebugConfiguration, DebuggerEvent, DebuggerType, DebugType, IConfiguration, IConfigurationSnippet, isDebugLaunchStr, MIConfigurations, PipeTransportConfigurations, TaskStatus, WindowsConfigurations, WSLConfigurations } from './configurations'; -import { NativeAttachItemsProviderFactory } from './nativeAttach'; -import { Environment, ParsedEnvironmentFile } from './ParsedEnvironmentFile'; -import * as debugUtils from './utils'; - -nls.config({ messageFormat: nls.MessageFormat.bundle, bundleFormat: nls.BundleFormat.standalone })(); -const localize: nls.LocalizeFunc = nls.loadMessageBundle(); - -enum StepType { - scp = 'scp', - rsync = 'rsync', - ssh = 'ssh', - shell = 'shell', - remoteShell = 'remoteShell', - command = 'command' -} - -const globAsync: (pattern: string, options?: glob.IOptions | undefined) => Promise = promisify(glob); - -/* - * Retrieves configurations from a provider and displays them in a quickpick menu to be selected. - * Ensures that the selected configuration's preLaunchTask (if existent) is populated in the user's task.json. - * Automatically starts debugging for "Build and Debug" configurations. - */ -export class DebugConfigurationProvider implements vscode.DebugConfigurationProvider { - - private type: DebuggerType; - private assetProvider: IConfigurationAssetProvider; - // Keep a list of tasks detected by cppBuildTaskProvider. - private static detectedBuildTasks: CppBuildTask[] = []; - private static detectedCppBuildTasks: CppBuildTask[] = []; - private static detectedCBuildTasks: CppBuildTask[] = []; - protected static recentBuildTaskLabel: string; - - public constructor(assetProvider: IConfigurationAssetProvider, type: DebuggerType) { - this.assetProvider = assetProvider; - this.type = type; - } - - public static ClearDetectedBuildTasks(): void { - DebugConfigurationProvider.detectedCppBuildTasks = []; - DebugConfigurationProvider.detectedCBuildTasks = []; - } - - /** - * Returns a list of initial debug configurations based on contextual information, e.g. package.json or folder. - * resolveDebugConfiguration will be automatically called after this function. - */ - async provideDebugConfigurations(folder?: vscode.WorkspaceFolder, token?: vscode.CancellationToken): Promise { - let configs: CppDebugConfiguration[] | null | undefined = await this.provideDebugConfigurationsForType(this.type, folder, token); - if (!configs) { - configs = []; - } - const defaultTemplateConfig: CppDebugConfiguration | undefined = configs.find(config => isDebugLaunchStr(config.name) && config.request === "launch"); - if (!defaultTemplateConfig) { - throw new Error("Default config not found in provideDebugConfigurations()"); - } - const editor: vscode.TextEditor | undefined = vscode.window.activeTextEditor; - if (!editor || !util.isCppOrCFile(editor.document.uri) || configs.length <= 1) { - return [defaultTemplateConfig]; - } - - const defaultConfig: CppDebugConfiguration[] = this.findDefaultConfig(configs); - // If there was only one config defined for the default task, choose that config, otherwise ask the user to choose. - if (defaultConfig.length === 1) { - return defaultConfig; - } - - // Find the recently used task and place it at the top of quickpick list. - let recentlyUsedConfig: CppDebugConfiguration | undefined; - configs = configs.filter(config => { - if (config.taskStatus !== TaskStatus.recentlyUsed) { - return true; - } else { - recentlyUsedConfig = config; - return false; - } - }); - if (recentlyUsedConfig) { - configs.unshift(recentlyUsedConfig); - } - - const items: ConfigMenu[] = configs.map(config => { - const quickPickConfig: CppDebugConfiguration = { ...config }; - const menuItem: ConfigMenu = { label: config.name, configuration: quickPickConfig, description: config.detail, detail: config.taskStatus }; - // Rename the menu item for the default configuration as its name is non-descriptive. - if (isDebugLaunchStr(menuItem.label)) { - menuItem.label = localize("default.configuration.menuitem", "Default Configuration"); - } - return menuItem; - }); - - const selection: ConfigMenu | undefined = await vscode.window.showQuickPick(this.localizeConfigDetail(items), { placeHolder: localize("select.configuration", "Select a configuration") }); - if (!selection) { - Telemetry.logDebuggerEvent(DebuggerEvent.debugPanel, { "debugType": "debug", "configSource": ConfigSource.unknown, "configMode": ConfigMode.unknown, "cancelled": "true", "succeeded": "true" }); - return []; // User canceled it. - } - - if (this.isClConfiguration(selection.label)) { - this.showErrorIfClNotAvailable(selection.label); - } - - return [selection.configuration]; - } - - /** - * Error checks the provided 'config' without any variables substituted. - * If return "undefined", the debugging will be aborted silently. - * If return "null", the debugging will be aborted and launch.json will be opened. - * resolveDebugConfigurationWithSubstitutedVariables will be automatically called after this function. - */ - async resolveDebugConfiguration(folder: vscode.WorkspaceFolder | undefined, config: CppDebugConfiguration, _token?: vscode.CancellationToken): Promise { - if (!config || !config.type) { - // When DebugConfigurationProviderTriggerKind is Dynamic, this function will be called with an empty config. - // Hence, providing debug configs, and start debugging should be done manually. - // resolveDebugConfiguration will be automatically called after calling provideDebugConfigurations. - const configs: CppDebugConfiguration[] = await this.provideDebugConfigurations(folder); - if (!configs || configs.length === 0) { - Telemetry.logDebuggerEvent(DebuggerEvent.debugPanel, { "debugType": DebugType.debug, "configSource": folder ? ConfigSource.workspaceFolder : ConfigSource.singleFile, "configMode": ConfigMode.noLaunchConfig, "cancelled": "true", "succeeded": "true" }); - return undefined; // aborts debugging silently - } else { - // Currently, we expect only one debug config to be selected. - console.assert(configs.length === 1, "More than one debug config is selected."); - config = configs[0]; - // Keep track of the entry point where the debug config has been selected, for telemetry purposes. - config.debuggerEvent = DebuggerEvent.debugPanel; - config.configSource = folder ? ConfigSource.workspaceFolder : ConfigSource.singleFile; - } - } - - /** If the config is coming from the "Run and Debug" debugPanel, there are three cases where the folder is undefined: - * 1. when debugging is done on a single file where there is no folder open, - * 2. when the debug configuration is defined at the User level (global). - * 3. when the debug configuration is defined at the workspace level. - * If the config is coming from the "Run and Debug" playButton, there is one case where the folder is undefined: - * 1. when debugging is done on a single file where there is no folder open. - */ - - /** Do not resolve PreLaunchTask for these three cases, and let the Vs Code resolve it: - * 1: The existing configs that are found for a single file. - * 2: The existing configs that come from the playButton (the PreLaunchTask should already be defined for these configs). - * 3: The existing configs that come from the debugPanel where the folder is undefined and the PreLaunchTask cannot be found. - */ - - if (config.preLaunchTask) { - config.configSource = this.getDebugConfigSource(config, folder); - const isIntelliSenseDisabled: boolean = new CppSettings((vscode.workspace.workspaceFolders && vscode.workspace.workspaceFolders.length > 0) ? vscode.workspace.workspaceFolders[0]?.uri : undefined).intelliSenseEngine === "disabled"; - // Run the build task if IntelliSense is disabled. - if (isIntelliSenseDisabled) { - try { - await cppBuildTaskProvider.runBuildTask(config.preLaunchTask); - config.preLaunchTask = undefined; - Telemetry.logDebuggerEvent(DebuggerEvent.debugPanel, { "debugType": DebugType.debug, "configSource": config.configSource || ConfigSource.unknown, "configMode": ConfigMode.launchConfig, "cancelled": "false", "succeeded": "true" }); - } catch (err) { - Telemetry.logDebuggerEvent(DebuggerEvent.debugPanel, { "debugType": DebugType.debug, "configSource": config.configSource || ConfigSource.unknown, "configMode": ConfigMode.launchConfig, "cancelled": "false", "succeeded": "false" }); - } - return config; - } - let resolveByVsCode: boolean = false; - const isDebugPanel: boolean = !config.debuggerEvent || (config.debuggerEvent && config.debuggerEvent === DebuggerEvent.debugPanel); - const singleFile: boolean = config.configSource === ConfigSource.singleFile; - const isExistingConfig: boolean = this.isExistingConfig(config, folder); - const isExistingTask: boolean = await this.isExistingTask(config, folder); - if (singleFile) { - if (isExistingConfig) { - resolveByVsCode = true; - } - } else { - if (!isDebugPanel && (isExistingConfig || isExistingTask)) { - resolveByVsCode = true; - } else if (isDebugPanel && !folder && isExistingConfig && !isExistingTask) { - resolveByVsCode = true; - } - } - - // Send the telemetry before writing into files - config.debugType = config.debugType ? config.debugType : DebugType.debug; - const configMode: ConfigMode = isExistingConfig ? ConfigMode.launchConfig : ConfigMode.noLaunchConfig; - // if configuration.debuggerEvent === undefined, it means this configuration is already defined in launch.json and is shown in debugPanel. - Telemetry.logDebuggerEvent(config.debuggerEvent || DebuggerEvent.debugPanel, { "debugType": config.debugType || DebugType.debug, "configSource": config.configSource || ConfigSource.unknown, "configMode": configMode, "cancelled": "false", "succeeded": "true" }); - - if (!resolveByVsCode) { - if (singleFile || (isDebugPanel && !folder && isExistingTask)) { - await this.resolvePreLaunchTask(config, configMode); - config.preLaunchTask = undefined; - } else { - await this.resolvePreLaunchTask(config, configMode, folder); - DebugConfigurationProvider.recentBuildTaskLabelStr = config.preLaunchTask; - } - } else { - DebugConfigurationProvider.recentBuildTaskLabelStr = config.preLaunchTask; - } - } - - // resolveDebugConfigurationWithSubstitutedVariables will be automatically called after this return. - return config; - } - - /** - * This hook is directly called after 'resolveDebugConfiguration' but with all variables substituted. - * This is also ran after the tasks.json has completed. - * - * Try to add all missing attributes to the debug configuration being launched. - * If return "undefined", the debugging will be aborted silently. - * If return "null", the debugging will be aborted and launch.json will be opened. - */ - async resolveDebugConfigurationWithSubstitutedVariables(folder: vscode.WorkspaceFolder | undefined, config: CppDebugConfiguration, token?: vscode.CancellationToken): Promise { - if (!config || !config.type) { - return undefined; // Abort debugging silently. - } - - if (config.type === DebuggerType.cppvsdbg) { - // Fail if cppvsdbg type is running on non-Windows - if (os.platform() !== 'win32') { - void logger.getOutputChannelLogger().showWarningMessage(localize("debugger.not.available", "Debugger of type: '{0}' is only available on Windows. Use type: '{1}' on the current OS platform.", "cppvsdbg", "cppdbg")); - return undefined; // Abort debugging silently. - } - - // Handle legacy 'externalConsole' bool and convert to console: "externalTerminal" - // eslint-disable-next-line no-prototype-builtins - if (config.hasOwnProperty("externalConsole")) { - void logger.getOutputChannelLogger().showWarningMessage(localize("debugger.deprecated.config", "The key '{0}' is deprecated. Please use '{1}' instead.", "externalConsole", "console")); - if (config.externalConsole && !config.console) { - config.console = "externalTerminal"; - } - delete config.externalConsole; - } - - // Disable debug heap by default, enable if 'enableDebugHeap' is set. - if (!config.enableDebugHeap) { - const disableDebugHeapEnvSetting: Environment = { "name": "_NO_DEBUG_HEAP", "value": "1" }; - - if (config.environment && util.isArray(config.environment)) { - config.environment.push(disableDebugHeapEnvSetting); - } else { - config.environment = [disableDebugHeapEnvSetting]; - } - } - } - - // Add environment variables from .env file - this.resolveEnvFile(config, folder); - - await this.expand(config, folder); - - this.resolveSourceFileMapVariables(config); - - // Modify WSL config for OpenDebugAD7 - if (os.platform() === 'win32' && - config.pipeTransport && - config.pipeTransport.pipeProgram) { - let replacedPipeProgram: string | undefined; - const pipeProgramStr: string = config.pipeTransport.pipeProgram.toLowerCase().trim(); - - // OpenDebugAD7 is a 32-bit process. Make sure the WSL pipe transport is using the correct program. - replacedPipeProgram = debugUtils.ArchitectureReplacer.checkAndReplaceWSLPipeProgram(pipeProgramStr, debugUtils.ArchType.ia32); - - // If pipeProgram does not get replaced and there is a pipeCwd, concatenate with pipeProgramStr and attempt to replace. - if (!replacedPipeProgram && !path.isAbsolute(pipeProgramStr) && config.pipeTransport.pipeCwd) { - const pipeCwdStr: string = config.pipeTransport.pipeCwd.toLowerCase().trim(); - const newPipeProgramStr: string = path.join(pipeCwdStr, pipeProgramStr); - - replacedPipeProgram = debugUtils.ArchitectureReplacer.checkAndReplaceWSLPipeProgram(newPipeProgramStr, debugUtils.ArchType.ia32); - } - - if (replacedPipeProgram) { - config.pipeTransport.pipeProgram = replacedPipeProgram; - } - } - - const macOSMIMode: string = config.osx?.MIMode ?? config.MIMode; - const macOSMIDebuggerPath: string = config.osx?.miDebuggerPath ?? config.miDebuggerPath; - - const lldb_mi_10_x_path: string = path.join(util.extensionPath, "debugAdapters", "lldb-mi", "bin", "lldb-mi"); - - // Validate LLDB-MI - if (os.platform() === 'darwin' && // Check for macOS - fs.existsSync(lldb_mi_10_x_path) && // lldb-mi 10.x exists - (!macOSMIMode || macOSMIMode === 'lldb') && - !macOSMIDebuggerPath // User did not provide custom lldb-mi - ) { - const frameworkPath: string | undefined = this.getLLDBFrameworkPath(); - - if (!frameworkPath) { - const moreInfoButton: string = localize("lldb.framework.install.xcode", "More Info"); - const LLDBFrameworkMissingMessage: string = localize("lldb.framework.not.found", "Unable to locate 'LLDB.framework' for lldb-mi. Please install XCode or XCode Command Line Tools."); - - void vscode.window.showErrorMessage(LLDBFrameworkMissingMessage, moreInfoButton) - .then(value => { - if (value === moreInfoButton) { - const helpURL: string = "https://aka.ms/vscode-cpptools/LLDBFrameworkNotFound"; - void vscode.env.openExternal(vscode.Uri.parse(helpURL)); - } - }); - - return undefined; - } - } - - if (config.logging?.engineLogging) { - const outputChannel: logger.Logger = logger.getOutputChannelLogger(); - outputChannel.appendLine(localize("debugger.launchConfig", "Launch configuration:")); - outputChannel.appendLine(JSON.stringify(config, undefined, 2)); - // TODO: Enable when https://github.com/microsoft/vscode/issues/108619 is resolved. - // logger.showOutputChannel(); - } - - // Run deploy steps - if (config.deploySteps && config.deploySteps.length !== 0) { - const codeVersion: number[] = util.getVsCodeVersion(); - if ((util.isNumber(codeVersion[0]) && codeVersion[0] < 1) || (util.isNumber(codeVersion[0]) && codeVersion[0] === 1 && util.isNumber(codeVersion[1]) && codeVersion[1] < 69)) { - void logger.getOutputChannelLogger().showErrorMessage(localize("vs.code.1.69+.required", "'deploySteps' require VS Code 1.69+.")); - return undefined; - } - - const deploySucceeded: boolean = await vscode.window.withProgress({ - location: vscode.ProgressLocation.Notification, - title: localize("running.deploy.steps", "Running deploy steps...") - }, async () => this.deploySteps(config, token)); - - if (!deploySucceeded || token?.isCancellationRequested) { - return undefined; - } - } - - // Pick process if process id is empty - if (config.request === "attach" && !config.processId) { - let processId: string | undefined; - if (config.pipeTransport || config.useExtendedRemote) { - const remoteAttachPicker: RemoteAttachPicker = new RemoteAttachPicker(); - processId = await remoteAttachPicker.ShowAttachEntries(config); - } else { - const attachItemsProvider: AttachItemsProvider = NativeAttachItemsProviderFactory.Get(); - const attacher: AttachPicker = new AttachPicker(attachItemsProvider); - processId = await attacher.ShowAttachEntries(token); - } - - if (processId) { - config.processId = processId; - } else { - void logger.getOutputChannelLogger().showErrorMessage("No process was selected."); - return undefined; - } - } - - return config; - } - - async provideDebugConfigurationsForType(type: DebuggerType, folder?: vscode.WorkspaceFolder, _token?: vscode.CancellationToken): Promise { - const defaultTemplateConfig: CppDebugConfiguration = this.assetProvider.getInitialConfigurations(type).find((config: any) => - isDebugLaunchStr(config.name) && config.request === "launch"); - console.assert(defaultTemplateConfig, "Could not find default debug configuration."); - - const platformInfo: PlatformInformation = await PlatformInformation.GetPlatformInformation(); - - // Import the existing configured tasks from tasks.json file. - const configuredBuildTasks: CppBuildTask[] = await cppBuildTaskProvider.getJsonTasks(); - - let buildTasks: CppBuildTask[] = []; - await this.loadDetectedTasks(); - // Remove the tasks that are already configured once in tasks.json. - const dedupDetectedBuildTasks: CppBuildTask[] = DebugConfigurationProvider.detectedBuildTasks.filter(taskDetected => - !configuredBuildTasks.some(taskJson => taskJson.definition.label === taskDetected.definition.label)); - buildTasks = buildTasks.concat(configuredBuildTasks, dedupDetectedBuildTasks); - - // Filter out build tasks that don't match the currently selected debug configuration type. - if (buildTasks.length !== 0) { - buildTasks = buildTasks.filter((task: CppBuildTask) => { - const command: string = task.definition.command as string; - if (!command) { - return false; - } - if (defaultTemplateConfig.name.startsWith("(Windows) ")) { - if (command.startsWith("cl.exe")) { - return true; - } - } else { - if (!command.startsWith("cl.exe")) { - return true; - } - } - return false; - }); - } - - // Generate new configurations for each build task. - // Generating a task is async, therefore we must *await* *all* map(task => config) Promises to resolve. - let configs: CppDebugConfiguration[] = []; - if (buildTasks.length !== 0) { - configs = (await Promise.all(buildTasks.map>(async task => { - const definition: CppBuildTaskDefinition = task.definition as CppBuildTaskDefinition; - const compilerPath: string = util.isString(definition.command) ? definition.command : definition.command.value; - // Filter out the tasks that has an invalid compiler path. - const compilerPathExists: boolean = path.isAbsolute(compilerPath) ? - // Absolute path, just check if it exists - await util.checkFileExists(compilerPath) : - // Non-absolute. Check on $PATH - (await util.whichAsync(compilerPath) !== undefined); - if (!compilerPathExists) { - logger.getOutputChannelLogger().appendLine(localize('compiler.path.not.exists', "Unable to find {0}. {1} task is ignored.", compilerPath, definition.label)); - } - const compilerName: string = path.basename(compilerPath); - const newConfig: CppDebugConfiguration = { ...defaultTemplateConfig }; // Copy enumerables and properties - newConfig.existing = false; - - newConfig.name = configPrefix + compilerName + " " + this.buildAndDebugActiveFileStr(); - newConfig.preLaunchTask = task.name; - if (newConfig.type === DebuggerType.cppdbg) { - newConfig.externalConsole = false; - } else { - newConfig.console = "integratedTerminal"; - } - // Extract the .exe path from the defined task. - const definedExePath: string | undefined = util.findExePathInArgs(task.definition.args); - newConfig.program = definedExePath ? definedExePath : util.defaultExePath(); - // Add the "detail" property to show the compiler path in QuickPickItem. - // This property will be removed before writing the DebugConfiguration in launch.json. - newConfig.detail = localize("pre.Launch.Task", "preLaunchTask: {0}", task.name); - newConfig.taskDetail = task.detail; - newConfig.taskStatus = task.existing ? - (task.name === DebugConfigurationProvider.recentBuildTaskLabelStr) ? TaskStatus.recentlyUsed : TaskStatus.configured : - TaskStatus.detected; - if (task.isDefault) { - newConfig.isDefault = true; - } - const isCl: boolean = compilerName === "cl.exe"; - newConfig.cwd = isWindows && !isCl && !process.env.PATH?.includes(path.dirname(compilerPath)) ? path.dirname(compilerPath) : "${fileDirname}"; - - if (platformInfo.platform !== "darwin") { - let debuggerName: string; - if (compilerName.startsWith("clang")) { - newConfig.MIMode = "lldb"; - if (isWindows) { - debuggerName = "lldb"; - } else { - debuggerName = "lldb-mi"; - // Search for clang-8, clang-10, etc. - if ((compilerName !== "clang-cl.exe") && (compilerName !== "clang-cpp.exe")) { - const suffixIndex: number = compilerName.indexOf("-"); - if (suffixIndex !== -1) { - const suffix: string = compilerName.substring(suffixIndex); - debuggerName += suffix; - } - } - } - newConfig.type = DebuggerType.cppdbg; - } else if (compilerName === "cl.exe") { - newConfig.miDebuggerPath = undefined; - newConfig.type = DebuggerType.cppvsdbg; - return newConfig; - } else { - debuggerName = "gdb"; - } - if (isWindows) { - debuggerName = debuggerName.endsWith(".exe") ? debuggerName : (debuggerName + ".exe"); - } - const compilerDirname: string = path.dirname(compilerPath); - const debuggerPath: string = path.join(compilerDirname, debuggerName); - - // Check if debuggerPath exists. - if (await util.checkFileExists(debuggerPath)) { - newConfig.miDebuggerPath = debuggerPath; - } else if (await util.whichAsync(debuggerName) !== undefined) { - // Check if debuggerName exists on $PATH - newConfig.miDebuggerPath = debuggerName; - } else { - // Try the usr path for non-Windows platforms. - const usrDebuggerPath: string = path.join("/usr", "bin", debuggerName); - if (!isWindows && await util.checkFileExists(usrDebuggerPath)) { - newConfig.miDebuggerPath = usrDebuggerPath; - } else { - logger.getOutputChannelLogger().appendLine(localize('debugger.path.not.exists', "Unable to find the {0} debugger. The debug configuration for {1} is ignored.", `\"${debuggerName}\"`, compilerName)); - return undefined; - } - } - } - return newConfig; - }))).filter((item): item is CppDebugConfiguration => !!item); - } - configs.push(defaultTemplateConfig); - const existingConfigs: CppDebugConfiguration[] | undefined = this.getLaunchConfigs(folder, type)?.map(config => { - if (!config.detail && config.preLaunchTask) { - config.detail = localize("pre.Launch.Task", "preLaunchTask: {0}", config.preLaunchTask); - } - config.existing = true; - return config; - }); - if (existingConfigs) { - // Remove the detected configs that are already configured once in launch.json. - const dedupExistingConfigs: CppDebugConfiguration[] = configs.filter(detectedConfig => !existingConfigs.some(config => { - if (config.preLaunchTask === detectedConfig.preLaunchTask && config.type === detectedConfig.type && config.request === detectedConfig.request) { - // Carry the default task information. - config.isDefault = detectedConfig.isDefault ? detectedConfig.isDefault : undefined; - return true; - } - return false; - })); - configs = existingConfigs.concat(dedupExistingConfigs); - } - return configs; - } - - private async loadDetectedTasks(): Promise { - const editor: vscode.TextEditor | undefined = vscode.window.activeTextEditor; - const emptyTasks: CppBuildTask[] = []; - if (!editor) { - DebugConfigurationProvider.detectedBuildTasks = emptyTasks; - return; - } - - const fileExt: string = path.extname(editor.document.fileName); - if (!fileExt) { - DebugConfigurationProvider.detectedBuildTasks = emptyTasks; - return; - } - - // Don't offer tasks for header files. - const isHeader: boolean = util.isHeaderFile(editor.document.uri); - if (isHeader) { - DebugConfigurationProvider.detectedBuildTasks = emptyTasks; - return; - } - - // Don't offer tasks if the active file's extension is not a recognized C/C++ extension. - const fileIsCpp: boolean = util.isCppFile(editor.document.uri); - const fileIsC: boolean = util.isCFile(editor.document.uri); - if (!(fileIsCpp || fileIsC)) { - DebugConfigurationProvider.detectedBuildTasks = emptyTasks; - return; - } - - if (fileIsCpp) { - if (!DebugConfigurationProvider.detectedCppBuildTasks || DebugConfigurationProvider.detectedCppBuildTasks.length === 0) { - DebugConfigurationProvider.detectedCppBuildTasks = await cppBuildTaskProvider.getTasks(true); - } - DebugConfigurationProvider.detectedBuildTasks = DebugConfigurationProvider.detectedCppBuildTasks; - } else { - if (!DebugConfigurationProvider.detectedCBuildTasks || DebugConfigurationProvider.detectedCBuildTasks.length === 0) { - DebugConfigurationProvider.detectedCBuildTasks = await cppBuildTaskProvider.getTasks(true); - } - DebugConfigurationProvider.detectedBuildTasks = DebugConfigurationProvider.detectedCBuildTasks; - } - } - - public static get recentBuildTaskLabelStr(): string { - return DebugConfigurationProvider.recentBuildTaskLabel; - } - - public static set recentBuildTaskLabelStr(recentTask: string) { - DebugConfigurationProvider.recentBuildTaskLabel = recentTask; - } - - private buildAndDebugActiveFileStr(): string { - return `${localize("build.and.debug.active.file", 'build and debug active file')}`; - } - - private isClConfiguration(configurationLabel: string): boolean { - return configurationLabel.startsWith("C/C++: cl.exe"); - } - - private showErrorIfClNotAvailable(_configurationLabel: string): boolean { - if (!process.env.DevEnvDir || process.env.DevEnvDir.length === 0) { - void vscode.window.showErrorMessage(localize("cl.exe.not.available", "{0} build and debug is only usable when VS Code is run from the Developer Command Prompt for VS.", "cl.exe")); - return true; - } - return false; - } - - private getLLDBFrameworkPath(): string | undefined { - const LLDBFramework: string = "LLDB.framework"; - // Note: When adding more search paths, make sure the shipped lldb-mi also has it. See Build/lldb-mi.yml and 'install_name_tool' commands. - const searchPaths: string[] = [ - "/Library/Developer/CommandLineTools/Library/PrivateFrameworks", // XCode CLI - "/Applications/Xcode.app/Contents/SharedFrameworks" // App Store XCode - ]; - - for (const searchPath of searchPaths) { - if (fs.existsSync(path.join(searchPath, LLDBFramework))) { - // Found a framework that 'lldb-mi' can use. - return searchPath; - } - } - - const outputChannel: logger.Logger = logger.getOutputChannelLogger(); - - outputChannel.appendLine(localize("lldb.find.failed", "Missing dependency '{0}' for lldb-mi executable.", LLDBFramework)); - outputChannel.appendLine(localize("lldb.search.paths", "Searched in:")); - searchPaths.forEach(searchPath => { - outputChannel.appendLine(`\t${searchPath}`); - }); - const xcodeCLIInstallCmd: string = "xcode-select --install"; - outputChannel.appendLine(localize("lldb.install.help", "To resolve this issue, either install XCode through the Apple App Store or install the XCode Command Line Tools by running '{0}' in a Terminal window.", xcodeCLIInstallCmd)); - logger.showOutputChannel(); - - return undefined; - } - - private resolveEnvFile(config: CppDebugConfiguration, folder?: vscode.WorkspaceFolder): void { - if (config.envFile) { - // replace ${env:???} variables - let envFilePath: string = util.resolveVariables(config.envFile, undefined); - - try { - if (folder && folder.uri && folder.uri.fsPath) { - // Try to replace ${workspaceFolder} or ${workspaceRoot} - envFilePath = envFilePath.replace(/(\${workspaceFolder}|\${workspaceRoot})/g, folder.uri.fsPath); - } - - const parsedFile: ParsedEnvironmentFile = ParsedEnvironmentFile.CreateFromFile(envFilePath, config["environment"]); - - // show error message if single lines cannot get parsed - if (parsedFile.Warning) { - void DebugConfigurationProvider.showFileWarningAsync(parsedFile.Warning, config.envFile); - } - - config.environment = parsedFile.Env; - - delete config.envFile; - } catch (errJS) { - const e: Error = errJS as Error; - throw new Error(localize("envfile.failed", "Failed to use {0}. Reason: {1}", "envFile", e.message)); - } - } - } - - private resolveSourceFileMapVariables(config: CppDebugConfiguration): void { - const messages: string[] = []; - if (config.sourceFileMap) { - for (const sourceFileMapSource of Object.keys(config.sourceFileMap)) { - let message: string = ""; - const sourceFileMapTarget: string = config.sourceFileMap[sourceFileMapSource]; - - let source: string = sourceFileMapSource; - let target: string | object = sourceFileMapTarget; - - // TODO: pass config.environment as 'additionalEnvironment' to resolveVariables when it is { key: value } instead of { "key": key, "value": value } - const newSourceFileMapSource: string = util.resolveVariables(sourceFileMapSource, undefined); - if (sourceFileMapSource !== newSourceFileMapSource) { - message = "\t" + localize("replacing.sourcepath", "Replacing {0} '{1}' with '{2}'.", "sourcePath", sourceFileMapSource, newSourceFileMapSource); - // eslint-disable-next-line @typescript-eslint/no-dynamic-delete - delete config.sourceFileMap[sourceFileMapSource]; - source = newSourceFileMapSource; - } - - if (util.isString(sourceFileMapTarget)) { - const newSourceFileMapTarget: string = util.resolveVariables(sourceFileMapTarget, undefined); - if (sourceFileMapTarget !== newSourceFileMapTarget) { - // Add a space if source was changed, else just tab the target message. - message += message ? ' ' : '\t'; - message += localize("replacing.targetpath", "Replacing {0} '{1}' with '{2}'.", "targetPath", sourceFileMapTarget, newSourceFileMapTarget); - target = newSourceFileMapTarget; - } - } else if (util.isObject(sourceFileMapTarget)) { - const newSourceFileMapTarget: { "editorPath": string; "useForBreakpoints": boolean } = sourceFileMapTarget; - newSourceFileMapTarget["editorPath"] = util.resolveVariables(sourceFileMapTarget["editorPath"], undefined); - - if (sourceFileMapTarget !== newSourceFileMapTarget) { - // Add a space if source was changed, else just tab the target message. - message += message ? ' ' : '\t'; - message += localize("replacing.editorPath", "Replacing {0} '{1}' with '{2}'.", "editorPath", sourceFileMapTarget, newSourceFileMapTarget["editorPath"]); - target = newSourceFileMapTarget; - } - } - - if (message) { - config.sourceFileMap[source] = target; - messages.push(message); - } - } - - if (messages.length > 0) { - logger.getOutputChannel().appendLine(localize("resolving.variables.in.sourcefilemap", "Resolving variables in {0}...", "sourceFileMap")); - messages.forEach((message) => { - logger.getOutputChannel().appendLine(message); - }); - logger.showOutputChannel(); - } - } - } - - private static async showFileWarningAsync(message: string, fileName: string): Promise { - const openItem: vscode.MessageItem = { title: localize("open.envfile", "Open {0}", "envFile") }; - const result: vscode.MessageItem | undefined = await vscode.window.showWarningMessage(message, openItem); - if (result && result.title === openItem.title) { - const doc: vscode.TextDocument = await vscode.workspace.openTextDocument(fileName); - if (doc) { - void vscode.window.showTextDocument(doc); - } - } - } - - private localizeConfigDetail(items: ConfigMenu[]): ConfigMenu[] { - items.map((item: ConfigMenu) => { - switch (item.detail) { - case TaskStatus.recentlyUsed: { - item.detail = localize("recently.used.task", "Recently Used Task"); - break; - } - case TaskStatus.configured: { - item.detail = localize("configured.task", "Configured Task"); - break; - } - case TaskStatus.detected: { - item.detail = localize("detected.task", "Detected Task"); - break; - } - default: { - break; - } - } - if (item.configuration.taskDetail) { - // Add the compiler path of the preLaunchTask to the description of the debug configuration. - item.detail = (item.detail ?? "") + " (" + item.configuration.taskDetail + ")"; - } - }); - return items; - } - - private findDefaultConfig(configs: CppDebugConfiguration[]): CppDebugConfiguration[] { - // eslint-disable-next-line no-prototype-builtins - return configs.filter((config: CppDebugConfiguration) => config.hasOwnProperty("isDefault") && config.isDefault); - } - - private async isExistingTask(config: CppDebugConfiguration, folder?: vscode.WorkspaceFolder): Promise { - if (config.taskStatus && (config.taskStatus !== TaskStatus.detected)) { - return true; - } else if (config.taskStatus && (config.taskStatus === TaskStatus.detected)) { - return false; - } - return cppBuildTaskProvider.isExistingTask(config.preLaunchTask, folder); - } - - private isExistingConfig(config: CppDebugConfiguration, folder?: vscode.WorkspaceFolder): boolean { - if (config.existing) { - return config.existing; - } - const configs: CppDebugConfiguration[] | undefined = this.getLaunchConfigs(folder, config.type); - if (configs && configs.length > 0) { - const selectedConfig: any | undefined = configs.find((item: any) => item.name && item.name === config.name); - if (selectedConfig) { - return true; - } - } - return false; - } - - private getDebugConfigSource(config: CppDebugConfiguration, folder?: vscode.WorkspaceFolder): ConfigSource | undefined { - if (config.configSource) { - return config.configSource; - } - const isExistingConfig: boolean = this.isExistingConfig(config, folder); - if (!isExistingConfig && !folder) { - return ConfigSource.singleFile; - } else if (!isExistingConfig) { - return ConfigSource.workspaceFolder; - } - - const configs: CppDebugConfiguration[] | undefined = this.getLaunchConfigs(folder, config.type); - const matchingConfig: CppDebugConfiguration | undefined = configs?.find((item: any) => item.name && item.name === config.name); - if (matchingConfig?.configSource) { - return matchingConfig.configSource; - } - return ConfigSource.unknown; - } - - public getLaunchConfigs(folder?: vscode.WorkspaceFolder, type?: DebuggerType | string): CppDebugConfiguration[] | undefined { - // Get existing debug configurations from launch.json or user/workspace "launch" settings. - const WorkspaceConfigs: vscode.WorkspaceConfiguration = vscode.workspace.getConfiguration('launch', folder); - const configs: any = WorkspaceConfigs.inspect('configurations'); - if (!configs) { - return undefined; - } - let detailedConfigs: CppDebugConfiguration[] = []; - if (configs.workspaceFolderValue !== undefined) { - detailedConfigs = detailedConfigs.concat(configs.workspaceFolderValue.map((item: CppDebugConfiguration) => { - item.configSource = ConfigSource.workspaceFolder; - return item; - })); - } - if (configs.workspaceValue !== undefined) { - detailedConfigs = detailedConfigs.concat(configs.workspaceValue.map((item: CppDebugConfiguration) => { - item.configSource = ConfigSource.workspace; - return item; - })); - } - if (configs.globalValue !== undefined) { - detailedConfigs = detailedConfigs.concat(configs.globalValue.map((item: CppDebugConfiguration) => { - item.configSource = ConfigSource.global; - return item; - })); - } - detailedConfigs = detailedConfigs.filter((config: any) => config.name && config.request === "launch" && type ? (config.type === type) : true); - return detailedConfigs; - } - - private getLaunchJsonPath(): string | undefined { - return util.getJsonPath("launch.json"); - } - - private getRawLaunchJson(): Promise { - const path: string | undefined = this.getLaunchJsonPath(); - return util.getRawJson(path); - } - - public async writeDebugConfig(config: vscode.DebugConfiguration, isExistingConfig: boolean, _folder?: vscode.WorkspaceFolder): Promise { - const launchJsonPath: string | undefined = this.getLaunchJsonPath(); - - if (isExistingConfig) { - if (launchJsonPath) { - const doc: vscode.TextDocument = await vscode.workspace.openTextDocument(launchJsonPath); - if (doc) { - void vscode.window.showTextDocument(doc); - } - } - return; - } - const rawLaunchJson: any = await this.getRawLaunchJson(); - if (!rawLaunchJson.configurations) { - rawLaunchJson.configurations = []; - } - if (!rawLaunchJson.version) { - rawLaunchJson.version = "2.0.0"; - } - - // Remove the extra properties that are not a part of the vsCode.DebugConfiguration. - config.detail = undefined; - config.taskStatus = undefined; - config.isDefault = undefined; - config.source = undefined; - config.debuggerEvent = undefined; - config.debugType = undefined; - config.existing = undefined; - config.taskDetail = undefined; - rawLaunchJson.configurations.push(config); - - if (!launchJsonPath) { - throw new Error("Failed to get tasksJsonPath in checkBuildTaskExists()"); - } - - const settings: OtherSettings = new OtherSettings(); - await util.writeFileText(launchJsonPath, jsonc.stringify(rawLaunchJson, null, settings.editorTabSize)); - await vscode.workspace.openTextDocument(launchJsonPath); - const doc: vscode.TextDocument = await vscode.workspace.openTextDocument(launchJsonPath); - if (doc) { - void vscode.window.showTextDocument(doc); - } - } - - public async addDebugConfiguration(textEditor: vscode.TextEditor): Promise { - const folder: vscode.WorkspaceFolder | undefined = vscode.workspace.getWorkspaceFolder(textEditor.document.uri); - if (!folder) { - return; - } - const selectedConfig: vscode.DebugConfiguration | undefined = await this.selectConfiguration(textEditor, false, true); - if (!selectedConfig) { - Telemetry.logDebuggerEvent(DebuggerEvent.addConfigGear, { "configSource": ConfigSource.workspaceFolder, "configMode": ConfigMode.launchConfig, "cancelled": "true", "succeeded": "true" }); - return; // User canceled it. - } - - const isExistingConfig: boolean = this.isExistingConfig(selectedConfig, folder); - // Write preLaunchTask into tasks.json file. - if (!isExistingConfig && selectedConfig.preLaunchTask && (selectedConfig.taskStatus && selectedConfig.taskStatus === TaskStatus.detected)) { - await cppBuildTaskProvider.writeBuildTask(selectedConfig.preLaunchTask); - } - // Remove the extra properties that are not a part of the DebugConfiguration, as these properties will be written in launch.json. - selectedConfig.detail = undefined; - selectedConfig.taskStatus = undefined; - selectedConfig.isDefault = undefined; - selectedConfig.source = undefined; - selectedConfig.debuggerEvent = undefined; - // Write debug configuration in launch.json file. - await this.writeDebugConfig(selectedConfig, isExistingConfig, folder); - Telemetry.logDebuggerEvent(DebuggerEvent.addConfigGear, { "configSource": ConfigSource.workspaceFolder, "configMode": ConfigMode.launchConfig, "cancelled": "false", "succeeded": "true" }); - } - - public async buildAndRun(textEditor: vscode.TextEditor): Promise { - // Turn off the debug mode. - return this.buildAndDebug(textEditor, false); - } - - public async buildAndDebug(textEditor: vscode.TextEditor, debugModeOn: boolean = true): Promise { - let folder: vscode.WorkspaceFolder | undefined = vscode.workspace.getWorkspaceFolder(textEditor.document.uri); - const selectedConfig: CppDebugConfiguration | undefined = await this.selectConfiguration(textEditor); - if (!selectedConfig) { - Telemetry.logDebuggerEvent(DebuggerEvent.playButton, { "debugType": debugModeOn ? DebugType.debug : DebugType.run, "configSource": ConfigSource.unknown, "cancelled": "true", "succeeded": "true" }); - return; // User canceled it. - } - - // Keep track of the entry point where the debug has been selected, for telemetry purposes. - selectedConfig.debuggerEvent = DebuggerEvent.playButton; - // If the configs are coming from workspace or global settings and the task is not found in tasks.json, let that to be resolved by VsCode. - if (selectedConfig.preLaunchTask && selectedConfig.configSource && - (selectedConfig.configSource === ConfigSource.global || selectedConfig.configSource === ConfigSource.workspace) && - !await this.isExistingTask(selectedConfig)) { - folder = undefined; - } - selectedConfig.debugType = debugModeOn ? DebugType.debug : DebugType.run; - // startDebugging will trigger a call to resolveDebugConfiguration. - await vscode.debug.startDebugging(folder, selectedConfig, { noDebug: !debugModeOn }); - } - - private async selectConfiguration(textEditor: vscode.TextEditor, pickDefault: boolean = true, onlyWorkspaceFolder: boolean = false): Promise { - const folder: vscode.WorkspaceFolder | undefined = vscode.workspace.getWorkspaceFolder(textEditor.document.uri); - if (!util.isCppOrCFile(textEditor.document.uri)) { - void vscode.window.showErrorMessage(localize("cannot.build.non.cpp", 'Cannot build and debug because the active file is not a C or C++ source file.')); - return; - } - - // Get debug configurations for all debugger types. - let configs: CppDebugConfiguration[] = await this.provideDebugConfigurationsForType(DebuggerType.cppdbg, folder); - if (os.platform() === 'win32') { - configs = configs.concat(await this.provideDebugConfigurationsForType(DebuggerType.cppvsdbg, folder)); - } - if (onlyWorkspaceFolder) { - configs = configs.filter(item => !item.configSource || item.configSource === ConfigSource.workspaceFolder); - } - - const defaultConfig: CppDebugConfiguration[] | undefined = pickDefault ? this.findDefaultConfig(configs) : undefined; - - const items: ConfigMenu[] = configs.map(config => ({ label: config.name, configuration: config, description: config.detail, detail: config.taskStatus })); - - let selection: ConfigMenu | undefined; - - // if there was only one config for the default task, choose that config, otherwise ask the user to choose. - if (defaultConfig && defaultConfig.length === 1) { - selection = { label: defaultConfig[0].name, configuration: defaultConfig[0], description: defaultConfig[0].detail, detail: defaultConfig[0].taskStatus }; - } else { - let sortedItems: ConfigMenu[] = []; - // Find the recently used task and place it at the top of quickpick list. - const recentTask: ConfigMenu[] = items.filter(item => item.configuration.preLaunchTask && item.configuration.preLaunchTask === DebugConfigurationProvider.recentBuildTaskLabelStr); - if (recentTask.length !== 0 && recentTask[0].detail !== TaskStatus.detected) { - recentTask[0].detail = TaskStatus.recentlyUsed; - sortedItems.push(recentTask[0]); - } - sortedItems = sortedItems.concat(items.filter(item => item.detail === TaskStatus.configured)); - sortedItems = sortedItems.concat(items.filter(item => item.detail === TaskStatus.detected)); - sortedItems = sortedItems.concat(items.filter(item => item.detail === undefined)); - - selection = await vscode.window.showQuickPick(this.localizeConfigDetail(sortedItems), { - placeHolder: items.length === 0 ? localize("no.compiler.found", "No compiler found") : localize("select.debug.configuration", "Select a debug configuration") - }); - } - if (selection && this.isClConfiguration(selection.configuration.name) && this.showErrorIfClNotAvailable(selection.configuration.name)) { - return; - } - return selection?.configuration; - } - - private async resolvePreLaunchTask(config: CppDebugConfiguration, configMode: ConfigMode, folder?: vscode.WorkspaceFolder | undefined): Promise { - if (config.preLaunchTask) { - try { - if (config.configSource === ConfigSource.singleFile) { - // In case of singleFile, remove the preLaunch task from the debug configuration and run it here instead. - await cppBuildTaskProvider.runBuildTask(config.preLaunchTask); - } else { - await cppBuildTaskProvider.writeDefaultBuildTask(config.preLaunchTask, folder); - } - } catch (errJS) { - const e: Error = errJS as Error; - if (e && e.message === util.failedToParseJson) { - void vscode.window.showErrorMessage(util.failedToParseJson); - } - Telemetry.logDebuggerEvent(config.debuggerEvent || DebuggerEvent.debugPanel, { "debugType": config.debugType || DebugType.debug, "configSource": config.configSource || ConfigSource.unknown, "configMode": configMode, "cancelled": "false", "succeeded": "false" }); - } - } - } - - private async expand(config: vscode.DebugConfiguration, folder: vscode.WorkspaceFolder | undefined): Promise { - const folderPath: string | undefined = folder?.uri.fsPath || vscode.workspace.workspaceFolders?.[0].uri.fsPath; - const vars: ExpansionVars = config.variables ? config.variables : {}; - vars.workspaceFolder = folderPath || '{workspaceFolder}'; - vars.workspaceFolderBasename = folderPath ? path.basename(folderPath) : '{workspaceFolderBasename}'; - const expansionOptions: ExpansionOptions = { vars, recursive: true }; - return expandAllStrings(config, expansionOptions); - } - - // Returns true when ALL steps succeed; stop all subsequent steps if one fails - private async deploySteps(config: vscode.DebugConfiguration, cancellationToken?: vscode.CancellationToken): Promise { - let succeeded: boolean = true; - const deployStart: number = new Date().getTime(); - - for (const step of config.deploySteps) { - succeeded = await this.singleDeployStep(config, step, cancellationToken); - if (!succeeded) { - break; - } - } - - const deployEnd: number = new Date().getTime(); - - const telemetryProperties: { [key: string]: string } = { - Succeeded: `${succeeded}`, - IsDebugging: `${!config.noDebug || false}` - }; - const telemetryMetrics: { [key: string]: number } = { - NumSteps: config.deploySteps.length, - Duration: deployEnd - deployStart - }; - Telemetry.logDebuggerEvent('deploy', telemetryProperties, telemetryMetrics); - - return succeeded; - } - - private async singleDeployStep(config: vscode.DebugConfiguration, step: any, cancellationToken?: vscode.CancellationToken): Promise { - if ((config.noDebug && step.debug === true) || (!config.noDebug && step.debug === false)) { - // Skip steps that doesn't match current launch mode. Explicit true/false check, since a step is always run when debug is undefined. - return true; - } - const stepType: StepType = step.type; - switch (stepType) { - case StepType.command: { - // VS Code commands are the same regardless of which extension invokes them, so just invoke them here. - if (step.args && !Array.isArray(step.args)) { - void logger.getOutputChannelLogger().showErrorMessage(localize('command.args.must.be.array', '"args" in command deploy step must be an array.')); - return false; - } - const returnCode: unknown = await vscode.commands.executeCommand(step.command, ...step.args); - return !returnCode; - } - case StepType.scp: - case StepType.rsync: { - const isScp: boolean = stepType === StepType.scp; - if (!step.files || !step.targetDir || !step.host) { - void logger.getOutputChannelLogger().showErrorMessage(localize('missing.properties.copyFile', '"host", "files", and "targetDir" are required in {0} steps.', isScp ? 'SCP' : 'rsync')); - return false; - } - const host: util.ISshHostInfo = util.isString(step.host) ? { hostName: step.host } : { hostName: step.host.hostName, user: step.host.user, port: step.host.port }; - const jumpHosts: util.ISshHostInfo[] = step.host.jumpHosts; - let files: vscode.Uri[] = []; - if (util.isString(step.files)) { - files = files.concat((await globAsync(step.files)).map(file => vscode.Uri.file(file))); - } else if (util.isArrayOfString(step.files)) { - for (const fileGlob of (step.files as string[])) { - files = files.concat((await globAsync(fileGlob)).map(file => vscode.Uri.file(file))); - } - } else { - void logger.getOutputChannelLogger().showErrorMessage(localize('incorrect.files.type.copyFile', '"files" must be a string or an array of strings in {0} steps.', isScp ? 'SCP' : 'rsync')); - return false; - } - - let scpResult: util.ProcessReturnType; - if (isScp) { - scpResult = await scp(files, host, step.targetDir, config.scpPath, config.recursive, jumpHosts, cancellationToken); - } else { - scpResult = await rsync(files, host, step.targetDir, config.scpPath, config.recursive, jumpHosts, cancellationToken); - } - - if (!scpResult.succeeded || cancellationToken?.isCancellationRequested) { - return false; - } - break; - } - case StepType.ssh: { - if (!step.host || !step.command) { - void logger.getOutputChannelLogger().showErrorMessage(localize('missing.properties.ssh', '"host" and "command" are required for ssh steps.')); - return false; - } - const host: util.ISshHostInfo = util.isString(step.host) ? { hostName: step.host } : { hostName: step.host.hostName, user: step.host.user, port: step.host.port }; - const jumpHosts: util.ISshHostInfo[] = step.host.jumpHosts; - const localForwards: util.ISshLocalForwardInfo[] = step.host.localForwards; - const continueOn: string = step.continueOn; - const sshResult: util.ProcessReturnType = await ssh(host, step.command, config.sshPath, jumpHosts, localForwards, continueOn, cancellationToken); - if (!sshResult.succeeded || cancellationToken?.isCancellationRequested) { - return false; - } - break; - } - case StepType.shell: { - if (!step.command) { - void logger.getOutputChannelLogger().showErrorMessage(localize('missing.properties.shell', '"command" is required for shell steps.')); - return false; - } - const taskResult: util.ProcessReturnType = await util.spawnChildProcess(step.command, undefined, step.continueOn); - if (!taskResult.succeeded || cancellationToken?.isCancellationRequested) { - void logger.getOutputChannelLogger().showErrorMessage(taskResult.output); - return false; - } - break; - } - default: { - logger.getOutputChannelLogger().appendLine(localize('deploy.step.type.not.supported', 'Deploy step type {0} is not supported.', step.type)); - return false; - } - } - return true; - } -} - -export interface IConfigurationAssetProvider { - getInitialConfigurations(debuggerType: DebuggerType): any; - getConfigurationSnippets(): vscode.CompletionItem[]; -} - -export class ConfigurationAssetProviderFactory { - public static getConfigurationProvider(): IConfigurationAssetProvider { - switch (os.platform()) { - case 'win32': - return new WindowsConfigurationProvider(); - case 'darwin': - return new OSXConfigurationProvider(); - case 'linux': - return new LinuxConfigurationProvider(); - default: - throw new Error(localize("unexpected.os", "Unexpected OS type")); - } - } -} - -abstract class DefaultConfigurationProvider implements IConfigurationAssetProvider { - configurations: IConfiguration[] = []; - - public getInitialConfigurations(debuggerType: DebuggerType): any { - const configurationSnippet: IConfigurationSnippet[] = []; - - // Only launch configurations are initial configurations - this.configurations.forEach(configuration => { - configurationSnippet.push(configuration.GetLaunchConfiguration()); - }); - - const initialConfigurations: any = configurationSnippet.filter(snippet => snippet.debuggerType === debuggerType && snippet.isInitialConfiguration) - .map(snippet => JSON.parse(snippet.bodyText)); - - // If configurations is empty, then it will only have an empty configurations array in launch.json. Users can still add snippets. - return initialConfigurations; - } - - public getConfigurationSnippets(): vscode.CompletionItem[] { - const completionItems: vscode.CompletionItem[] = []; - - this.configurations.forEach(configuration => { - completionItems.push(convertConfigurationSnippetToCompletionItem(configuration.GetLaunchConfiguration())); - completionItems.push(convertConfigurationSnippetToCompletionItem(configuration.GetAttachConfiguration())); - }); - - return completionItems; - } -} - -class WindowsConfigurationProvider extends DefaultConfigurationProvider { - private executable: string = "a.exe"; - private pipeProgram: string = "<" + localize("path.to.pipe.program", "full path to pipe program such as {0}", "plink.exe").replace(/"/g, '') + ">"; - private MIMode: string = 'gdb'; - private setupCommandsBlock: string = `"setupCommands": [ - { - "description": "${localize("enable.pretty.printing", "Enable pretty-printing for {0}", "gdb").replace(/"/g, '')}", - "text": "-enable-pretty-printing", - "ignoreFailures": true - }, - { - "description": "${localize("enable.intel.disassembly.flavor", "Set Disassembly Flavor to {0}", "Intel").replace(/"/g, '')}", - "text": "-gdb-set disassembly-flavor intel", - "ignoreFailures": true - } -]`; - - constructor() { - super(); - this.configurations = [ - new MIConfigurations(this.MIMode, this.executable, this.pipeProgram, this.setupCommandsBlock), - new PipeTransportConfigurations(this.MIMode, this.executable, this.pipeProgram, this.setupCommandsBlock), - new WindowsConfigurations(this.MIMode, this.executable, this.pipeProgram, this.setupCommandsBlock), - new WSLConfigurations(this.MIMode, this.executable, this.pipeProgram, this.setupCommandsBlock) - ]; - } -} - -class OSXConfigurationProvider extends DefaultConfigurationProvider { - private MIMode: string = 'lldb'; - private executable: string = "a.out"; - private pipeProgram: string = "/usr/bin/ssh"; - - constructor() { - super(); - this.configurations = [ - new MIConfigurations(this.MIMode, this.executable, this.pipeProgram) - ]; - } -} - -class LinuxConfigurationProvider extends DefaultConfigurationProvider { - private MIMode: string = 'gdb'; - private setupCommandsBlock: string = `"setupCommands": [ - { - "description": "${localize("enable.pretty.printing", "Enable pretty-printing for {0}", "gdb").replace(/"/g, '')}", - "text": "-enable-pretty-printing", - "ignoreFailures": true - }, - { - "description": "${localize("enable.intel.disassembly.flavor", "Set Disassembly Flavor to {0}", "Intel").replace(/"/g, '')}", - "text": "-gdb-set disassembly-flavor intel", - "ignoreFailures": true - } -]`; - private executable: string = "a.out"; - private pipeProgram: string = "/usr/bin/ssh"; - - constructor() { - super(); - this.configurations = [ - new MIConfigurations(this.MIMode, this.executable, this.pipeProgram, this.setupCommandsBlock), - new PipeTransportConfigurations(this.MIMode, this.executable, this.pipeProgram, this.setupCommandsBlock) - ]; - } -} - -function convertConfigurationSnippetToCompletionItem(snippet: IConfigurationSnippet): vscode.CompletionItem { - const item: vscode.CompletionItem = new vscode.CompletionItem(snippet.label, vscode.CompletionItemKind.Module); - - item.insertText = snippet.bodyText; - - return item; -} - -export class ConfigurationSnippetProvider implements vscode.CompletionItemProvider { - private provider: IConfigurationAssetProvider; - private snippets: vscode.CompletionItem[]; - - constructor(provider: IConfigurationAssetProvider) { - this.provider = provider; - this.snippets = this.provider.getConfigurationSnippets(); - } - public resolveCompletionItem(item: vscode.CompletionItem, _token: vscode.CancellationToken): Thenable { - return Promise.resolve(item); - } - - // This function will only provide completion items via the Add Configuration Button - // There are two cases where the configuration array has nothing or has some items. - // 1. If it has nothing, insert a snippet the user selected. - // 2. If there are items, the Add Configuration button will append it to the start of the configuration array. This function inserts a comma at the end of the snippet. - public provideCompletionItems(document: vscode.TextDocument, _position: vscode.Position, _token: vscode.CancellationToken, _context: vscode.CompletionContext): Thenable { - let items: vscode.CompletionItem[] = this.snippets; - let hasLaunchConfigs: boolean = false; - try { - const launch: any = jsonc.parse(document.getText()); - hasLaunchConfigs = launch.configurations.length !== 0; - } catch { - // ignore - } - - // Check to see if the array is empty, so any additional inserted snippets will need commas. - if (hasLaunchConfigs) { - items = []; - - // Make a copy of each snippet since we are adding a comma to the end of the insertText. - this.snippets.forEach((item) => items.push({ ...item })); - - items.map((item) => { - item.insertText = item.insertText + ','; // Add comma - }); - } - - return Promise.resolve(new vscode.CompletionList(items, true)); - } -} +/* -------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All Rights Reserved. + * See 'LICENSE' in the project root for license information. + * ------------------------------------------------------------------------------------------ */ + +import * as jsonc from 'comment-json'; +import * as fs from 'fs'; +import * as glob from 'glob'; +import * as os from 'os'; +import * as path from 'path'; +import { promisify } from 'util'; +import * as vscode from 'vscode'; +import * as nls from 'vscode-nls'; +import * as util from '../common'; +import { isWindows } from '../constants'; +import { expandAllStrings, ExpansionOptions, ExpansionVars } from '../expand'; +import { CppBuildTask, CppBuildTaskDefinition, cppBuildTaskProvider } from '../LanguageServer/cppBuildTaskProvider'; +import { configPrefix } from '../LanguageServer/extension'; +import { CppSettings, OtherSettings } from '../LanguageServer/settings'; +import { getOutputChannel, getOutputChannelLogger, Logger, showOutputChannel } from '../logger'; +import { PlatformInformation } from '../platform'; +import { rsync, scp, ssh } from '../SSH/commands'; +import * as Telemetry from '../telemetry'; +import { AttachItemsProvider, AttachPicker, RemoteAttachPicker } from './attachToProcess'; +import { ConfigMenu, ConfigMode, ConfigSource, CppDebugConfiguration, DebuggerEvent, DebuggerType, DebugType, IConfiguration, IConfigurationSnippet, isDebugLaunchStr, MIConfigurations, PipeTransportConfigurations, TaskStatus, WindowsConfigurations, WSLConfigurations } from './configurations'; +import { NativeAttachItemsProviderFactory } from './nativeAttach'; +import { Environment, ParsedEnvironmentFile } from './ParsedEnvironmentFile'; +import * as debugUtils from './utils'; + +nls.config({ messageFormat: nls.MessageFormat.bundle, bundleFormat: nls.BundleFormat.standalone })(); +const localize: nls.LocalizeFunc = nls.loadMessageBundle(); + +enum StepType { + scp = 'scp', + rsync = 'rsync', + ssh = 'ssh', + shell = 'shell', + remoteShell = 'remoteShell', + command = 'command' +} + +const globAsync: (pattern: string, options?: glob.IOptions | undefined) => Promise = promisify(glob); + +/* + * Retrieves configurations from a provider and displays them in a quickpick menu to be selected. + * Ensures that the selected configuration's preLaunchTask (if existent) is populated in the user's task.json. + * Automatically starts debugging for "Build and Debug" configurations. + */ +export class DebugConfigurationProvider implements vscode.DebugConfigurationProvider { + + private type: DebuggerType; + private assetProvider: IConfigurationAssetProvider; + // Keep a list of tasks detected by cppBuildTaskProvider. + private static detectedBuildTasks: CppBuildTask[] = []; + private static detectedCppBuildTasks: CppBuildTask[] = []; + private static detectedCBuildTasks: CppBuildTask[] = []; + protected static recentBuildTaskLabel: string; + + public constructor(assetProvider: IConfigurationAssetProvider, type: DebuggerType) { + this.assetProvider = assetProvider; + this.type = type; + } + + public static ClearDetectedBuildTasks(): void { + DebugConfigurationProvider.detectedCppBuildTasks = []; + DebugConfigurationProvider.detectedCBuildTasks = []; + } + + /** + * Returns a list of initial debug configurations based on contextual information, e.g. package.json or folder. + * resolveDebugConfiguration will be automatically called after this function. + */ + async provideDebugConfigurations(folder?: vscode.WorkspaceFolder, token?: vscode.CancellationToken): Promise { + let configs: CppDebugConfiguration[] | null | undefined = await this.provideDebugConfigurationsForType(this.type, folder, token); + if (!configs) { + configs = []; + } + const defaultTemplateConfig: CppDebugConfiguration | undefined = configs.find(config => isDebugLaunchStr(config.name) && config.request === "launch"); + if (!defaultTemplateConfig) { + throw new Error("Default config not found in provideDebugConfigurations()"); + } + const editor: vscode.TextEditor | undefined = vscode.window.activeTextEditor; + if (!editor || !util.isCppOrCFile(editor.document.uri) || configs.length <= 1) { + return [defaultTemplateConfig]; + } + + const defaultConfig: CppDebugConfiguration[] = this.findDefaultConfig(configs); + // If there was only one config defined for the default task, choose that config, otherwise ask the user to choose. + if (defaultConfig.length === 1) { + return defaultConfig; + } + + // Find the recently used task and place it at the top of quickpick list. + let recentlyUsedConfig: CppDebugConfiguration | undefined; + configs = configs.filter(config => { + if (config.taskStatus !== TaskStatus.recentlyUsed) { + return true; + } else { + recentlyUsedConfig = config; + return false; + } + }); + if (recentlyUsedConfig) { + configs.unshift(recentlyUsedConfig); + } + + const items: ConfigMenu[] = configs.map(config => { + const quickPickConfig: CppDebugConfiguration = { ...config }; + const menuItem: ConfigMenu = { label: config.name, configuration: quickPickConfig, description: config.detail, detail: config.taskStatus }; + // Rename the menu item for the default configuration as its name is non-descriptive. + if (isDebugLaunchStr(menuItem.label)) { + menuItem.label = localize("default.configuration.menuitem", "Default Configuration"); + } + return menuItem; + }); + + const selection: ConfigMenu | undefined = await vscode.window.showQuickPick(this.localizeConfigDetail(items), { placeHolder: localize("select.configuration", "Select a configuration") }); + if (!selection) { + Telemetry.logDebuggerEvent(DebuggerEvent.debugPanel, { "debugType": "debug", "configSource": ConfigSource.unknown, "configMode": ConfigMode.unknown, "cancelled": "true", "succeeded": "true" }); + return []; // User canceled it. + } + + if (this.isClConfiguration(selection.label)) { + this.showErrorIfClNotAvailable(selection.label); + } + + return [selection.configuration]; + } + + /** + * Error checks the provided 'config' without any variables substituted. + * If return "undefined", the debugging will be aborted silently. + * If return "null", the debugging will be aborted and launch.json will be opened. + * resolveDebugConfigurationWithSubstitutedVariables will be automatically called after this function. + */ + async resolveDebugConfiguration(folder: vscode.WorkspaceFolder | undefined, config: CppDebugConfiguration, _token?: vscode.CancellationToken): Promise { + if (!config || !config.type) { + // When DebugConfigurationProviderTriggerKind is Dynamic, this function will be called with an empty config. + // Hence, providing debug configs, and start debugging should be done manually. + // resolveDebugConfiguration will be automatically called after calling provideDebugConfigurations. + const configs: CppDebugConfiguration[] = await this.provideDebugConfigurations(folder); + if (!configs || configs.length === 0) { + Telemetry.logDebuggerEvent(DebuggerEvent.debugPanel, { "debugType": DebugType.debug, "configSource": folder ? ConfigSource.workspaceFolder : ConfigSource.singleFile, "configMode": ConfigMode.noLaunchConfig, "cancelled": "true", "succeeded": "true" }); + return undefined; // aborts debugging silently + } else { + // Currently, we expect only one debug config to be selected. + console.assert(configs.length === 1, "More than one debug config is selected."); + config = configs[0]; + // Keep track of the entry point where the debug config has been selected, for telemetry purposes. + config.debuggerEvent = DebuggerEvent.debugPanel; + config.configSource = folder ? ConfigSource.workspaceFolder : ConfigSource.singleFile; + } + } + + /** If the config is coming from the "Run and Debug" debugPanel, there are three cases where the folder is undefined: + * 1. when debugging is done on a single file where there is no folder open, + * 2. when the debug configuration is defined at the User level (global). + * 3. when the debug configuration is defined at the workspace level. + * If the config is coming from the "Run and Debug" playButton, there is one case where the folder is undefined: + * 1. when debugging is done on a single file where there is no folder open. + */ + + /** Do not resolve PreLaunchTask for these three cases, and let the Vs Code resolve it: + * 1: The existing configs that are found for a single file. + * 2: The existing configs that come from the playButton (the PreLaunchTask should already be defined for these configs). + * 3: The existing configs that come from the debugPanel where the folder is undefined and the PreLaunchTask cannot be found. + */ + + if (config.preLaunchTask) { + config.configSource = this.getDebugConfigSource(config, folder); + const isIntelliSenseDisabled: boolean = new CppSettings((vscode.workspace.workspaceFolders && vscode.workspace.workspaceFolders.length > 0) ? vscode.workspace.workspaceFolders[0]?.uri : undefined).intelliSenseEngine === "disabled"; + // Run the build task if IntelliSense is disabled. + if (isIntelliSenseDisabled) { + try { + await cppBuildTaskProvider.runBuildTask(config.preLaunchTask); + config.preLaunchTask = undefined; + Telemetry.logDebuggerEvent(DebuggerEvent.debugPanel, { "debugType": DebugType.debug, "configSource": config.configSource || ConfigSource.unknown, "configMode": ConfigMode.launchConfig, "cancelled": "false", "succeeded": "true" }); + } catch (err) { + Telemetry.logDebuggerEvent(DebuggerEvent.debugPanel, { "debugType": DebugType.debug, "configSource": config.configSource || ConfigSource.unknown, "configMode": ConfigMode.launchConfig, "cancelled": "false", "succeeded": "false" }); + } + return config; + } + let resolveByVsCode: boolean = false; + const isDebugPanel: boolean = !config.debuggerEvent || (config.debuggerEvent && config.debuggerEvent === DebuggerEvent.debugPanel); + const singleFile: boolean = config.configSource === ConfigSource.singleFile; + const isExistingConfig: boolean = this.isExistingConfig(config, folder); + const isExistingTask: boolean = await this.isExistingTask(config, folder); + if (singleFile) { + if (isExistingConfig) { + resolveByVsCode = true; + } + } else { + if (!isDebugPanel && (isExistingConfig || isExistingTask)) { + resolveByVsCode = true; + } else if (isDebugPanel && !folder && isExistingConfig && !isExistingTask) { + resolveByVsCode = true; + } + } + + // Send the telemetry before writing into files + config.debugType = config.debugType ? config.debugType : DebugType.debug; + const configMode: ConfigMode = isExistingConfig ? ConfigMode.launchConfig : ConfigMode.noLaunchConfig; + // if configuration.debuggerEvent === undefined, it means this configuration is already defined in launch.json and is shown in debugPanel. + Telemetry.logDebuggerEvent(config.debuggerEvent || DebuggerEvent.debugPanel, { "debugType": config.debugType || DebugType.debug, "configSource": config.configSource || ConfigSource.unknown, "configMode": configMode, "cancelled": "false", "succeeded": "true" }); + + if (!resolveByVsCode) { + if (singleFile || (isDebugPanel && !folder && isExistingTask)) { + await this.resolvePreLaunchTask(config, configMode); + config.preLaunchTask = undefined; + } else { + await this.resolvePreLaunchTask(config, configMode, folder); + DebugConfigurationProvider.recentBuildTaskLabelStr = config.preLaunchTask; + } + } else { + DebugConfigurationProvider.recentBuildTaskLabelStr = config.preLaunchTask; + } + } + + // resolveDebugConfigurationWithSubstitutedVariables will be automatically called after this return. + return config; + } + + /** + * This hook is directly called after 'resolveDebugConfiguration' but with all variables substituted. + * This is also ran after the tasks.json has completed. + * + * Try to add all missing attributes to the debug configuration being launched. + * If return "undefined", the debugging will be aborted silently. + * If return "null", the debugging will be aborted and launch.json will be opened. + */ + async resolveDebugConfigurationWithSubstitutedVariables(folder: vscode.WorkspaceFolder | undefined, config: CppDebugConfiguration, token?: vscode.CancellationToken): Promise { + if (!config || !config.type) { + return undefined; // Abort debugging silently. + } + + if (config.type === DebuggerType.cppvsdbg) { + // Fail if cppvsdbg type is running on non-Windows + if (os.platform() !== 'win32') { + void getOutputChannelLogger().showWarningMessage(localize("debugger.not.available", "Debugger of type: '{0}' is only available on Windows. Use type: '{1}' on the current OS platform.", "cppvsdbg", "cppdbg")); + return undefined; // Abort debugging silently. + } + + // Handle legacy 'externalConsole' bool and convert to console: "externalTerminal" + // eslint-disable-next-line no-prototype-builtins + if (config.hasOwnProperty("externalConsole")) { + void getOutputChannelLogger().showWarningMessage(localize("debugger.deprecated.config", "The key '{0}' is deprecated. Please use '{1}' instead.", "externalConsole", "console")); + if (config.externalConsole && !config.console) { + config.console = "externalTerminal"; + } + delete config.externalConsole; + } + + // Disable debug heap by default, enable if 'enableDebugHeap' is set. + if (!config.enableDebugHeap) { + const disableDebugHeapEnvSetting: Environment = { "name": "_NO_DEBUG_HEAP", "value": "1" }; + + if (config.environment && util.isArray(config.environment)) { + config.environment.push(disableDebugHeapEnvSetting); + } else { + config.environment = [disableDebugHeapEnvSetting]; + } + } + } + + // Add environment variables from .env file + this.resolveEnvFile(config, folder); + + await this.expand(config, folder); + + this.resolveSourceFileMapVariables(config); + + // Modify WSL config for OpenDebugAD7 + if (os.platform() === 'win32' && + config.pipeTransport && + config.pipeTransport.pipeProgram) { + let replacedPipeProgram: string | undefined; + const pipeProgramStr: string = config.pipeTransport.pipeProgram.toLowerCase().trim(); + + // OpenDebugAD7 is a 32-bit process. Make sure the WSL pipe transport is using the correct program. + replacedPipeProgram = debugUtils.ArchitectureReplacer.checkAndReplaceWSLPipeProgram(pipeProgramStr, debugUtils.ArchType.ia32); + + // If pipeProgram does not get replaced and there is a pipeCwd, concatenate with pipeProgramStr and attempt to replace. + if (!replacedPipeProgram && !path.isAbsolute(pipeProgramStr) && config.pipeTransport.pipeCwd) { + const pipeCwdStr: string = config.pipeTransport.pipeCwd.toLowerCase().trim(); + const newPipeProgramStr: string = path.join(pipeCwdStr, pipeProgramStr); + + replacedPipeProgram = debugUtils.ArchitectureReplacer.checkAndReplaceWSLPipeProgram(newPipeProgramStr, debugUtils.ArchType.ia32); + } + + if (replacedPipeProgram) { + config.pipeTransport.pipeProgram = replacedPipeProgram; + } + } + + const macOSMIMode: string = config.osx?.MIMode ?? config.MIMode; + const macOSMIDebuggerPath: string = config.osx?.miDebuggerPath ?? config.miDebuggerPath; + + const lldb_mi_10_x_path: string = path.join(util.extensionPath, "debugAdapters", "lldb-mi", "bin", "lldb-mi"); + + // Validate LLDB-MI + if (os.platform() === 'darwin' && // Check for macOS + fs.existsSync(lldb_mi_10_x_path) && // lldb-mi 10.x exists + (!macOSMIMode || macOSMIMode === 'lldb') && + !macOSMIDebuggerPath // User did not provide custom lldb-mi + ) { + const frameworkPath: string | undefined = this.getLLDBFrameworkPath(); + + if (!frameworkPath) { + const moreInfoButton: string = localize("lldb.framework.install.xcode", "More Info"); + const LLDBFrameworkMissingMessage: string = localize("lldb.framework.not.found", "Unable to locate 'LLDB.framework' for lldb-mi. Please install XCode or XCode Command Line Tools."); + + void vscode.window.showErrorMessage(LLDBFrameworkMissingMessage, moreInfoButton) + .then(value => { + if (value === moreInfoButton) { + const helpURL: string = "https://aka.ms/vscode-cpptools/LLDBFrameworkNotFound"; + void vscode.env.openExternal(vscode.Uri.parse(helpURL)); + } + }); + + return undefined; + } + } + + if (config.logging?.engineLogging) { + const outputChannel: Logger = getOutputChannelLogger(); + outputChannel.appendLine(localize("debugger.launchConfig", "Launch configuration:")); + outputChannel.appendLine(JSON.stringify(config, undefined, 2)); + // TODO: Enable when https://github.com/microsoft/vscode/issues/108619 is resolved. + // showOutputChannel(); + } + + // Run deploy steps + if (config.deploySteps && config.deploySteps.length !== 0) { + const codeVersion: number[] = util.getVsCodeVersion(); + if ((util.isNumber(codeVersion[0]) && codeVersion[0] < 1) || (util.isNumber(codeVersion[0]) && codeVersion[0] === 1 && util.isNumber(codeVersion[1]) && codeVersion[1] < 69)) { + void getOutputChannelLogger().showErrorMessage(localize("vs.code.1.69+.required", "'deploySteps' require VS Code 1.69+.")); + return undefined; + } + + const deploySucceeded: boolean = await vscode.window.withProgress({ + location: vscode.ProgressLocation.Notification, + title: localize("running.deploy.steps", "Running deploy steps...") + }, async () => this.deploySteps(config, token)); + + if (!deploySucceeded || token?.isCancellationRequested) { + return undefined; + } + } + + // Pick process if process id is empty + if (config.request === "attach" && !config.processId) { + let processId: string | undefined; + if (config.pipeTransport || config.useExtendedRemote) { + const remoteAttachPicker: RemoteAttachPicker = new RemoteAttachPicker(); + processId = await remoteAttachPicker.ShowAttachEntries(config); + } else { + const attachItemsProvider: AttachItemsProvider = NativeAttachItemsProviderFactory.Get(); + const attacher: AttachPicker = new AttachPicker(attachItemsProvider); + processId = await attacher.ShowAttachEntries(token); + } + + if (processId) { + config.processId = processId; + } else { + void getOutputChannelLogger().showErrorMessage("No process was selected."); + return undefined; + } + } + + return config; + } + + async provideDebugConfigurationsForType(type: DebuggerType, folder?: vscode.WorkspaceFolder, _token?: vscode.CancellationToken): Promise { + const defaultTemplateConfig: CppDebugConfiguration = this.assetProvider.getInitialConfigurations(type).find((config: any) => + isDebugLaunchStr(config.name) && config.request === "launch"); + console.assert(defaultTemplateConfig, "Could not find default debug configuration."); + + const platformInfo: PlatformInformation = await PlatformInformation.GetPlatformInformation(); + + // Import the existing configured tasks from tasks.json file. + const configuredBuildTasks: CppBuildTask[] = await cppBuildTaskProvider.getJsonTasks(); + + let buildTasks: CppBuildTask[] = []; + await this.loadDetectedTasks(); + // Remove the tasks that are already configured once in tasks.json. + const dedupDetectedBuildTasks: CppBuildTask[] = DebugConfigurationProvider.detectedBuildTasks.filter(taskDetected => + !configuredBuildTasks.some(taskJson => taskJson.definition.label === taskDetected.definition.label)); + buildTasks = buildTasks.concat(configuredBuildTasks, dedupDetectedBuildTasks); + + // Filter out build tasks that don't match the currently selected debug configuration type. + if (buildTasks.length !== 0) { + buildTasks = buildTasks.filter((task: CppBuildTask) => { + const command: string = task.definition.command as string; + if (!command) { + return false; + } + if (defaultTemplateConfig.name.startsWith("(Windows) ")) { + if (command.startsWith("cl.exe")) { + return true; + } + } else { + if (!command.startsWith("cl.exe")) { + return true; + } + } + return false; + }); + } + + // Generate new configurations for each build task. + // Generating a task is async, therefore we must *await* *all* map(task => config) Promises to resolve. + let configs: CppDebugConfiguration[] = []; + if (buildTasks.length !== 0) { + configs = (await Promise.all(buildTasks.map>(async task => { + const definition: CppBuildTaskDefinition = task.definition as CppBuildTaskDefinition; + const compilerPath: string = util.isString(definition.command) ? definition.command : definition.command.value; + // Filter out the tasks that has an invalid compiler path. + const compilerPathExists: boolean = path.isAbsolute(compilerPath) ? + // Absolute path, just check if it exists + await util.checkFileExists(compilerPath) : + // Non-absolute. Check on $PATH + (await util.whichAsync(compilerPath) !== undefined); + if (!compilerPathExists) { + getOutputChannelLogger().appendLine(localize('compiler.path.not.exists', "Unable to find {0}. {1} task is ignored.", compilerPath, definition.label)); + } + const compilerName: string = path.basename(compilerPath); + const newConfig: CppDebugConfiguration = { ...defaultTemplateConfig }; // Copy enumerables and properties + newConfig.existing = false; + + newConfig.name = configPrefix + compilerName + " " + this.buildAndDebugActiveFileStr(); + newConfig.preLaunchTask = task.name; + if (newConfig.type === DebuggerType.cppdbg) { + newConfig.externalConsole = false; + } else { + newConfig.console = "integratedTerminal"; + } + // Extract the .exe path from the defined task. + const definedExePath: string | undefined = util.findExePathInArgs(task.definition.args); + newConfig.program = definedExePath ? definedExePath : util.defaultExePath(); + // Add the "detail" property to show the compiler path in QuickPickItem. + // This property will be removed before writing the DebugConfiguration in launch.json. + newConfig.detail = localize("pre.Launch.Task", "preLaunchTask: {0}", task.name); + newConfig.taskDetail = task.detail; + newConfig.taskStatus = task.existing ? + (task.name === DebugConfigurationProvider.recentBuildTaskLabelStr) ? TaskStatus.recentlyUsed : TaskStatus.configured : + TaskStatus.detected; + if (task.isDefault) { + newConfig.isDefault = true; + } + const isCl: boolean = compilerName === "cl.exe"; + newConfig.cwd = isWindows && !isCl && !process.env.PATH?.includes(path.dirname(compilerPath)) ? path.dirname(compilerPath) : "${fileDirname}"; + + if (platformInfo.platform !== "darwin") { + let debuggerName: string; + if (compilerName.startsWith("clang")) { + newConfig.MIMode = "lldb"; + if (isWindows) { + debuggerName = "lldb"; + } else { + debuggerName = "lldb-mi"; + // Search for clang-8, clang-10, etc. + if ((compilerName !== "clang-cl.exe") && (compilerName !== "clang-cpp.exe")) { + const suffixIndex: number = compilerName.indexOf("-"); + if (suffixIndex !== -1) { + const suffix: string = compilerName.substring(suffixIndex); + debuggerName += suffix; + } + } + } + newConfig.type = DebuggerType.cppdbg; + } else if (compilerName === "cl.exe") { + newConfig.miDebuggerPath = undefined; + newConfig.type = DebuggerType.cppvsdbg; + return newConfig; + } else { + debuggerName = "gdb"; + } + if (isWindows) { + debuggerName = debuggerName.endsWith(".exe") ? debuggerName : (debuggerName + ".exe"); + } + const compilerDirname: string = path.dirname(compilerPath); + const debuggerPath: string = path.join(compilerDirname, debuggerName); + + // Check if debuggerPath exists. + if (await util.checkFileExists(debuggerPath)) { + newConfig.miDebuggerPath = debuggerPath; + } else if (await util.whichAsync(debuggerName) !== undefined) { + // Check if debuggerName exists on $PATH + newConfig.miDebuggerPath = debuggerName; + } else { + // Try the usr path for non-Windows platforms. + const usrDebuggerPath: string = path.join("/usr", "bin", debuggerName); + if (!isWindows && await util.checkFileExists(usrDebuggerPath)) { + newConfig.miDebuggerPath = usrDebuggerPath; + } else { + getOutputChannelLogger().appendLine(localize('debugger.path.not.exists', "Unable to find the {0} debugger. The debug configuration for {1} is ignored.", `\"${debuggerName}\"`, compilerName)); + return undefined; + } + } + } + return newConfig; + }))).filter((item): item is CppDebugConfiguration => !!item); + } + configs.push(defaultTemplateConfig); + const existingConfigs: CppDebugConfiguration[] | undefined = this.getLaunchConfigs(folder, type)?.map(config => { + if (!config.detail && config.preLaunchTask) { + config.detail = localize("pre.Launch.Task", "preLaunchTask: {0}", config.preLaunchTask); + } + config.existing = true; + return config; + }); + if (existingConfigs) { + // Remove the detected configs that are already configured once in launch.json. + const dedupExistingConfigs: CppDebugConfiguration[] = configs.filter(detectedConfig => !existingConfigs.some(config => { + if (config.preLaunchTask === detectedConfig.preLaunchTask && config.type === detectedConfig.type && config.request === detectedConfig.request) { + // Carry the default task information. + config.isDefault = detectedConfig.isDefault ? detectedConfig.isDefault : undefined; + return true; + } + return false; + })); + configs = existingConfigs.concat(dedupExistingConfigs); + } + return configs; + } + + private async loadDetectedTasks(): Promise { + const editor: vscode.TextEditor | undefined = vscode.window.activeTextEditor; + const emptyTasks: CppBuildTask[] = []; + if (!editor) { + DebugConfigurationProvider.detectedBuildTasks = emptyTasks; + return; + } + + const fileExt: string = path.extname(editor.document.fileName); + if (!fileExt) { + DebugConfigurationProvider.detectedBuildTasks = emptyTasks; + return; + } + + // Don't offer tasks for header files. + const isHeader: boolean = util.isHeaderFile(editor.document.uri); + if (isHeader) { + DebugConfigurationProvider.detectedBuildTasks = emptyTasks; + return; + } + + // Don't offer tasks if the active file's extension is not a recognized C/C++ extension. + const fileIsCpp: boolean = util.isCppFile(editor.document.uri); + const fileIsC: boolean = util.isCFile(editor.document.uri); + if (!(fileIsCpp || fileIsC)) { + DebugConfigurationProvider.detectedBuildTasks = emptyTasks; + return; + } + + if (fileIsCpp) { + if (!DebugConfigurationProvider.detectedCppBuildTasks || DebugConfigurationProvider.detectedCppBuildTasks.length === 0) { + DebugConfigurationProvider.detectedCppBuildTasks = await cppBuildTaskProvider.getTasks(true); + } + DebugConfigurationProvider.detectedBuildTasks = DebugConfigurationProvider.detectedCppBuildTasks; + } else { + if (!DebugConfigurationProvider.detectedCBuildTasks || DebugConfigurationProvider.detectedCBuildTasks.length === 0) { + DebugConfigurationProvider.detectedCBuildTasks = await cppBuildTaskProvider.getTasks(true); + } + DebugConfigurationProvider.detectedBuildTasks = DebugConfigurationProvider.detectedCBuildTasks; + } + } + + public static get recentBuildTaskLabelStr(): string { + return DebugConfigurationProvider.recentBuildTaskLabel; + } + + public static set recentBuildTaskLabelStr(recentTask: string) { + DebugConfigurationProvider.recentBuildTaskLabel = recentTask; + } + + private buildAndDebugActiveFileStr(): string { + return `${localize("build.and.debug.active.file", 'build and debug active file')}`; + } + + private isClConfiguration(configurationLabel: string): boolean { + return configurationLabel.startsWith("C/C++: cl.exe"); + } + + private showErrorIfClNotAvailable(_configurationLabel: string): boolean { + if (!process.env.DevEnvDir || process.env.DevEnvDir.length === 0) { + void vscode.window.showErrorMessage(localize("cl.exe.not.available", "{0} build and debug is only usable when VS Code is run from the Developer Command Prompt for VS.", "cl.exe")); + return true; + } + return false; + } + + private getLLDBFrameworkPath(): string | undefined { + const LLDBFramework: string = "LLDB.framework"; + // Note: When adding more search paths, make sure the shipped lldb-mi also has it. See Build/lldb-mi.yml and 'install_name_tool' commands. + const searchPaths: string[] = [ + "/Library/Developer/CommandLineTools/Library/PrivateFrameworks", // XCode CLI + "/Applications/Xcode.app/Contents/SharedFrameworks" // App Store XCode + ]; + + for (const searchPath of searchPaths) { + if (fs.existsSync(path.join(searchPath, LLDBFramework))) { + // Found a framework that 'lldb-mi' can use. + return searchPath; + } + } + + const outputChannel: Logger = getOutputChannelLogger(); + + outputChannel.appendLine(localize("lldb.find.failed", "Missing dependency '{0}' for lldb-mi executable.", LLDBFramework)); + outputChannel.appendLine(localize("lldb.search.paths", "Searched in:")); + searchPaths.forEach(searchPath => { + outputChannel.appendLine(`\t${searchPath}`); + }); + const xcodeCLIInstallCmd: string = "xcode-select --install"; + outputChannel.appendLine(localize("lldb.install.help", "To resolve this issue, either install XCode through the Apple App Store or install the XCode Command Line Tools by running '{0}' in a Terminal window.", xcodeCLIInstallCmd)); + showOutputChannel(); + + return undefined; + } + + private resolveEnvFile(config: CppDebugConfiguration, folder?: vscode.WorkspaceFolder): void { + if (config.envFile) { + // replace ${env:???} variables + let envFilePath: string = util.resolveVariables(config.envFile, undefined); + + try { + if (folder && folder.uri && folder.uri.fsPath) { + // Try to replace ${workspaceFolder} or ${workspaceRoot} + envFilePath = envFilePath.replace(/(\${workspaceFolder}|\${workspaceRoot})/g, folder.uri.fsPath); + } + + const parsedFile: ParsedEnvironmentFile = ParsedEnvironmentFile.CreateFromFile(envFilePath, config["environment"]); + + // show error message if single lines cannot get parsed + if (parsedFile.Warning) { + void DebugConfigurationProvider.showFileWarningAsync(parsedFile.Warning, config.envFile); + } + + config.environment = parsedFile.Env; + + delete config.envFile; + } catch (errJS) { + const e: Error = errJS as Error; + throw new Error(localize("envfile.failed", "Failed to use {0}. Reason: {1}", "envFile", e.message)); + } + } + } + + private resolveSourceFileMapVariables(config: CppDebugConfiguration): void { + const messages: string[] = []; + if (config.sourceFileMap) { + for (const sourceFileMapSource of Object.keys(config.sourceFileMap)) { + let message: string = ""; + const sourceFileMapTarget: string = config.sourceFileMap[sourceFileMapSource]; + + let source: string = sourceFileMapSource; + let target: string | object = sourceFileMapTarget; + + // TODO: pass config.environment as 'additionalEnvironment' to resolveVariables when it is { key: value } instead of { "key": key, "value": value } + const newSourceFileMapSource: string = util.resolveVariables(sourceFileMapSource, undefined); + if (sourceFileMapSource !== newSourceFileMapSource) { + message = "\t" + localize("replacing.sourcepath", "Replacing {0} '{1}' with '{2}'.", "sourcePath", sourceFileMapSource, newSourceFileMapSource); + // eslint-disable-next-line @typescript-eslint/no-dynamic-delete + delete config.sourceFileMap[sourceFileMapSource]; + source = newSourceFileMapSource; + } + + if (util.isString(sourceFileMapTarget)) { + const newSourceFileMapTarget: string = util.resolveVariables(sourceFileMapTarget, undefined); + if (sourceFileMapTarget !== newSourceFileMapTarget) { + // Add a space if source was changed, else just tab the target message. + message += message ? ' ' : '\t'; + message += localize("replacing.targetpath", "Replacing {0} '{1}' with '{2}'.", "targetPath", sourceFileMapTarget, newSourceFileMapTarget); + target = newSourceFileMapTarget; + } + } else if (util.isObject(sourceFileMapTarget)) { + const newSourceFileMapTarget: { "editorPath": string; "useForBreakpoints": boolean } = sourceFileMapTarget; + newSourceFileMapTarget["editorPath"] = util.resolveVariables(sourceFileMapTarget["editorPath"], undefined); + + if (sourceFileMapTarget !== newSourceFileMapTarget) { + // Add a space if source was changed, else just tab the target message. + message += message ? ' ' : '\t'; + message += localize("replacing.editorPath", "Replacing {0} '{1}' with '{2}'.", "editorPath", sourceFileMapTarget, newSourceFileMapTarget["editorPath"]); + target = newSourceFileMapTarget; + } + } + + if (message) { + config.sourceFileMap[source] = target; + messages.push(message); + } + } + + if (messages.length > 0) { + getOutputChannel().appendLine(localize("resolving.variables.in.sourcefilemap", "Resolving variables in {0}...", "sourceFileMap")); + messages.forEach((message) => { + getOutputChannel().appendLine(message); + }); + showOutputChannel(); + } + } + } + + private static async showFileWarningAsync(message: string, fileName: string): Promise { + const openItem: vscode.MessageItem = { title: localize("open.envfile", "Open {0}", "envFile") }; + const result: vscode.MessageItem | undefined = await vscode.window.showWarningMessage(message, openItem); + if (result && result.title === openItem.title) { + const doc: vscode.TextDocument = await vscode.workspace.openTextDocument(fileName); + if (doc) { + void vscode.window.showTextDocument(doc); + } + } + } + + private localizeConfigDetail(items: ConfigMenu[]): ConfigMenu[] { + items.map((item: ConfigMenu) => { + switch (item.detail) { + case TaskStatus.recentlyUsed: { + item.detail = localize("recently.used.task", "Recently Used Task"); + break; + } + case TaskStatus.configured: { + item.detail = localize("configured.task", "Configured Task"); + break; + } + case TaskStatus.detected: { + item.detail = localize("detected.task", "Detected Task"); + break; + } + default: { + break; + } + } + if (item.configuration.taskDetail) { + // Add the compiler path of the preLaunchTask to the description of the debug configuration. + item.detail = (item.detail ?? "") + " (" + item.configuration.taskDetail + ")"; + } + }); + return items; + } + + private findDefaultConfig(configs: CppDebugConfiguration[]): CppDebugConfiguration[] { + // eslint-disable-next-line no-prototype-builtins + return configs.filter((config: CppDebugConfiguration) => config.hasOwnProperty("isDefault") && config.isDefault); + } + + private async isExistingTask(config: CppDebugConfiguration, folder?: vscode.WorkspaceFolder): Promise { + if (config.taskStatus && (config.taskStatus !== TaskStatus.detected)) { + return true; + } else if (config.taskStatus && (config.taskStatus === TaskStatus.detected)) { + return false; + } + return cppBuildTaskProvider.isExistingTask(config.preLaunchTask, folder); + } + + private isExistingConfig(config: CppDebugConfiguration, folder?: vscode.WorkspaceFolder): boolean { + if (config.existing) { + return config.existing; + } + const configs: CppDebugConfiguration[] | undefined = this.getLaunchConfigs(folder, config.type); + if (configs && configs.length > 0) { + const selectedConfig: any | undefined = configs.find((item: any) => item.name && item.name === config.name); + if (selectedConfig) { + return true; + } + } + return false; + } + + private getDebugConfigSource(config: CppDebugConfiguration, folder?: vscode.WorkspaceFolder): ConfigSource | undefined { + if (config.configSource) { + return config.configSource; + } + const isExistingConfig: boolean = this.isExistingConfig(config, folder); + if (!isExistingConfig && !folder) { + return ConfigSource.singleFile; + } else if (!isExistingConfig) { + return ConfigSource.workspaceFolder; + } + + const configs: CppDebugConfiguration[] | undefined = this.getLaunchConfigs(folder, config.type); + const matchingConfig: CppDebugConfiguration | undefined = configs?.find((item: any) => item.name && item.name === config.name); + if (matchingConfig?.configSource) { + return matchingConfig.configSource; + } + return ConfigSource.unknown; + } + + public getLaunchConfigs(folder?: vscode.WorkspaceFolder, type?: DebuggerType | string): CppDebugConfiguration[] | undefined { + // Get existing debug configurations from launch.json or user/workspace "launch" settings. + const WorkspaceConfigs: vscode.WorkspaceConfiguration = vscode.workspace.getConfiguration('launch', folder); + const configs: any = WorkspaceConfigs.inspect('configurations'); + if (!configs) { + return undefined; + } + let detailedConfigs: CppDebugConfiguration[] = []; + if (configs.workspaceFolderValue !== undefined) { + detailedConfigs = detailedConfigs.concat(configs.workspaceFolderValue.map((item: CppDebugConfiguration) => { + item.configSource = ConfigSource.workspaceFolder; + return item; + })); + } + if (configs.workspaceValue !== undefined) { + detailedConfigs = detailedConfigs.concat(configs.workspaceValue.map((item: CppDebugConfiguration) => { + item.configSource = ConfigSource.workspace; + return item; + })); + } + if (configs.globalValue !== undefined) { + detailedConfigs = detailedConfigs.concat(configs.globalValue.map((item: CppDebugConfiguration) => { + item.configSource = ConfigSource.global; + return item; + })); + } + detailedConfigs = detailedConfigs.filter((config: any) => config.name && config.request === "launch" && type ? (config.type === type) : true); + return detailedConfigs; + } + + private getLaunchJsonPath(): string | undefined { + return util.getJsonPath("launch.json"); + } + + private getRawLaunchJson(): Promise { + const path: string | undefined = this.getLaunchJsonPath(); + return util.getRawJson(path); + } + + public async writeDebugConfig(config: vscode.DebugConfiguration, isExistingConfig: boolean, _folder?: vscode.WorkspaceFolder): Promise { + const launchJsonPath: string | undefined = this.getLaunchJsonPath(); + + if (isExistingConfig) { + if (launchJsonPath) { + const doc: vscode.TextDocument = await vscode.workspace.openTextDocument(launchJsonPath); + if (doc) { + void vscode.window.showTextDocument(doc); + } + } + return; + } + const rawLaunchJson: any = await this.getRawLaunchJson(); + if (!rawLaunchJson.configurations) { + rawLaunchJson.configurations = []; + } + if (!rawLaunchJson.version) { + rawLaunchJson.version = "2.0.0"; + } + + // Remove the extra properties that are not a part of the vsCode.DebugConfiguration. + config.detail = undefined; + config.taskStatus = undefined; + config.isDefault = undefined; + config.source = undefined; + config.debuggerEvent = undefined; + config.debugType = undefined; + config.existing = undefined; + config.taskDetail = undefined; + rawLaunchJson.configurations.push(config); + + if (!launchJsonPath) { + throw new Error("Failed to get tasksJsonPath in checkBuildTaskExists()"); + } + + const settings: OtherSettings = new OtherSettings(); + await util.writeFileText(launchJsonPath, jsonc.stringify(rawLaunchJson, null, settings.editorTabSize)); + await vscode.workspace.openTextDocument(launchJsonPath); + const doc: vscode.TextDocument = await vscode.workspace.openTextDocument(launchJsonPath); + if (doc) { + void vscode.window.showTextDocument(doc); + } + } + + public async addDebugConfiguration(textEditor: vscode.TextEditor): Promise { + const folder: vscode.WorkspaceFolder | undefined = vscode.workspace.getWorkspaceFolder(textEditor.document.uri); + if (!folder) { + return; + } + const selectedConfig: vscode.DebugConfiguration | undefined = await this.selectConfiguration(textEditor, false, true); + if (!selectedConfig) { + Telemetry.logDebuggerEvent(DebuggerEvent.addConfigGear, { "configSource": ConfigSource.workspaceFolder, "configMode": ConfigMode.launchConfig, "cancelled": "true", "succeeded": "true" }); + return; // User canceled it. + } + + const isExistingConfig: boolean = this.isExistingConfig(selectedConfig, folder); + // Write preLaunchTask into tasks.json file. + if (!isExistingConfig && selectedConfig.preLaunchTask && (selectedConfig.taskStatus && selectedConfig.taskStatus === TaskStatus.detected)) { + await cppBuildTaskProvider.writeBuildTask(selectedConfig.preLaunchTask); + } + // Remove the extra properties that are not a part of the DebugConfiguration, as these properties will be written in launch.json. + selectedConfig.detail = undefined; + selectedConfig.taskStatus = undefined; + selectedConfig.isDefault = undefined; + selectedConfig.source = undefined; + selectedConfig.debuggerEvent = undefined; + // Write debug configuration in launch.json file. + await this.writeDebugConfig(selectedConfig, isExistingConfig, folder); + Telemetry.logDebuggerEvent(DebuggerEvent.addConfigGear, { "configSource": ConfigSource.workspaceFolder, "configMode": ConfigMode.launchConfig, "cancelled": "false", "succeeded": "true" }); + } + + public async buildAndRun(textEditor: vscode.TextEditor): Promise { + // Turn off the debug mode. + return this.buildAndDebug(textEditor, false); + } + + public async buildAndDebug(textEditor: vscode.TextEditor, debugModeOn: boolean = true): Promise { + let folder: vscode.WorkspaceFolder | undefined = vscode.workspace.getWorkspaceFolder(textEditor.document.uri); + const selectedConfig: CppDebugConfiguration | undefined = await this.selectConfiguration(textEditor); + if (!selectedConfig) { + Telemetry.logDebuggerEvent(DebuggerEvent.playButton, { "debugType": debugModeOn ? DebugType.debug : DebugType.run, "configSource": ConfigSource.unknown, "cancelled": "true", "succeeded": "true" }); + return; // User canceled it. + } + + // Keep track of the entry point where the debug has been selected, for telemetry purposes. + selectedConfig.debuggerEvent = DebuggerEvent.playButton; + // If the configs are coming from workspace or global settings and the task is not found in tasks.json, let that to be resolved by VsCode. + if (selectedConfig.preLaunchTask && selectedConfig.configSource && + (selectedConfig.configSource === ConfigSource.global || selectedConfig.configSource === ConfigSource.workspace) && + !await this.isExistingTask(selectedConfig)) { + folder = undefined; + } + selectedConfig.debugType = debugModeOn ? DebugType.debug : DebugType.run; + // startDebugging will trigger a call to resolveDebugConfiguration. + await vscode.debug.startDebugging(folder, selectedConfig, { noDebug: !debugModeOn }); + } + + private async selectConfiguration(textEditor: vscode.TextEditor, pickDefault: boolean = true, onlyWorkspaceFolder: boolean = false): Promise { + const folder: vscode.WorkspaceFolder | undefined = vscode.workspace.getWorkspaceFolder(textEditor.document.uri); + if (!util.isCppOrCFile(textEditor.document.uri)) { + void vscode.window.showErrorMessage(localize("cannot.build.non.cpp", 'Cannot build and debug because the active file is not a C or C++ source file.')); + return; + } + + // Get debug configurations for all debugger types. + let configs: CppDebugConfiguration[] = await this.provideDebugConfigurationsForType(DebuggerType.cppdbg, folder); + if (os.platform() === 'win32') { + configs = configs.concat(await this.provideDebugConfigurationsForType(DebuggerType.cppvsdbg, folder)); + } + if (onlyWorkspaceFolder) { + configs = configs.filter(item => !item.configSource || item.configSource === ConfigSource.workspaceFolder); + } + + const defaultConfig: CppDebugConfiguration[] | undefined = pickDefault ? this.findDefaultConfig(configs) : undefined; + + const items: ConfigMenu[] = configs.map(config => ({ label: config.name, configuration: config, description: config.detail, detail: config.taskStatus })); + + let selection: ConfigMenu | undefined; + + // if there was only one config for the default task, choose that config, otherwise ask the user to choose. + if (defaultConfig && defaultConfig.length === 1) { + selection = { label: defaultConfig[0].name, configuration: defaultConfig[0], description: defaultConfig[0].detail, detail: defaultConfig[0].taskStatus }; + } else { + let sortedItems: ConfigMenu[] = []; + // Find the recently used task and place it at the top of quickpick list. + const recentTask: ConfigMenu[] = items.filter(item => item.configuration.preLaunchTask && item.configuration.preLaunchTask === DebugConfigurationProvider.recentBuildTaskLabelStr); + if (recentTask.length !== 0 && recentTask[0].detail !== TaskStatus.detected) { + recentTask[0].detail = TaskStatus.recentlyUsed; + sortedItems.push(recentTask[0]); + } + sortedItems = sortedItems.concat(items.filter(item => item.detail === TaskStatus.configured)); + sortedItems = sortedItems.concat(items.filter(item => item.detail === TaskStatus.detected)); + sortedItems = sortedItems.concat(items.filter(item => item.detail === undefined)); + + selection = await vscode.window.showQuickPick(this.localizeConfigDetail(sortedItems), { + placeHolder: items.length === 0 ? localize("no.compiler.found", "No compiler found") : localize("select.debug.configuration", "Select a debug configuration") + }); + } + if (selection && this.isClConfiguration(selection.configuration.name) && this.showErrorIfClNotAvailable(selection.configuration.name)) { + return; + } + return selection?.configuration; + } + + private async resolvePreLaunchTask(config: CppDebugConfiguration, configMode: ConfigMode, folder?: vscode.WorkspaceFolder | undefined): Promise { + if (config.preLaunchTask) { + try { + if (config.configSource === ConfigSource.singleFile) { + // In case of singleFile, remove the preLaunch task from the debug configuration and run it here instead. + await cppBuildTaskProvider.runBuildTask(config.preLaunchTask); + } else { + await cppBuildTaskProvider.writeDefaultBuildTask(config.preLaunchTask, folder); + } + } catch (errJS) { + const e: Error = errJS as Error; + if (e && e.message === util.failedToParseJson) { + void vscode.window.showErrorMessage(util.failedToParseJson); + } + Telemetry.logDebuggerEvent(config.debuggerEvent || DebuggerEvent.debugPanel, { "debugType": config.debugType || DebugType.debug, "configSource": config.configSource || ConfigSource.unknown, "configMode": configMode, "cancelled": "false", "succeeded": "false" }); + } + } + } + + private async expand(config: vscode.DebugConfiguration, folder: vscode.WorkspaceFolder | undefined): Promise { + const folderPath: string | undefined = folder?.uri.fsPath || vscode.workspace.workspaceFolders?.[0].uri.fsPath; + const vars: ExpansionVars = config.variables ? config.variables : {}; + vars.workspaceFolder = folderPath || '{workspaceFolder}'; + vars.workspaceFolderBasename = folderPath ? path.basename(folderPath) : '{workspaceFolderBasename}'; + const expansionOptions: ExpansionOptions = { vars, recursive: true }; + return expandAllStrings(config, expansionOptions); + } + + // Returns true when ALL steps succeed; stop all subsequent steps if one fails + private async deploySteps(config: vscode.DebugConfiguration, cancellationToken?: vscode.CancellationToken): Promise { + let succeeded: boolean = true; + const deployStart: number = new Date().getTime(); + + for (const step of config.deploySteps) { + succeeded = await this.singleDeployStep(config, step, cancellationToken); + if (!succeeded) { + break; + } + } + + const deployEnd: number = new Date().getTime(); + + const telemetryProperties: { [key: string]: string } = { + Succeeded: `${succeeded}`, + IsDebugging: `${!config.noDebug || false}` + }; + const telemetryMetrics: { [key: string]: number } = { + NumSteps: config.deploySteps.length, + Duration: deployEnd - deployStart + }; + Telemetry.logDebuggerEvent('deploy', telemetryProperties, telemetryMetrics); + + return succeeded; + } + + private async singleDeployStep(config: vscode.DebugConfiguration, step: any, cancellationToken?: vscode.CancellationToken): Promise { + if ((config.noDebug && step.debug === true) || (!config.noDebug && step.debug === false)) { + // Skip steps that doesn't match current launch mode. Explicit true/false check, since a step is always run when debug is undefined. + return true; + } + const stepType: StepType = step.type; + switch (stepType) { + case StepType.command: { + // VS Code commands are the same regardless of which extension invokes them, so just invoke them here. + if (step.args && !Array.isArray(step.args)) { + void getOutputChannelLogger().showErrorMessage(localize('command.args.must.be.array', '"args" in command deploy step must be an array.')); + return false; + } + const returnCode: unknown = await vscode.commands.executeCommand(step.command, ...step.args); + return !returnCode; + } + case StepType.scp: + case StepType.rsync: { + const isScp: boolean = stepType === StepType.scp; + if (!step.files || !step.targetDir || !step.host) { + void getOutputChannelLogger().showErrorMessage(localize('missing.properties.copyFile', '"host", "files", and "targetDir" are required in {0} steps.', isScp ? 'SCP' : 'rsync')); + return false; + } + const host: util.ISshHostInfo = util.isString(step.host) ? { hostName: step.host } : { hostName: step.host.hostName, user: step.host.user, port: step.host.port }; + const jumpHosts: util.ISshHostInfo[] = step.host.jumpHosts; + let files: vscode.Uri[] = []; + if (util.isString(step.files)) { + files = files.concat((await globAsync(step.files)).map(file => vscode.Uri.file(file))); + } else if (util.isArrayOfString(step.files)) { + for (const fileGlob of (step.files as string[])) { + files = files.concat((await globAsync(fileGlob)).map(file => vscode.Uri.file(file))); + } + } else { + void getOutputChannelLogger().showErrorMessage(localize('incorrect.files.type.copyFile', '"files" must be a string or an array of strings in {0} steps.', isScp ? 'SCP' : 'rsync')); + return false; + } + + let scpResult: util.ProcessReturnType; + if (isScp) { + scpResult = await scp(files, host, step.targetDir, config.scpPath, config.recursive, jumpHosts, cancellationToken); + } else { + scpResult = await rsync(files, host, step.targetDir, config.scpPath, config.recursive, jumpHosts, cancellationToken); + } + + if (!scpResult.succeeded || cancellationToken?.isCancellationRequested) { + return false; + } + break; + } + case StepType.ssh: { + if (!step.host || !step.command) { + void getOutputChannelLogger().showErrorMessage(localize('missing.properties.ssh', '"host" and "command" are required for ssh steps.')); + return false; + } + const host: util.ISshHostInfo = util.isString(step.host) ? { hostName: step.host } : { hostName: step.host.hostName, user: step.host.user, port: step.host.port }; + const jumpHosts: util.ISshHostInfo[] = step.host.jumpHosts; + const localForwards: util.ISshLocalForwardInfo[] = step.host.localForwards; + const continueOn: string = step.continueOn; + const sshResult: util.ProcessReturnType = await ssh(host, step.command, config.sshPath, jumpHosts, localForwards, continueOn, cancellationToken); + if (!sshResult.succeeded || cancellationToken?.isCancellationRequested) { + return false; + } + break; + } + case StepType.shell: { + if (!step.command) { + void getOutputChannelLogger().showErrorMessage(localize('missing.properties.shell', '"command" is required for shell steps.')); + return false; + } + const taskResult: util.ProcessReturnType = await util.spawnChildProcess(step.command, undefined, step.continueOn); + if (!taskResult.succeeded || cancellationToken?.isCancellationRequested) { + void getOutputChannelLogger().showErrorMessage(taskResult.output); + return false; + } + break; + } + default: { + getOutputChannelLogger().appendLine(localize('deploy.step.type.not.supported', 'Deploy step type {0} is not supported.', step.type)); + return false; + } + } + return true; + } +} + +export interface IConfigurationAssetProvider { + getInitialConfigurations(debuggerType: DebuggerType): any; + getConfigurationSnippets(): vscode.CompletionItem[]; +} + +export class ConfigurationAssetProviderFactory { + public static getConfigurationProvider(): IConfigurationAssetProvider { + switch (os.platform()) { + case 'win32': + return new WindowsConfigurationProvider(); + case 'darwin': + return new OSXConfigurationProvider(); + case 'linux': + return new LinuxConfigurationProvider(); + default: + throw new Error(localize("unexpected.os", "Unexpected OS type")); + } + } +} + +abstract class DefaultConfigurationProvider implements IConfigurationAssetProvider { + configurations: IConfiguration[] = []; + + public getInitialConfigurations(debuggerType: DebuggerType): any { + const configurationSnippet: IConfigurationSnippet[] = []; + + // Only launch configurations are initial configurations + this.configurations.forEach(configuration => { + configurationSnippet.push(configuration.GetLaunchConfiguration()); + }); + + const initialConfigurations: any = configurationSnippet.filter(snippet => snippet.debuggerType === debuggerType && snippet.isInitialConfiguration) + .map(snippet => JSON.parse(snippet.bodyText)); + + // If configurations is empty, then it will only have an empty configurations array in launch.json. Users can still add snippets. + return initialConfigurations; + } + + public getConfigurationSnippets(): vscode.CompletionItem[] { + const completionItems: vscode.CompletionItem[] = []; + + this.configurations.forEach(configuration => { + completionItems.push(convertConfigurationSnippetToCompletionItem(configuration.GetLaunchConfiguration())); + completionItems.push(convertConfigurationSnippetToCompletionItem(configuration.GetAttachConfiguration())); + }); + + return completionItems; + } +} + +class WindowsConfigurationProvider extends DefaultConfigurationProvider { + private executable: string = "a.exe"; + private pipeProgram: string = "<" + localize("path.to.pipe.program", "full path to pipe program such as {0}", "plink.exe").replace(/"/g, '') + ">"; + private MIMode: string = 'gdb'; + private setupCommandsBlock: string = `"setupCommands": [ + { + "description": "${localize("enable.pretty.printing", "Enable pretty-printing for {0}", "gdb").replace(/"/g, '')}", + "text": "-enable-pretty-printing", + "ignoreFailures": true + }, + { + "description": "${localize("enable.intel.disassembly.flavor", "Set Disassembly Flavor to {0}", "Intel").replace(/"/g, '')}", + "text": "-gdb-set disassembly-flavor intel", + "ignoreFailures": true + } +]`; + + constructor() { + super(); + this.configurations = [ + new MIConfigurations(this.MIMode, this.executable, this.pipeProgram, this.setupCommandsBlock), + new PipeTransportConfigurations(this.MIMode, this.executable, this.pipeProgram, this.setupCommandsBlock), + new WindowsConfigurations(this.MIMode, this.executable, this.pipeProgram, this.setupCommandsBlock), + new WSLConfigurations(this.MIMode, this.executable, this.pipeProgram, this.setupCommandsBlock) + ]; + } +} + +class OSXConfigurationProvider extends DefaultConfigurationProvider { + private MIMode: string = 'lldb'; + private executable: string = "a.out"; + private pipeProgram: string = "/usr/bin/ssh"; + + constructor() { + super(); + this.configurations = [ + new MIConfigurations(this.MIMode, this.executable, this.pipeProgram) + ]; + } +} + +class LinuxConfigurationProvider extends DefaultConfigurationProvider { + private MIMode: string = 'gdb'; + private setupCommandsBlock: string = `"setupCommands": [ + { + "description": "${localize("enable.pretty.printing", "Enable pretty-printing for {0}", "gdb").replace(/"/g, '')}", + "text": "-enable-pretty-printing", + "ignoreFailures": true + }, + { + "description": "${localize("enable.intel.disassembly.flavor", "Set Disassembly Flavor to {0}", "Intel").replace(/"/g, '')}", + "text": "-gdb-set disassembly-flavor intel", + "ignoreFailures": true + } +]`; + private executable: string = "a.out"; + private pipeProgram: string = "/usr/bin/ssh"; + + constructor() { + super(); + this.configurations = [ + new MIConfigurations(this.MIMode, this.executable, this.pipeProgram, this.setupCommandsBlock), + new PipeTransportConfigurations(this.MIMode, this.executable, this.pipeProgram, this.setupCommandsBlock) + ]; + } +} + +function convertConfigurationSnippetToCompletionItem(snippet: IConfigurationSnippet): vscode.CompletionItem { + const item: vscode.CompletionItem = new vscode.CompletionItem(snippet.label, vscode.CompletionItemKind.Module); + + item.insertText = snippet.bodyText; + + return item; +} + +export class ConfigurationSnippetProvider implements vscode.CompletionItemProvider { + private provider: IConfigurationAssetProvider; + private snippets: vscode.CompletionItem[]; + + constructor(provider: IConfigurationAssetProvider) { + this.provider = provider; + this.snippets = this.provider.getConfigurationSnippets(); + } + public resolveCompletionItem(item: vscode.CompletionItem, _token: vscode.CancellationToken): Thenable { + return Promise.resolve(item); + } + + // This function will only provide completion items via the Add Configuration Button + // There are two cases where the configuration array has nothing or has some items. + // 1. If it has nothing, insert a snippet the user selected. + // 2. If there are items, the Add Configuration button will append it to the start of the configuration array. This function inserts a comma at the end of the snippet. + public provideCompletionItems(document: vscode.TextDocument, _position: vscode.Position, _token: vscode.CancellationToken, _context: vscode.CompletionContext): Thenable { + let items: vscode.CompletionItem[] = this.snippets; + let hasLaunchConfigs: boolean = false; + try { + const launch: any = jsonc.parse(document.getText()); + hasLaunchConfigs = launch.configurations.length !== 0; + } catch { + // ignore + } + + // Check to see if the array is empty, so any additional inserted snippets will need commas. + if (hasLaunchConfigs) { + items = []; + + // Make a copy of each snippet since we are adding a comma to the end of the insertText. + this.snippets.forEach((item) => items.push({ ...item })); + + items.map((item) => { + item.insertText = item.insertText + ','; // Add comma + }); + } + + return Promise.resolve(new vscode.CompletionList(items, true)); + } +} diff --git a/Extension/src/LanguageServer/client.ts b/Extension/src/LanguageServer/client.ts index 6dd00d741b..5440f9861a 100644 --- a/Extension/src/LanguageServer/client.ts +++ b/Extension/src/LanguageServer/client.ts @@ -1,4133 +1,4128 @@ -/* -------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All Rights Reserved. - * See 'LICENSE' in the project root for license information. - * ------------------------------------------------------------------------------------------ */ -'use strict'; - -import * as path from 'path'; -import * as vscode from 'vscode'; - -// Start provider imports -import { CallHierarchyProvider } from './Providers/callHierarchyProvider'; -import { CodeActionProvider } from './Providers/codeActionProvider'; -import { DocumentFormattingEditProvider } from './Providers/documentFormattingEditProvider'; -import { DocumentRangeFormattingEditProvider } from './Providers/documentRangeFormattingEditProvider'; -import { DocumentSymbolProvider } from './Providers/documentSymbolProvider'; -import { FindAllReferencesProvider } from './Providers/findAllReferencesProvider'; -import { FoldingRangeProvider } from './Providers/foldingRangeProvider'; -import { CppInlayHint, InlayHintsProvider } from './Providers/inlayHintProvider'; -import { OnTypeFormattingEditProvider } from './Providers/onTypeFormattingEditProvider'; -import { RenameProvider } from './Providers/renameProvider'; -import { SemanticToken, SemanticTokensProvider } from './Providers/semanticTokensProvider'; -import { WorkspaceSymbolProvider } from './Providers/workspaceSymbolProvider'; -// End provider imports - -import { ok } from 'assert'; -import * as fs from 'fs'; -import * as os from 'os'; -import { SourceFileConfiguration, SourceFileConfigurationItem, Version, WorkspaceBrowseConfiguration } from 'vscode-cpptools'; -import { IntelliSenseStatus, Status } from 'vscode-cpptools/out/testApi'; -import { CloseAction, DidOpenTextDocumentParams, ErrorAction, LanguageClientOptions, NotificationType, Position, Range, RequestType, ResponseError, TextDocumentIdentifier, TextDocumentPositionParams } from 'vscode-languageclient'; -import { LanguageClient, ServerOptions } from 'vscode-languageclient/node'; -import * as nls from 'vscode-nls'; -import { DebugConfigurationProvider } from '../Debugger/configurationProvider'; -import { CustomConfigurationProvider1, getCustomConfigProviders, isSameProviderExtensionId } from '../LanguageServer/customProviders'; -import { ManualPromise } from '../Utility/Async/manualPromise'; -import { ManualSignal } from '../Utility/Async/manualSignal'; -import { logAndReturn } from '../Utility/Async/returns'; -import { is } from '../Utility/System/guards'; -import * as util from '../common'; -import { isWindows } from '../constants'; -import { DebugProtocolParams, Logger, ShowWarningParams, getDiagnosticsChannel, getOutputChannelLogger, logDebugProtocol, logLocalized, showWarning } from '../logger'; -import { localizedStringCount, lookupString } from '../nativeStrings'; -import { SessionState } from '../sessionState'; -import * as telemetry from '../telemetry'; -import { TestHook, getTestHook } from '../testHook'; -import { HoverProvider } from './Providers/HoverProvider'; -import { - CodeAnalysisDiagnosticIdentifiersAndUri, - RegisterCodeAnalysisNotifications, - RemoveCodeAnalysisProblemsParams, - removeAllCodeAnalysisProblems, - removeCodeAnalysisProblems -} from './codeAnalysis'; -import { Location, TextEdit, WorkspaceEdit } from './commonTypes'; -import * as configs from './configurations'; -import { DataBinding } from './dataBinding'; -import { cachedEditorConfigSettings, getEditorConfigSettings } from './editorConfig'; -import { CppSourceStr, clients, configPrefix, updateLanguageConfigurations, usesCrashHandler, watchForCrashes } from './extension'; -import { LocalizeStringParams, getLocaleId, getLocalizedString } from './localization'; -import { PersistentFolderState, PersistentWorkspaceState } from './persistentState'; -import { RequestCancelled, ServerCancelled, createProtocolFilter } from './protocolFilter'; -import * as refs from './references'; -import { CppSettings, OtherSettings, SettingsParams, WorkspaceFolderSettingsParams } from './settings'; -import { SettingsTracker } from './settingsTracker'; -import { ConfigurationType, LanguageStatusUI, getUI } from './ui'; -import { handleChangedFromCppToC, makeLspRange, makeVscodeLocation, makeVscodeRange, withCancellation } from './utils'; -import minimatch = require("minimatch"); - -function deepCopy(obj: any) { - return JSON.parse(JSON.stringify(obj)); -} -nls.config({ messageFormat: nls.MessageFormat.bundle, bundleFormat: nls.BundleFormat.standalone })(); -const localize: nls.LocalizeFunc = nls.loadMessageBundle(); - -let ui: LanguageStatusUI; -let timeStamp: number = 0; -const configProviderTimeout: number = 2000; -let initializedClientCount: number = 0; - -// Compiler paths that are known to be acceptable to execute. -const trustedCompilerPaths: string[] = []; -export function hasTrustedCompilerPaths(): boolean { - return trustedCompilerPaths.length !== 0; -} - -// Data shared by all clients. -let languageClient: LanguageClient; -let firstClientStarted: Promise; -let languageClientCrashedNeedsRestart: boolean = false; -const languageClientCrashTimes: number[] = []; -let compilerDefaults: configs.CompilerDefaults | undefined; -let diagnosticsCollectionIntelliSense: vscode.DiagnosticCollection; -let diagnosticsCollectionRefactor: vscode.DiagnosticCollection; - -interface ConfigStateReceived { - compilers: boolean; - compileCommands: boolean; - configProviders?: CustomConfigurationProvider1[]; - timeout: boolean; -} - -let workspaceHash: string = ""; - -let workspaceDisposables: vscode.Disposable[] = []; -export let workspaceReferences: refs.ReferencesManager; -export const openFileVersions: Map = new Map(); -export const cachedEditorConfigLookups: Map = new Map(); -export let semanticTokensLegend: vscode.SemanticTokensLegend | undefined; - -export function disposeWorkspaceData(): void { - workspaceDisposables.forEach((d) => d.dispose()); - workspaceDisposables = []; -} - -/** Note: We should not await on the following functions, - * or any function that returns a promise acquired from them, - * vscode.window.showInformationMessage, vscode.window.showWarningMessage, vscode.window.showErrorMessage -*/ -function showMessageWindow(params: ShowMessageWindowParams): void { - const message: string = getLocalizedString(params.localizeStringParams); - switch (params.type) { - case 1: // Error - void vscode.window.showErrorMessage(message); - break; - case 2: // Warning - void vscode.window.showWarningMessage(message); - break; - case 3: // Info - void vscode.window.showInformationMessage(message); - break; - default: - console.assert("Unrecognized type for showMessageWindow"); - break; - } -} - -function publishRefactorDiagnostics(params: PublishRefactorDiagnosticsParams): void { - if (!diagnosticsCollectionRefactor) { - diagnosticsCollectionRefactor = vscode.languages.createDiagnosticCollection(configPrefix + "Refactor"); - } - - const newDiagnostics: vscode.Diagnostic[] = []; - params.diagnostics.forEach((d) => { - const message: string = getLocalizedString(d.localizeStringParams); - const diagnostic: vscode.Diagnostic = new vscode.Diagnostic(makeVscodeRange(d.range), message, d.severity); - diagnostic.code = d.code; - diagnostic.source = CppSourceStr; - if (d.relatedInformation) { - diagnostic.relatedInformation = []; - for (const info of d.relatedInformation) { - diagnostic.relatedInformation.push(new vscode.DiagnosticRelatedInformation(makeVscodeLocation(info.location), info.message)); - } - } - - newDiagnostics.push(diagnostic); - }); - - const fileUri: vscode.Uri = vscode.Uri.parse(params.uri); - diagnosticsCollectionRefactor.set(fileUri, newDiagnostics); -} - -interface WorkspaceFolderParams { - workspaceFolderUri?: string; -} - -interface SelectionParams { - uri: string; - range: Range; -} - -export interface VsCodeUriAndRange { - uri: vscode.Uri; - range: vscode.Range; -} - -interface WorkspaceEditResult { - workspaceEdits: WorkspaceEdit[]; - errorText?: string; -} - -interface TelemetryPayload { - event: string; - properties?: Record; - metrics?: Record; -} - -interface ReportStatusNotificationBody extends WorkspaceFolderParams { - status: string; -} - -interface QueryDefaultCompilerParams { - newTrustedCompilerPath: string; -} - -interface CppPropertiesParams extends WorkspaceFolderParams { - currentConfiguration: number; - configurations: configs.Configuration[]; - isReady?: boolean; -} - -interface FolderSelectedSettingParams extends WorkspaceFolderParams { - currentConfiguration: number; -} - -interface SwitchHeaderSourceParams extends WorkspaceFolderParams { - switchHeaderSourceFileName: string; -} - -interface FileChangedParams extends WorkspaceFolderParams { - uri: string; -} - -interface InputRegion { - startLine: number; - endLine: number; -} - -interface DecorationRangesPair { - decoration: vscode.TextEditorDecorationType; - ranges: vscode.Range[]; -} - -interface InternalSourceFileConfiguration extends SourceFileConfiguration { - compilerArgsLegacy?: string[]; -} - -interface InternalWorkspaceBrowseConfiguration extends WorkspaceBrowseConfiguration { - compilerArgsLegacy?: string[]; -} - -// Need to convert vscode.Uri to a string before sending it to the language server. -interface SourceFileConfigurationItemAdapter { - uri: string; - configuration: InternalSourceFileConfiguration; -} - -interface CustomConfigurationParams extends WorkspaceFolderParams { - configurationItems: SourceFileConfigurationItemAdapter[]; -} - -interface CustomBrowseConfigurationParams extends WorkspaceFolderParams { - browseConfiguration: InternalWorkspaceBrowseConfiguration; -} - -interface CompileCommandsPaths extends WorkspaceFolderParams { - paths: string[]; -} - -interface GetDiagnosticsResult { - diagnostics: string; -} - -interface IntelliSenseDiagnosticRelatedInformation { - location: Location; - message: string; -} - -interface RefactorDiagnosticRelatedInformation { - location: Location; - message: string; -} - -interface IntelliSenseDiagnostic { - range: Range; - code?: number; - severity: vscode.DiagnosticSeverity; - localizeStringParams: LocalizeStringParams; - relatedInformation?: IntelliSenseDiagnosticRelatedInformation[]; -} - -interface RefactorDiagnostic { - range: Range; - code?: number; - severity: vscode.DiagnosticSeverity; - localizeStringParams: LocalizeStringParams; - relatedInformation?: RefactorDiagnosticRelatedInformation[]; -} - -interface PublishRefactorDiagnosticsParams { - uri: string; - diagnostics: RefactorDiagnostic[]; -} - -export interface CreateDeclarationOrDefinitionParams extends SelectionParams { - formatParams: FormatParams; - copyToClipboard: boolean; -} - -export interface CreateDeclarationOrDefinitionResult extends WorkspaceEditResult { - clipboardText?: string; -} - -export interface ExtractToFunctionParams extends SelectionParams { - extractAsGlobal: boolean; - name: string; -} - -interface ShowMessageWindowParams { - type: number; - localizeStringParams: LocalizeStringParams; -} - -export interface GetDocumentSymbolRequestParams { - uri: string; -} - -export interface WorkspaceSymbolParams extends WorkspaceFolderParams { - query: string; - experimentEnabled: boolean; -} - -export enum SymbolScope { - Public = 0, - Protected = 1, - Private = 2 -} - -export interface LocalizeDocumentSymbol { - name: string; - detail: LocalizeStringParams; - kind: vscode.SymbolKind; - scope: SymbolScope; - range: Range; - selectionRange: Range; - children: LocalizeDocumentSymbol[]; -} - -export interface GetDocumentSymbolResult { - symbols: LocalizeDocumentSymbol[]; -} - -export interface LocalizeSymbolInformation { - name: string; - kind: vscode.SymbolKind; - scope: SymbolScope; - location: Location; - containerName: string; - suffix: LocalizeStringParams; -} - -export interface FormatParams extends SelectionParams { - character: string; - insertSpaces: boolean; - tabSize: number; - editorConfigSettings: any; - useVcFormat: boolean; - onChanges: boolean; -} - -export interface FormatResult { - edits: TextEdit[]; -} - -export interface GetFoldingRangesParams { - uri: string; -} - -export enum FoldingRangeKind { - None = 0, - Comment = 1, - Imports = 2, - Region = 3 -} - -export interface CppFoldingRange { - kind: FoldingRangeKind; - range: InputRegion; -} - -export interface GetFoldingRangesResult { - ranges: CppFoldingRange[]; -} - -export interface IntelliSenseResult { - uri: string; - fileVersion: number; - diagnostics: IntelliSenseDiagnostic[]; - inactiveRegions: InputRegion[]; - semanticTokens: SemanticToken[]; - inlayHints: CppInlayHint[]; - clearExistingDiagnostics: boolean; - clearExistingInactiveRegions: boolean; - clearExistingSemanticTokens: boolean; - clearExistingInlayHint: boolean; - isCompletePass: boolean; -} - -enum SemanticTokenTypes { - // These are camelCase as the enum names are used directly as strings in our legend. - macro = 0, - enumMember = 1, - variable = 2, - parameter = 3, - type = 4, - referenceType = 5, - valueType = 6, - function = 7, - method = 8, - property = 9, - cliProperty = 10, - event = 11, - genericType = 12, - templateFunction = 13, - templateType = 14, - namespace = 15, - label = 16, - customLiteral = 17, - numberLiteral = 18, - stringLiteral = 19, - operatorOverload = 20, - memberOperatorOverload = 21, - newOperator = 22 -} - -enum SemanticTokenModifiers { - // These are camelCase as the enum names are used directly as strings in our legend. - static = 0b001, - global = 0b010, - local = 0b100 -} - -interface IntelliSenseSetup { - uri: string; -} - -interface GoToDirectiveInGroupParams { - uri: string; - position: Position; - next: boolean; -} - -export interface GenerateDoxygenCommentParams { - uri: string; - position: Position; - isCodeAction: boolean; - isCursorAboveSignatureLine: boolean | undefined; -} - -export interface GenerateDoxygenCommentResult { - contents: string; - initPosition: Position; - finalInsertionLine: number; - finalCursorPosition: Position; - fileVersion: number; - isCursorAboveSignatureLine: boolean; -} - -export interface IndexableQuickPickItem extends vscode.QuickPickItem { - index: number; -} - -export interface UpdateTrustedCompilerPathsResult { - compilerPath: string; -} - -export interface DoxygenCodeActionCommandArguments { - initialCursor: Position; - adjustedCursor: Position; - isCursorAboveSignatureLine: boolean; -} - -interface SetTemporaryTextDocumentLanguageParams { - uri: string; - isC: boolean; - isCuda: boolean; -} - -enum CodeAnalysisScope { - ActiveFile, - OpenFiles, - AllFiles, - ClearSquiggles -} - -interface CodeAnalysisParams { - scope: CodeAnalysisScope; -} - -interface FinishedRequestCustomConfigParams { - uri: string; -} - -export interface TextDocumentWillSaveParams { - textDocument: TextDocumentIdentifier; - reason: vscode.TextDocumentSaveReason; -} - -interface LspInitializationOptions { - loggingLevel: number; -} - -interface CppInitializationParams { - packageVersion: string; - extensionPath: string; - cacheStoragePath: string; - workspaceStoragePath: string; - databaseStoragePath: string; - vcpkgRoot: string; - intelliSenseCacheDisabled: boolean; - caseSensitiveFileSupport: boolean; - resetDatabase: boolean; - edgeMessagesDirectory: string; - localizedStrings: string[]; - settings: SettingsParams; -} - -interface TagParseStatus { - localizeStringParams: LocalizeStringParams; - isPaused: boolean; -} - -interface DidChangeVisibleTextEditorsParams { - activeUri?: string; - activeSelection?: Range; - visibleRanges?: { [uri: string]: Range[] }; -} - -interface DidChangeTextEditorVisibleRangesParams { - uri: string; - visibleRanges: Range[]; -} - -interface DidChangeActiveEditorParams { - uri?: string; - selection?: Range; -} - -interface GetIncludesParams { - maxDepth: number; -} - -export interface GetIncludesResult { - includedFiles: string[]; -} - -export interface ChatContextResult { - language: string; - standardVersion: string; - compiler: string; - targetPlatform: string; - targetArchitecture: string; -} - -// Requests -const PreInitializationRequest: RequestType = new RequestType('cpptools/preinitialize'); -const InitializationRequest: RequestType = new RequestType('cpptools/initialize'); -const QueryCompilerDefaultsRequest: RequestType = new RequestType('cpptools/queryCompilerDefaults'); -const SwitchHeaderSourceRequest: RequestType = new RequestType('cpptools/didSwitchHeaderSource'); -const GetDiagnosticsRequest: RequestType = new RequestType('cpptools/getDiagnostics'); -export const GetDocumentSymbolRequest: RequestType = new RequestType('cpptools/getDocumentSymbols'); -export const GetSymbolInfoRequest: RequestType = new RequestType('cpptools/getWorkspaceSymbols'); -export const GetFoldingRangesRequest: RequestType = new RequestType('cpptools/getFoldingRanges'); -export const FormatDocumentRequest: RequestType = new RequestType('cpptools/formatDocument'); -export const FormatRangeRequest: RequestType = new RequestType('cpptools/formatRange'); -export const FormatOnTypeRequest: RequestType = new RequestType('cpptools/formatOnType'); -export const HoverRequest: RequestType = new RequestType('cpptools/hover'); -const CreateDeclarationOrDefinitionRequest: RequestType = new RequestType('cpptools/createDeclDef'); -const ExtractToFunctionRequest: RequestType = new RequestType('cpptools/extractToFunction'); -const GoToDirectiveInGroupRequest: RequestType = new RequestType('cpptools/goToDirectiveInGroup'); -const GenerateDoxygenCommentRequest: RequestType = new RequestType('cpptools/generateDoxygenComment'); -const ChangeCppPropertiesRequest: RequestType = new RequestType('cpptools/didChangeCppProperties'); -const IncludesRequest: RequestType = new RequestType('cpptools/getIncludes'); -const CppContextRequest: RequestType = new RequestType('cpptools/getChatContext'); - -// Notifications to the server -const DidOpenNotification: NotificationType = new NotificationType('textDocument/didOpen'); -const FileCreatedNotification: NotificationType = new NotificationType('cpptools/fileCreated'); -const FileChangedNotification: NotificationType = new NotificationType('cpptools/fileChanged'); -const FileDeletedNotification: NotificationType = new NotificationType('cpptools/fileDeleted'); -const ResetDatabaseNotification: NotificationType = new NotificationType('cpptools/resetDatabase'); -const PauseParsingNotification: NotificationType = new NotificationType('cpptools/pauseParsing'); -const ResumeParsingNotification: NotificationType = new NotificationType('cpptools/resumeParsing'); -const DidChangeActiveEditorNotification: NotificationType = new NotificationType('cpptools/didChangeActiveEditor'); -const RestartIntelliSenseForFileNotification: NotificationType = new NotificationType('cpptools/restartIntelliSenseForFile'); -const DidChangeTextEditorSelectionNotification: NotificationType = new NotificationType('cpptools/didChangeTextEditorSelection'); -const ChangeCompileCommandsNotification: NotificationType = new NotificationType('cpptools/didChangeCompileCommands'); -const ChangeSelectedSettingNotification: NotificationType = new NotificationType('cpptools/didChangeSelectedSetting'); -const IntervalTimerNotification: NotificationType = new NotificationType('cpptools/onIntervalTimer'); -const CustomConfigurationHighPriorityNotification: NotificationType = new NotificationType('cpptools/didChangeCustomConfigurationHighPriority'); -const CustomConfigurationNotification: NotificationType = new NotificationType('cpptools/didChangeCustomConfiguration'); -const CustomBrowseConfigurationNotification: NotificationType = new NotificationType('cpptools/didChangeCustomBrowseConfiguration'); -const ClearCustomConfigurationsNotification: NotificationType = new NotificationType('cpptools/clearCustomConfigurations'); -const ClearCustomBrowseConfigurationNotification: NotificationType = new NotificationType('cpptools/clearCustomBrowseConfiguration'); -const PreviewReferencesNotification: NotificationType = new NotificationType('cpptools/previewReferences'); -const RescanFolderNotification: NotificationType = new NotificationType('cpptools/rescanFolder'); -const FinishedRequestCustomConfig: NotificationType = new NotificationType('cpptools/finishedRequestCustomConfig'); -const DidChangeSettingsNotification: NotificationType = new NotificationType('cpptools/didChangeSettings'); -const DidChangeVisibleTextEditorsNotification: NotificationType = new NotificationType('cpptools/didChangeVisibleTextEditors'); -const DidChangeTextEditorVisibleRangesNotification: NotificationType = new NotificationType('cpptools/didChangeTextEditorVisibleRanges'); - -const CodeAnalysisNotification: NotificationType = new NotificationType('cpptools/runCodeAnalysis'); -const PauseCodeAnalysisNotification: NotificationType = new NotificationType('cpptools/pauseCodeAnalysis'); -const ResumeCodeAnalysisNotification: NotificationType = new NotificationType('cpptools/resumeCodeAnalysis'); -const CancelCodeAnalysisNotification: NotificationType = new NotificationType('cpptools/cancelCodeAnalysis'); -const RemoveCodeAnalysisProblemsNotification: NotificationType = new NotificationType('cpptools/removeCodeAnalysisProblems'); - -// Notifications from the server -const ReloadWindowNotification: NotificationType = new NotificationType('cpptools/reloadWindow'); -const UpdateTrustedCompilersNotification: NotificationType = new NotificationType('cpptools/updateTrustedCompilersList'); -const LogTelemetryNotification: NotificationType = new NotificationType('cpptools/logTelemetry'); -const ReportTagParseStatusNotification: NotificationType = new NotificationType('cpptools/reportTagParseStatus'); -const ReportStatusNotification: NotificationType = new NotificationType('cpptools/reportStatus'); -const DebugProtocolNotification: NotificationType = new NotificationType('cpptools/debugProtocol'); -const DebugLogNotification: NotificationType = new NotificationType('cpptools/debugLog'); -const CompileCommandsPathsNotification: NotificationType = new NotificationType('cpptools/compileCommandsPaths'); -const ReferencesNotification: NotificationType = new NotificationType('cpptools/references'); -const ReportReferencesProgressNotification: NotificationType = new NotificationType('cpptools/reportReferencesProgress'); -const RequestCustomConfig: NotificationType = new NotificationType('cpptools/requestCustomConfig'); -const PublishRefactorDiagnosticsNotification: NotificationType = new NotificationType('cpptools/publishRefactorDiagnostics'); -const ShowMessageWindowNotification: NotificationType = new NotificationType('cpptools/showMessageWindow'); -const ShowWarningNotification: NotificationType = new NotificationType('cpptools/showWarning'); -const ReportTextDocumentLanguage: NotificationType = new NotificationType('cpptools/reportTextDocumentLanguage'); -const IntelliSenseSetupNotification: NotificationType = new NotificationType('cpptools/IntelliSenseSetup'); -const SetTemporaryTextDocumentLanguageNotification: NotificationType = new NotificationType('cpptools/setTemporaryTextDocumentLanguage'); -const ReportCodeAnalysisProcessedNotification: NotificationType = new NotificationType('cpptools/reportCodeAnalysisProcessed'); -const ReportCodeAnalysisTotalNotification: NotificationType = new NotificationType('cpptools/reportCodeAnalysisTotal'); -const DoxygenCommentGeneratedNotification: NotificationType = new NotificationType('cpptools/insertDoxygenComment'); -const CanceledReferencesNotification: NotificationType = new NotificationType('cpptools/canceledReferences'); -const IntelliSenseResultNotification: NotificationType = new NotificationType('cpptools/intelliSenseResult'); - -let failureMessageShown: boolean = false; - -class ClientModel { - public isInitializingWorkspace: DataBinding; - public isIndexingWorkspace: DataBinding; - public isParsingWorkspace: DataBinding; - public isParsingWorkspacePaused: DataBinding; - public isParsingFiles: DataBinding; - public isUpdatingIntelliSense: DataBinding; - public isRunningCodeAnalysis: DataBinding; - public isCodeAnalysisPaused: DataBinding; - public codeAnalysisProcessed: DataBinding; - public codeAnalysisTotal: DataBinding; - public referencesCommandMode: DataBinding; - public parsingWorkspaceStatus: DataBinding; - public activeConfigName: DataBinding; - - constructor() { - this.isInitializingWorkspace = new DataBinding(false); - this.isIndexingWorkspace = new DataBinding(false); - - // The following elements add a delay of 500ms before notitfying the UI that the icon can hide itself. - this.isParsingWorkspace = new DataBinding(false, 500, false); - this.isParsingWorkspacePaused = new DataBinding(false, 500, false); - this.isParsingFiles = new DataBinding(false, 500, false); - this.isUpdatingIntelliSense = new DataBinding(false, 500, false); - - this.isRunningCodeAnalysis = new DataBinding(false); - this.isCodeAnalysisPaused = new DataBinding(false); - this.codeAnalysisProcessed = new DataBinding(0); - this.codeAnalysisTotal = new DataBinding(0); - this.referencesCommandMode = new DataBinding(refs.ReferencesCommandMode.None); - this.parsingWorkspaceStatus = new DataBinding(""); - this.activeConfigName = new DataBinding(""); - } - - public activate(): void { - this.isInitializingWorkspace.activate(); - this.isIndexingWorkspace.activate(); - this.isParsingWorkspace.activate(); - this.isParsingWorkspacePaused.activate(); - this.isParsingFiles.activate(); - this.isUpdatingIntelliSense.activate(); - this.isRunningCodeAnalysis.activate(); - this.isCodeAnalysisPaused.activate(); - this.codeAnalysisProcessed.activate(); - this.codeAnalysisTotal.activate(); - this.referencesCommandMode.activate(); - this.parsingWorkspaceStatus.activate(); - this.activeConfigName.activate(); - } - - public deactivate(): void { - this.isInitializingWorkspace.deactivate(); - this.isIndexingWorkspace.deactivate(); - this.isParsingWorkspace.deactivate(); - this.isParsingWorkspacePaused.deactivate(); - this.isParsingFiles.deactivate(); - this.isUpdatingIntelliSense.deactivate(); - this.isRunningCodeAnalysis.deactivate(); - this.isCodeAnalysisPaused.deactivate(); - this.codeAnalysisProcessed.deactivate(); - this.codeAnalysisTotal.deactivate(); - this.referencesCommandMode.deactivate(); - this.parsingWorkspaceStatus.deactivate(); - this.activeConfigName.deactivate(); - } - - public dispose(): void { - this.isInitializingWorkspace.dispose(); - this.isIndexingWorkspace.dispose(); - this.isParsingWorkspace.dispose(); - this.isParsingWorkspacePaused.dispose(); - this.isParsingFiles.dispose(); - this.isUpdatingIntelliSense.dispose(); - this.isRunningCodeAnalysis.dispose(); - this.isCodeAnalysisPaused.dispose(); - this.codeAnalysisProcessed.dispose(); - this.codeAnalysisTotal.dispose(); - this.referencesCommandMode.dispose(); - this.parsingWorkspaceStatus.dispose(); - this.activeConfigName.dispose(); - } -} - -export interface Client { - readonly ready: Promise; - enqueue(task: () => Promise): Promise; - InitializingWorkspaceChanged: vscode.Event; - IndexingWorkspaceChanged: vscode.Event; - ParsingWorkspaceChanged: vscode.Event; - ParsingWorkspacePausedChanged: vscode.Event; - ParsingFilesChanged: vscode.Event; - IntelliSenseParsingChanged: vscode.Event; - RunningCodeAnalysisChanged: vscode.Event; - CodeAnalysisPausedChanged: vscode.Event; - CodeAnalysisProcessedChanged: vscode.Event; - CodeAnalysisTotalChanged: vscode.Event; - ReferencesCommandModeChanged: vscode.Event; - TagParserStatusChanged: vscode.Event; - ActiveConfigChanged: vscode.Event; - RootPath: string; - RootRealPath: string; - RootUri?: vscode.Uri; - RootFolder?: vscode.WorkspaceFolder; - Name: string; - TrackedDocuments: Map; - onDidChangeSettings(event: vscode.ConfigurationChangeEvent): Promise>; - onDidOpenTextDocument(document: vscode.TextDocument): void; - onDidCloseTextDocument(document: vscode.TextDocument): void; - onDidChangeVisibleTextEditors(editors: readonly vscode.TextEditor[]): Promise; - onDidChangeTextEditorVisibleRanges(uri: vscode.Uri): Promise; - onDidChangeTextDocument(textDocumentChangeEvent: vscode.TextDocumentChangeEvent): void; - onRegisterCustomConfigurationProvider(provider: CustomConfigurationProvider1): Thenable; - updateCustomConfigurations(requestingProvider?: CustomConfigurationProvider1): Thenable; - updateCustomBrowseConfiguration(requestingProvider?: CustomConfigurationProvider1): Thenable; - provideCustomConfiguration(docUri: vscode.Uri): Promise; - logDiagnostics(): Promise; - rescanFolder(): Promise; - toggleReferenceResultsView(): void; - setCurrentConfigName(configurationName: string): Thenable; - getCurrentConfigName(): Thenable; - getCurrentConfigCustomVariable(variableName: string): Thenable; - getVcpkgInstalled(): Thenable; - getVcpkgEnabled(): Thenable; - getCurrentCompilerPathAndArgs(): Thenable; - getKnownCompilers(): Thenable; - takeOwnership(document: vscode.TextDocument): void; - sendDidOpen(document: vscode.TextDocument): Promise; - requestSwitchHeaderSource(rootUri: vscode.Uri, fileName: string): Thenable; - updateActiveDocumentTextOptions(): void; - didChangeActiveEditor(editor?: vscode.TextEditor, selection?: Range): Promise; - restartIntelliSenseForFile(document: vscode.TextDocument): Promise; - activate(): void; - selectionChanged(selection: Range): void; - resetDatabase(): void; - deactivate(): void; - promptSelectIntelliSenseConfiguration(sender?: any): Promise; - rescanCompilers(sender?: any): Promise; - pauseParsing(): void; - resumeParsing(): void; - PauseCodeAnalysis(): void; - ResumeCodeAnalysis(): void; - CancelCodeAnalysis(): void; - handleConfigurationSelectCommand(): Promise; - handleConfigurationProviderSelectCommand(): Promise; - handleShowActiveCodeAnalysisCommands(): Promise; - handleShowIdleCodeAnalysisCommands(): Promise; - handleReferencesIcon(): void; - handleConfigurationEditCommand(viewColumn?: vscode.ViewColumn): void; - handleConfigurationEditJSONCommand(viewColumn?: vscode.ViewColumn): void; - handleConfigurationEditUICommand(viewColumn?: vscode.ViewColumn): void; - handleAddToIncludePathCommand(path: string): void; - handleGoToDirectiveInGroup(next: boolean): Promise; - handleGenerateDoxygenComment(args: DoxygenCodeActionCommandArguments | vscode.Uri | undefined): Promise; - handleRunCodeAnalysisOnActiveFile(): Promise; - handleRunCodeAnalysisOnOpenFiles(): Promise; - handleRunCodeAnalysisOnAllFiles(): Promise; - handleRemoveAllCodeAnalysisProblems(): Promise; - handleRemoveCodeAnalysisProblems(refreshSquigglesOnSave: boolean, identifiersAndUris: CodeAnalysisDiagnosticIdentifiersAndUri[]): Promise; - handleFixCodeAnalysisProblems(workspaceEdit: vscode.WorkspaceEdit, refreshSquigglesOnSave: boolean, identifiersAndUris: CodeAnalysisDiagnosticIdentifiersAndUri[]): Promise; - handleDisableAllTypeCodeAnalysisProblems(code: string, identifiersAndUris: CodeAnalysisDiagnosticIdentifiersAndUri[]): Promise; - handleCreateDeclarationOrDefinition(isCopyToClipboard: boolean, codeActionRange?: Range): Promise; - handleExtractToFunction(extractAsGlobal: boolean): Promise; - onInterval(): void; - dispose(): void; - addFileAssociations(fileAssociations: string, languageId: string): void; - sendDidChangeSettings(): void; - isInitialized(): boolean; - getShowConfigureIntelliSenseButton(): boolean; - setShowConfigureIntelliSenseButton(show: boolean): void; - addTrustedCompiler(path: string): Promise; - getIncludes(maxDepth: number, token: vscode.CancellationToken): Promise; - getChatContext(token: vscode.CancellationToken): Promise; -} - -export function createClient(workspaceFolder?: vscode.WorkspaceFolder): Client { - return new DefaultClient(workspaceFolder); -} - -export function createNullClient(): Client { - return new NullClient(); -} - -export class DefaultClient implements Client { - private innerLanguageClient?: LanguageClient; // The "client" that launches and communicates with our language "server" process. - private disposables: vscode.Disposable[] = []; - private documentFormattingProviderDisposable: vscode.Disposable | undefined; - private formattingRangeProviderDisposable: vscode.Disposable | undefined; - private onTypeFormattingProviderDisposable: vscode.Disposable | undefined; - private codeFoldingProvider: FoldingRangeProvider | undefined; - private codeFoldingProviderDisposable: vscode.Disposable | undefined; - private inlayHintsProvider: InlayHintsProvider | undefined; - private semanticTokensProvider: SemanticTokensProvider | undefined; - private semanticTokensProviderDisposable: vscode.Disposable | undefined; - private innerConfiguration?: configs.CppProperties; - private rootPathFileWatcher?: vscode.FileSystemWatcher; - private rootFolder?: vscode.WorkspaceFolder; - private rootRealPath: string; - private workspaceStoragePath: string; - private trackedDocuments = new Map(); - private isSupported: boolean = true; - private inactiveRegionsDecorations = new Map(); - private settingsTracker: SettingsTracker; - private loggingLevel: number = 1; - private configurationProvider?: string; - - public lastCustomBrowseConfiguration: PersistentFolderState | undefined; - public lastCustomBrowseConfigurationProviderId: PersistentFolderState | undefined; - public lastCustomBrowseConfigurationProviderVersion: PersistentFolderState | undefined; - public currentCaseSensitiveFileSupport: PersistentWorkspaceState | undefined; - private registeredProviders: PersistentFolderState | undefined; - - private configStateReceived: ConfigStateReceived = { compilers: false, compileCommands: false, configProviders: undefined, timeout: false }; - private showConfigureIntelliSenseButton: boolean = false; - - /** A queue of asynchronous tasks that need to be processed befofe ready is considered active. */ - private static queue = new Array<[ManualPromise, () => Promise] | [ManualPromise]>(); - - /** returns a promise that waits initialization and/or a change to configuration to complete (i.e. language client is ready-to-use) */ - private static readonly isStarted = new ManualSignal(true); - - /** - * Indicates if the blocking task dispatcher is currently running - * - * This will be in the Set state when the dispatcher is not running (i.e. if you await this it will be resolved immediately) - * If the dispatcher is running, this will be in the Reset state (i.e. if you await this it will be resolved when the dispatcher is done) - */ - private static readonly dispatching = new ManualSignal(); - - // The "model" that is displayed via the UI (status bar). - private model: ClientModel = new ClientModel(); - - public get InitializingWorkspaceChanged(): vscode.Event { return this.model.isInitializingWorkspace.ValueChanged; } - public get IndexingWorkspaceChanged(): vscode.Event { return this.model.isIndexingWorkspace.ValueChanged; } - public get ParsingWorkspaceChanged(): vscode.Event { return this.model.isParsingWorkspace.ValueChanged; } - public get ParsingWorkspacePausedChanged(): vscode.Event { return this.model.isParsingWorkspacePaused.ValueChanged; } - public get ParsingFilesChanged(): vscode.Event { return this.model.isParsingFiles.ValueChanged; } - public get IntelliSenseParsingChanged(): vscode.Event { return this.model.isUpdatingIntelliSense.ValueChanged; } - public get RunningCodeAnalysisChanged(): vscode.Event { return this.model.isRunningCodeAnalysis.ValueChanged; } - public get CodeAnalysisPausedChanged(): vscode.Event { return this.model.isCodeAnalysisPaused.ValueChanged; } - public get CodeAnalysisProcessedChanged(): vscode.Event { return this.model.codeAnalysisProcessed.ValueChanged; } - public get CodeAnalysisTotalChanged(): vscode.Event { return this.model.codeAnalysisTotal.ValueChanged; } - public get ReferencesCommandModeChanged(): vscode.Event { return this.model.referencesCommandMode.ValueChanged; } - public get TagParserStatusChanged(): vscode.Event { return this.model.parsingWorkspaceStatus.ValueChanged; } - public get ActiveConfigChanged(): vscode.Event { return this.model.activeConfigName.ValueChanged; } - public isInitialized(): boolean { return this.innerLanguageClient !== undefined; } - public getShowConfigureIntelliSenseButton(): boolean { return this.showConfigureIntelliSenseButton; } - public setShowConfigureIntelliSenseButton(show: boolean): void { this.showConfigureIntelliSenseButton = show; } - - /** - * don't use this.rootFolder directly since it can be undefined - */ - public get RootPath(): string { - return this.rootFolder ? this.rootFolder.uri.fsPath : ""; - } - public get RootRealPath(): string { - return this.rootRealPath; - } - public get RootUri(): vscode.Uri | undefined { - return this.rootFolder ? this.rootFolder.uri : undefined; - } - public get RootFolder(): vscode.WorkspaceFolder | undefined { - return this.rootFolder; - } - public get Name(): string { - return this.getName(this.rootFolder); - } - public get TrackedDocuments(): Map { - return this.trackedDocuments; - } - public get IsTagParsing(): boolean { - return this.model.isParsingWorkspace.Value || this.model.isParsingFiles.Value || this.model.isInitializingWorkspace.Value || this.model.isIndexingWorkspace.Value; - } - public get ReferencesCommandMode(): refs.ReferencesCommandMode { - return this.model.referencesCommandMode.Value; - } - - public get languageClient(): LanguageClient { - if (!this.innerLanguageClient) { - throw new Error("Attempting to use languageClient before initialized"); - } - return this.innerLanguageClient; - } - - public get configuration(): configs.CppProperties { - if (!this.innerConfiguration) { - throw new Error("Attempting to use configuration before initialized"); - } - return this.innerConfiguration; - } - - public get AdditionalEnvironment(): Record { - return { - workspaceFolderBasename: this.Name, - workspaceStorage: this.workspaceStoragePath, - execPath: process.execPath, - pathSeparator: (os.platform() === 'win32') ? "\\" : "/" - }; - } - - private getName(workspaceFolder?: vscode.WorkspaceFolder): string { - return workspaceFolder ? workspaceFolder.name : "untitled"; - } - - public static updateClientConfigurations(): void { - clients.forEach(client => { - if (client instanceof DefaultClient) { - const defaultClient: DefaultClient = client as DefaultClient; - if (!client.isInitialized() || !compilerDefaults) { - // This can randomly get hit when adding/removing workspace folders. - return; - } - defaultClient.configuration.CompilerDefaults = compilerDefaults; - defaultClient.configuration.handleConfigurationChange(); - } - }); - } - - private static readonly configurationProvidersLabel: string = "configuration providers"; - private static readonly compileCommandsLabel: string = "compile_commands.json"; - private static readonly compilersLabel: string = "compilers"; - - public async showSelectIntelliSenseConfiguration(paths: string[], compilersOnly?: boolean): Promise { - const options: vscode.QuickPickOptions = {}; - options.placeHolder = compilersOnly || !vscode.workspace.workspaceFolders || !this.RootFolder ? - localize("select.compiler", "Select a compiler to configure for IntelliSense") : - vscode.workspace.workspaceFolders.length > 1 ? - localize("configure.intelliSense.forFolder", "How would you like to configure IntelliSense for the '{0}' folder?", this.RootFolder.name) : - localize("configure.intelliSense.thisFolder", "How would you like to configure IntelliSense for this folder?"); - - const items: IndexableQuickPickItem[] = []; - let isCompilerSection: boolean = false; - for (let i: number = 0; i < paths.length; i++) { - const compilerName: string = path.basename(paths[i]); - const isCompiler: boolean = isCompilerSection && compilerName !== paths[i]; - - if (isCompiler) { - const path: string | undefined = paths[i].replace(compilerName, ""); - const description: string = localize("found.string", "Found at {0}", path); - const label: string = localize("use.compiler", "Use {0}", compilerName); - items.push({ label: label, description: description, index: i }); - } else if (paths[i] === DefaultClient.configurationProvidersLabel) { - items.push({ label: localize("configuration.providers", "configuration providers"), index: i, kind: vscode.QuickPickItemKind.Separator }); - } else if (paths[i] === DefaultClient.compileCommandsLabel) { - items.push({ label: paths[i], index: i, kind: vscode.QuickPickItemKind.Separator }); - } else if (paths[i] === DefaultClient.compilersLabel) { - isCompilerSection = true; - items.push({ label: localize("compilers", "compilers"), index: i, kind: vscode.QuickPickItemKind.Separator }); - } else { - items.push({ label: paths[i], index: i }); - } - } - - const selection: IndexableQuickPickItem | undefined = await vscode.window.showQuickPick(items, options); - return selection ? selection.index : -1; - } - - public async showPrompt(sender?: any): Promise { - const buttonMessage: string = localize("selectIntelliSenseConfiguration.string", "Select IntelliSense Configuration..."); - const value: string | undefined = await vscode.window.showInformationMessage(localize("setCompiler.message", "You do not have IntelliSense configured. Unless you set your own configurations, IntelliSense may not be functional."), buttonMessage); - if (value === buttonMessage) { - return this.handleIntelliSenseConfigurationQuickPick(sender); - } - } - - public async handleIntelliSenseConfigurationQuickPick(sender?: any, showCompilersOnly?: boolean): Promise { - const settings: CppSettings = new CppSettings(showCompilersOnly ? undefined : this.RootUri); - const paths: string[] = []; - const configProviders: CustomConfigurationProvider1[] | undefined = showCompilersOnly ? undefined : this.configStateReceived.configProviders; - if (configProviders && configProviders.length > 0) { - paths.push(DefaultClient.configurationProvidersLabel); - for (const provider of configProviders) { - paths.push(localize("use.provider", "Use {0}", provider.name)); - } - } - const configProvidersIndex: number = paths.length; - const configProviderCount: number = configProvidersIndex === 0 ? 0 : configProvidersIndex - 1; - if (!showCompilersOnly && this.compileCommandsPaths.length > 0) { - paths.push(DefaultClient.compileCommandsLabel); - for (const compileCommandsPath of this.compileCommandsPaths) { - paths.push(localize("use.compileCommands", "Use {0}", compileCommandsPath)); - } - } - const compileCommandsIndex: number = paths.length; - const compileCommandsCount: number = compileCommandsIndex === configProvidersIndex ? 0 : compileCommandsIndex - configProvidersIndex - 1; - paths.push(DefaultClient.compilersLabel); - if (compilerDefaults?.knownCompilers !== undefined) { - const tempPaths: string[] = compilerDefaults.knownCompilers.map(function (a: configs.KnownCompiler): string { return a.path; }); - let clFound: boolean = false; - // Remove all but the first cl path. - for (const path of tempPaths) { - if (clFound) { - if (!util.isCl(path)) { - paths.push(path); - } - } else { - if (util.isCl(path)) { - clFound = true; - } - paths.push(path); - } - } - } - const compilersIndex: number = paths.length; - const compilerCount: number = compilersIndex === compileCommandsIndex ? 0 : compilersIndex - compileCommandsIndex - 1; - paths.push(localize("selectAnotherCompiler.string", "Select another compiler on my machine...")); - let installShown = true; - if (isWindows && util.getSenderType(sender) !== 'walkthrough') { - paths.push(localize("installCompiler.string", "Help me install a compiler")); - } else if (!isWindows) { - paths.push(localize("installCompiler.string.nix", "Install a compiler")); - } else { - installShown = false; - } - paths.push(localize("noConfig.string", "Do not configure with a compiler (not recommended)")); - const index: number = await this.showSelectIntelliSenseConfiguration(paths, showCompilersOnly); - let action: string = ""; - let configurationSelected: boolean = false; - const fromStatusBarButton: boolean = !showCompilersOnly; - try { - if (index === -1) { - action = "escaped"; - return; - } - if (index === paths.length - 1) { - action = "disable"; - settings.defaultCompilerPath = ""; - await this.configuration.updateCompilerPathIfSet(""); - configurationSelected = true; - await this.showPrompt(sender); - return ui.ShowConfigureIntelliSenseButton(false, this, ConfigurationType.CompilerPath, "disablePrompt"); - } - if (installShown && index === paths.length - 2) { - action = "install"; - void vscode.commands.executeCommand('C_Cpp.InstallCompiler', sender); - return; - } - const showButtonSender: string = "quickPick"; - if (index === paths.length - 3 || (!installShown && index === paths.length - 2)) { - const result: vscode.Uri[] | undefined = await vscode.window.showOpenDialog(); - if (result === undefined || result.length === 0) { - action = "browse dismissed"; - return; - } - configurationSelected = true; - action = "compiler browsed"; - settings.defaultCompilerPath = result[0].fsPath; - await this.configuration.updateCompilerPathIfSet(result[0].fsPath); - void SessionState.trustedCompilerFound.set(true); - } else { - configurationSelected = true; - if (index < configProvidersIndex && configProviders) { - action = "select config provider"; - const provider: CustomConfigurationProvider1 = configProviders[index - 1]; - await this.configuration.updateCustomConfigurationProvider(provider.extensionId); - void this.onCustomConfigurationProviderRegistered(provider).catch(logAndReturn.undefined); - telemetry.logLanguageServerEvent("customConfigurationProvider", { "providerId": provider.extensionId }); - - return ui.ShowConfigureIntelliSenseButton(false, this, ConfigurationType.ConfigProvider, showButtonSender); - } else if (index < compileCommandsIndex) { - action = "select compile commands"; - await this.configuration.setCompileCommands(this.compileCommandsPaths[index - configProvidersIndex - 1]); - return ui.ShowConfigureIntelliSenseButton(false, this, ConfigurationType.CompileCommands, showButtonSender); - } else { - action = "select compiler"; - const newCompiler: string = util.isCl(paths[index]) ? "cl.exe" : paths[index]; - settings.defaultCompilerPath = newCompiler; - await this.configuration.updateCompilerPathIfSet(newCompiler); - void SessionState.trustedCompilerFound.set(true); - } - } - - await ui.ShowConfigureIntelliSenseButton(false, this, ConfigurationType.CompilerPath, showButtonSender); - - await this.addTrustedCompiler(settings.defaultCompilerPath); - DefaultClient.updateClientConfigurations(); - } finally { - if (showCompilersOnly) { - telemetry.logLanguageServerEvent('compilerSelection', { action, sender: util.getSenderType(sender) }, - { compilerCount: compilerCount + 3 }); // + 3 is to match what was being incorrectly sent previously - } else { - telemetry.logLanguageServerEvent('configurationSelection', { action, sender: util.getSenderType(sender) }, - { configProviderCount, compileCommandsCount, compilerCount }); - } - - // Clear the prompt state. - // TODO: Add some way to change this state to true. - const rootFolder: vscode.WorkspaceFolder | undefined = this.RootFolder; - if (rootFolder && fromStatusBarButton) { - if (configurationSelected || configProviderCount > 0) { - const ask: PersistentFolderState = new PersistentFolderState("Client.registerProvider", true, rootFolder); - ask.Value = false; - } - if (configurationSelected || compileCommandsCount > 0) { - const ask: PersistentFolderState = new PersistentFolderState("CPP.showCompileCommandsSelection", true, rootFolder); - ask.Value = false; - } - if (!configurationSelected) { - await this.handleConfigStatus(); - } - } - } - } - - public async rescanCompilers(sender?: any): Promise { - compilerDefaults = await this.requestCompiler(); - DefaultClient.updateClientConfigurations(); - if (compilerDefaults.knownCompilers !== undefined && compilerDefaults.knownCompilers.length > 0) { - await this.handleIntelliSenseConfigurationQuickPick(sender, true); - } - } - - async promptSelectIntelliSenseConfiguration(sender?: any): Promise { - if (compilerDefaults === undefined) { - return; - } - if (compilerDefaults.compilerPath !== "") { - const showCompilersOnly: boolean = util.getSenderType(sender) === 'walkthrough'; - return this.handleIntelliSenseConfigurationQuickPick(sender, showCompilersOnly); - } - } - - /** - * All public methods on this class must be guarded by the "ready" promise. Requests and notifications received before the task is - * complete are executed after this promise is resolved. - */ - - constructor(workspaceFolder?: vscode.WorkspaceFolder) { - if (workspaceFolder !== undefined) { - this.lastCustomBrowseConfiguration = new PersistentFolderState("CPP.lastCustomBrowseConfiguration", undefined, workspaceFolder); - this.lastCustomBrowseConfigurationProviderId = new PersistentFolderState("CPP.lastCustomBrowseConfigurationProviderId", undefined, workspaceFolder); - this.lastCustomBrowseConfigurationProviderVersion = new PersistentFolderState("CPP.lastCustomBrowseConfigurationProviderVersion", Version.v5, workspaceFolder); - this.registeredProviders = new PersistentFolderState("CPP.registeredProviders", [], workspaceFolder); - // If this provider did the register in the last session, clear out the cached browse config. - if (!this.isProviderRegistered(this.lastCustomBrowseConfigurationProviderId.Value)) { - this.lastCustomBrowseConfigurationProviderId.Value = undefined; - if (this.lastCustomBrowseConfiguration !== undefined) { - this.lastCustomBrowseConfiguration.Value = undefined; - } - } - if (this.lastCustomBrowseConfigurationProviderId.Value) { - this.configStateReceived.configProviders = []; // avoid waiting for the timeout if it's cached - } - this.registeredProviders.Value = []; - } else { - this.configStateReceived.configProviders = []; - this.configStateReceived.compileCommands = true; - } - if (!semanticTokensLegend) { - // Semantic token types are identified by indexes in this list of types, in the legend. - const tokenTypesLegend: string[] = []; - for (const e in SemanticTokenTypes) { - // An enum is actually a set of mappings from key <=> value. Enumerate over only the names. - // This allow us to represent the constants using an enum, which we can match in native code. - if (isNaN(Number(e))) { - tokenTypesLegend.push(e); - } - } - // Semantic token modifiers are bit indexes corresponding to the indexes in this list of modifiers in the legend. - const tokenModifiersLegend: string[] = []; - for (const e in SemanticTokenModifiers) { - if (isNaN(Number(e))) { - tokenModifiersLegend.push(e); - } - } - semanticTokensLegend = new vscode.SemanticTokensLegend(tokenTypesLegend, tokenModifiersLegend); - } - - this.rootFolder = workspaceFolder; - this.rootRealPath = this.RootPath ? fs.existsSync(this.RootPath) ? fs.realpathSync(this.RootPath) : this.RootPath : ""; - - this.workspaceStoragePath = util.extensionContext?.storageUri?.fsPath ?? ""; - if (this.workspaceStoragePath.length > 0) { - workspaceHash = path.basename(path.dirname(this.workspaceStoragePath)); - } else { - this.workspaceStoragePath = this.RootPath ? path.join(this.RootPath, ".vscode") : ""; - } - - if (workspaceFolder && vscode.workspace.workspaceFolders && vscode.workspace.workspaceFolders.length > 1) { - this.workspaceStoragePath = path.join(this.workspaceStoragePath, util.getUniqueWorkspaceStorageName(workspaceFolder)); - } - - const rootUri: vscode.Uri | undefined = this.RootUri; - this.settingsTracker = new SettingsTracker(rootUri); - - try { - let isFirstClient: boolean = false; - if (firstClientStarted === undefined || languageClientCrashedNeedsRestart) { - if (languageClientCrashedNeedsRestart) { - languageClientCrashedNeedsRestart = false; - // if we're recovering, the isStarted needs to be reset. - // because we're starting the first client again. - DefaultClient.isStarted.reset(); - } - firstClientStarted = this.createLanguageClient(); - util.setProgress(util.getProgressExecutableStarted()); - isFirstClient = true; - } - void this.init(rootUri, isFirstClient).catch(logAndReturn.undefined); - - } catch (errJS) { - const err: NodeJS.ErrnoException = errJS as NodeJS.ErrnoException; - this.isSupported = false; // Running on an OS we don't support yet. - if (!failureMessageShown) { - failureMessageShown = true; - let additionalInfo: string; - if (err.code === "EPERM") { - additionalInfo = localize('check.permissions', "EPERM: Check permissions for '{0}'", getLanguageServerFileName()); - } else { - additionalInfo = String(err); - } - void vscode.window.showErrorMessage(localize("unable.to.start", "Unable to start the C/C++ language server. IntelliSense features will be disabled. Error: {0}", additionalInfo)); - } - } - } - - private async init(rootUri: vscode.Uri | undefined, isFirstClient: boolean) { - ui = getUI(); - ui.bind(this); - await firstClientStarted; - try { - const workspaceFolder: vscode.WorkspaceFolder | undefined = this.rootFolder; - this.innerConfiguration = new configs.CppProperties(this, rootUri, workspaceFolder); - this.innerConfiguration.ConfigurationsChanged((e) => this.onConfigurationsChanged(e)); - this.innerConfiguration.SelectionChanged((e) => this.onSelectedConfigurationChanged(e)); - this.innerConfiguration.CompileCommandsChanged((e) => this.onCompileCommandsChanged(e)); - this.disposables.push(this.innerConfiguration); - - this.innerLanguageClient = languageClient; - telemetry.logLanguageServerEvent("NonDefaultInitialCppSettings", this.settingsTracker.getUserModifiedSettings()); - failureMessageShown = false; - - if (isFirstClient) { - workspaceReferences = new refs.ReferencesManager(this); - // Only register file watchers and providers after the extension has finished initializing, - // e.g. prevents empty c_cpp_properties.json from generation. - this.registerFileWatcher(); - initializedClientCount = 0; - this.inlayHintsProvider = new InlayHintsProvider(); - - this.disposables.push(vscode.languages.registerHoverProvider(util.documentSelector, new HoverProvider(this))); - this.disposables.push(vscode.languages.registerInlayHintsProvider(util.documentSelector, this.inlayHintsProvider)); - this.disposables.push(vscode.languages.registerRenameProvider(util.documentSelector, new RenameProvider(this))); - this.disposables.push(vscode.languages.registerReferenceProvider(util.documentSelector, new FindAllReferencesProvider(this))); - this.disposables.push(vscode.languages.registerWorkspaceSymbolProvider(new WorkspaceSymbolProvider(this))); - this.disposables.push(vscode.languages.registerDocumentSymbolProvider(util.documentSelector, new DocumentSymbolProvider(), undefined)); - this.disposables.push(vscode.languages.registerCodeActionsProvider(util.documentSelector, new CodeActionProvider(this), undefined)); - this.disposables.push(vscode.languages.registerCallHierarchyProvider(util.documentSelector, new CallHierarchyProvider(this))); - // Because formatting and codeFolding can vary per folder, we need to register these providers once - // and leave them registered. The decision of whether to provide results needs to be made on a per folder basis, - // within the providers themselves. - this.documentFormattingProviderDisposable = vscode.languages.registerDocumentFormattingEditProvider(util.documentSelector, new DocumentFormattingEditProvider(this)); - this.formattingRangeProviderDisposable = vscode.languages.registerDocumentRangeFormattingEditProvider(util.documentSelector, new DocumentRangeFormattingEditProvider(this)); - this.onTypeFormattingProviderDisposable = vscode.languages.registerOnTypeFormattingEditProvider(util.documentSelector, new OnTypeFormattingEditProvider(this), ";", "}", "\n"); - - this.codeFoldingProvider = new FoldingRangeProvider(this); - this.codeFoldingProviderDisposable = vscode.languages.registerFoldingRangeProvider(util.documentSelector, this.codeFoldingProvider); - - const settings: CppSettings = new CppSettings(); - if (settings.isEnhancedColorizationEnabled && semanticTokensLegend) { - this.semanticTokensProvider = new SemanticTokensProvider(); - this.semanticTokensProviderDisposable = vscode.languages.registerDocumentSemanticTokensProvider(util.documentSelector, this.semanticTokensProvider, semanticTokensLegend); - } - - // Listen for messages from the language server. - this.registerNotifications(); - - // If a file is already open when we activate, sometimes we don't get any notifications about visible - // or active text editors, visible ranges, or text selection. As a workaround, we trigger - // onDidChangeVisibleTextEditors here. - const cppEditors: vscode.TextEditor[] = vscode.window.visibleTextEditors.filter(e => util.isCpp(e.document)); - await this.onDidChangeVisibleTextEditors(cppEditors); - } - - // update all client configurations - this.configuration.setupConfigurations(); - initializedClientCount++; - // count number of clients, once all clients are configured, check for trusted compiler to display notification to user and add a short delay to account for config provider logic to finish - if ((vscode.workspace.workspaceFolders === undefined) || (initializedClientCount >= vscode.workspace.workspaceFolders.length)) { - // Timeout waiting for compile_commands.json and config providers. - // The quick pick options will update if they're added later on. - clients.forEach(client => { - if (client instanceof DefaultClient) { - global.setTimeout(() => { - client.configStateReceived.timeout = true; - void client.handleConfigStatus(); - }, 15000); - } - }); - // The configurations will not be sent to the language server until the default include paths and frameworks have been set. - // The event handlers must be set before this happens. - compilerDefaults = await this.requestCompiler(); - DefaultClient.updateClientConfigurations(); - clients.forEach(client => { - if (client instanceof DefaultClient) { - client.configStateReceived.compilers = true; - void client.handleConfigStatus(); - } - }); - } - } catch (err) { - this.isSupported = false; // Running on an OS we don't support yet. - if (!failureMessageShown) { - failureMessageShown = true; - void vscode.window.showErrorMessage(localize("unable.to.start", "Unable to start the C/C++ language server. IntelliSense features will be disabled. Error: {0}", String(err))); - } - } - - DefaultClient.isStarted.resolve(); - } - - private getWorkspaceFolderSettings(workspaceFolderUri: vscode.Uri | undefined, settings: CppSettings, otherSettings: OtherSettings): WorkspaceFolderSettingsParams { - const result: WorkspaceFolderSettingsParams = { - uri: workspaceFolderUri?.toString(), - intelliSenseEngine: settings.intelliSenseEngine, - autocomplete: settings.autocomplete, - autocompleteAddParentheses: settings.autocompleteAddParentheses, - errorSquiggles: settings.errorSquiggles, - exclusionPolicy: settings.exclusionPolicy, - preferredPathSeparator: settings.preferredPathSeparator, - intelliSenseCachePath: util.resolveCachePath(settings.intelliSenseCachePath, this.AdditionalEnvironment), - intelliSenseCacheSize: settings.intelliSenseCacheSize, - intelliSenseMemoryLimit: settings.intelliSenseMemoryLimit, - dimInactiveRegions: settings.dimInactiveRegions, - suggestSnippets: settings.suggestSnippets, - legacyCompilerArgsBehavior: settings.legacyCompilerArgsBehavior, - defaultSystemIncludePath: settings.defaultSystemIncludePath, - cppFilesExclude: settings.filesExclude, - clangFormatPath: util.resolveVariables(settings.clangFormatPath, this.AdditionalEnvironment), - clangFormatStyle: settings.clangFormatStyle ? util.resolveVariables(settings.clangFormatStyle, this.AdditionalEnvironment) : undefined, - clangFormatFallbackStyle: settings.clangFormatFallbackStyle, - clangFormatSortIncludes: settings.clangFormatSortIncludes, - codeAnalysisRunAutomatically: settings.codeAnalysisRunAutomatically, - codeAnalysisExclude: settings.codeAnalysisExclude, - clangTidyEnabled: settings.clangTidyEnabled, - clangTidyPath: util.resolveVariables(settings.clangTidyPath, this.AdditionalEnvironment), - clangTidyConfig: settings.clangTidyConfig, - clangTidyFallbackConfig: settings.clangTidyFallbackConfig, - clangTidyHeaderFilter: settings.clangTidyHeaderFilter !== null ? util.resolveVariables(settings.clangTidyHeaderFilter, this.AdditionalEnvironment) : null, - clangTidyArgs: util.resolveVariablesArray(settings.clangTidyArgs, this.AdditionalEnvironment), - clangTidyUseBuildPath: settings.clangTidyUseBuildPath, - clangTidyChecksEnabled: settings.clangTidyChecksEnabled, - clangTidyChecksDisabled: settings.clangTidyChecksDisabled, - markdownInComments: settings.markdownInComments, - hover: settings.hover, - vcFormatIndentBraces: settings.vcFormatIndentBraces, - vcFormatIndentMultiLineRelativeTo: settings.vcFormatIndentMultiLineRelativeTo, - vcFormatIndentWithinParentheses: settings.vcFormatIndentWithinParentheses, - vcFormatIndentPreserveWithinParentheses: settings.vcFormatIndentPreserveWithinParentheses, - vcFormatIndentCaseLabels: settings.vcFormatIndentCaseLabels, - vcFormatIndentCaseContents: settings.vcFormatIndentCaseContents, - vcFormatIndentCaseContentsWhenBlock: settings.vcFormatIndentCaseContentsWhenBlock, - vcFormatIndentLambdaBracesWhenParameter: settings.vcFormatIndentLambdaBracesWhenParameter, - vcFormatIndentGotoLabels: settings.vcFormatIndentGotoLabels, - vcFormatIndentPreprocessor: settings.vcFormatIndentPreprocessor, - vcFormatIndentAccesSpecifiers: settings.vcFormatIndentAccessSpecifiers, - vcFormatIndentNamespaceContents: settings.vcFormatIndentNamespaceContents, - vcFormatIndentPreserveComments: settings.vcFormatIndentPreserveComments, - vcFormatNewLineScopeBracesOnSeparateLines: settings.vcFormatNewlineScopeBracesOnSeparateLines, - vcFormatNewLineBeforeOpenBraceNamespace: settings.vcFormatNewlineBeforeOpenBraceNamespace, - vcFormatNewLineBeforeOpenBraceType: settings.vcFormatNewlineBeforeOpenBraceType, - vcFormatNewLineBeforeOpenBraceFunction: settings.vcFormatNewlineBeforeOpenBraceFunction, - vcFormatNewLineBeforeOpenBraceBlock: settings.vcFormatNewlineBeforeOpenBraceBlock, - vcFormatNewLineBeforeOpenBraceLambda: settings.vcFormatNewlineBeforeOpenBraceLambda, - vcFormatNewLineBeforeCatch: settings.vcFormatNewlineBeforeCatch, - vcFormatNewLineBeforeElse: settings.vcFormatNewlineBeforeElse, - vcFormatNewLineBeforeWhileInDoWhile: settings.vcFormatNewlineBeforeWhileInDoWhile, - vcFormatNewLineCloseBraceSameLineEmptyType: settings.vcFormatNewlineCloseBraceSameLineEmptyType, - vcFormatNewLineCloseBraceSameLineEmptyFunction: settings.vcFormatNewlineCloseBraceSameLineEmptyFunction, - vcFormatSpaceBeforeFunctionOpenParenthesis: settings.vcFormatSpaceBeforeFunctionOpenParenthesis, - vcFormatSpaceWithinParameterListParentheses: settings.vcFormatSpaceWithinParameterListParentheses, - vcFormatSpaceBetweenEmptyParameterListParentheses: settings.vcFormatSpaceBetweenEmptyParameterListParentheses, - vcFormatSpaceAfterKeywordsInControlFlowStatements: settings.vcFormatSpaceAfterKeywordsInControlFlowStatements, - vcFormatSpaceWithinControlFlowStatementParentheses: settings.vcFormatSpaceWithinControlFlowStatementParentheses, - vcFormatSpaceBeforeLambdaOpenParenthesis: settings.vcFormatSpaceBeforeLambdaOpenParenthesis, - vcFormatSpaceWithinCastParentheses: settings.vcFormatSpaceWithinCastParentheses, - vcFormatSpaceAfterCastCloseParenthesis: settings.vcFormatSpaceAfterCastCloseParenthesis, - vcFormatSpaceWithinExpressionParentheses: settings.vcFormatSpaceWithinExpressionParentheses, - vcFormatSpaceBeforeBlockOpenBrace: settings.vcFormatSpaceBeforeBlockOpenBrace, - vcFormatSpaceBetweenEmptyBraces: settings.vcFormatSpaceBetweenEmptyBraces, - vcFormatSpaceBeforeInitializerListOpenBrace: settings.vcFormatSpaceBeforeInitializerListOpenBrace, - vcFormatSpaceWithinInitializerListBraces: settings.vcFormatSpaceWithinInitializerListBraces, - vcFormatSpacePreserveInInitializerList: settings.vcFormatSpacePreserveInInitializerList, - vcFormatSpaceBeforeOpenSquareBracket: settings.vcFormatSpaceBeforeOpenSquareBracket, - vcFormatSpaceWithinSquareBrackets: settings.vcFormatSpaceWithinSquareBrackets, - vcFormatSpaceBeforeEmptySquareBrackets: settings.vcFormatSpaceBeforeEmptySquareBrackets, - vcFormatSpaceBetweenEmptySquareBrackets: settings.vcFormatSpaceBetweenEmptySquareBrackets, - vcFormatSpaceGroupSquareBrackets: settings.vcFormatSpaceGroupSquareBrackets, - vcFormatSpaceWithinLambdaBrackets: settings.vcFormatSpaceWithinLambdaBrackets, - vcFormatSpaceBetweenEmptyLambdaBrackets: settings.vcFormatSpaceBetweenEmptyLambdaBrackets, - vcFormatSpaceBeforeComma: settings.vcFormatSpaceBeforeComma, - vcFormatSpaceAfterComma: settings.vcFormatSpaceAfterComma, - vcFormatSpaceRemoveAroundMemberOperators: settings.vcFormatSpaceRemoveAroundMemberOperators, - vcFormatSpaceBeforeInheritanceColon: settings.vcFormatSpaceBeforeInheritanceColon, - vcFormatSpaceBeforeConstructorColon: settings.vcFormatSpaceBeforeConstructorColon, - vcFormatSpaceRemoveBeforeSemicolon: settings.vcFormatSpaceRemoveBeforeSemicolon, - vcFormatSpaceInsertAfterSemicolon: settings.vcFormatSpaceInsertAfterSemicolon, - vcFormatSpaceRemoveAroundUnaryOperator: settings.vcFormatSpaceRemoveAroundUnaryOperator, - vcFormatSpaceAroundBinaryOperator: settings.vcFormatSpaceAroundBinaryOperator, - vcFormatSpaceAroundAssignmentOperator: settings.vcFormatSpaceAroundAssignmentOperator, - vcFormatSpacePointerReferenceAlignment: settings.vcFormatSpacePointerReferenceAlignment, - vcFormatSpaceAroundTernaryOperator: settings.vcFormatSpaceAroundTernaryOperator, - vcFormatWrapPreserveBlocks: settings.vcFormatWrapPreserveBlocks, - doxygenGenerateOnType: settings.doxygenGenerateOnType, - doxygenGeneratedStyle: settings.doxygenGeneratedCommentStyle, - doxygenSectionTags: settings.doxygenSectionTags, - filesExclude: otherSettings.filesExclude, - filesAutoSaveAfterDelay: otherSettings.filesAutoSaveAfterDelay, - filesEncoding: otherSettings.filesEncoding, - searchExclude: otherSettings.searchExclude, - editorAutoClosingBrackets: otherSettings.editorAutoClosingBrackets, - editorInlayHintsEnabled: otherSettings.editorInlayHintsEnabled, - editorParameterHintsEnabled: otherSettings.editorParameterHintsEnabled, - refactoringIncludeHeader: settings.refactoringIncludeHeader - }; - return result; - } - - private getAllWorkspaceFolderSettings(): WorkspaceFolderSettingsParams[] { - const workspaceSettings: CppSettings = new CppSettings(); - const workspaceOtherSettings: OtherSettings = new OtherSettings(); - const workspaceFolderSettingsParams: WorkspaceFolderSettingsParams[] = []; - if (vscode.workspace.workspaceFolders && vscode.workspace.workspaceFolders.length > 0) { - for (const workspaceFolder of vscode.workspace.workspaceFolders) { - workspaceFolderSettingsParams.push(this.getWorkspaceFolderSettings(workspaceFolder.uri, new CppSettings(workspaceFolder.uri), new OtherSettings(workspaceFolder.uri))); - } - } else { - workspaceFolderSettingsParams.push(this.getWorkspaceFolderSettings(this.RootUri, workspaceSettings, workspaceOtherSettings)); - } - return workspaceFolderSettingsParams; - } - - private getAllSettings(): SettingsParams { - const workspaceSettings: CppSettings = new CppSettings(); - const workspaceOtherSettings: OtherSettings = new OtherSettings(); - const workspaceFolderSettingsParams: WorkspaceFolderSettingsParams[] = this.getAllWorkspaceFolderSettings(); - if (this.currentCaseSensitiveFileSupport && workspaceSettings.isCaseSensitiveFileSupportEnabled !== this.currentCaseSensitiveFileSupport.Value) { - void util.promptForReloadWindowDueToSettingsChange(); - } - return { - filesAssociations: workspaceOtherSettings.filesAssociations, - workspaceFallbackEncoding: workspaceOtherSettings.filesEncoding, - maxConcurrentThreads: workspaceSettings.maxConcurrentThreads, - maxCachedProcesses: workspaceSettings.maxCachedProcesses, - maxMemory: workspaceSettings.maxMemory, - maxSymbolSearchResults: workspaceSettings.maxSymbolSearchResults, - loggingLevel: workspaceSettings.loggingLevel, - workspaceParsingPriority: workspaceSettings.workspaceParsingPriority, - workspaceSymbols: workspaceSettings.workspaceSymbols, - simplifyStructuredComments: workspaceSettings.simplifyStructuredComments, - intelliSenseUpdateDelay: workspaceSettings.intelliSenseUpdateDelay, - experimentalFeatures: workspaceSettings.experimentalFeatures, - enhancedColorization: workspaceSettings.isEnhancedColorizationEnabled, - intellisenseMaxCachedProcesses: workspaceSettings.intelliSenseMaxCachedProcesses, - intellisenseMaxMemory: workspaceSettings.intelliSenseMaxMemory, - referencesMaxConcurrentThreads: workspaceSettings.referencesMaxConcurrentThreads, - referencesMaxCachedProcesses: workspaceSettings.referencesMaxCachedProcesses, - referencesMaxMemory: workspaceSettings.referencesMaxMemory, - codeAnalysisMaxConcurrentThreads: workspaceSettings.codeAnalysisMaxConcurrentThreads, - codeAnalysisMaxMemory: workspaceSettings.codeAnalysisMaxMemory, - codeAnalysisUpdateDelay: workspaceSettings.codeAnalysisUpdateDelay, - workspaceFolderSettings: workspaceFolderSettingsParams - }; - } - - private async createLanguageClient(): Promise { - this.currentCaseSensitiveFileSupport = new PersistentWorkspaceState("CPP.currentCaseSensitiveFileSupport", false); - let resetDatabase: boolean = false; - const serverModule: string = getLanguageServerFileName(); - const exeExists: boolean = fs.existsSync(serverModule); - if (!exeExists) { - telemetry.logLanguageServerEvent("missingLanguageServerBinary"); - throw String('Missing binary at ' + serverModule); - } - const serverName: string = this.getName(this.rootFolder); - const serverOptions: ServerOptions = { - run: { command: serverModule, options: { detached: false, cwd: util.getExtensionFilePath("bin") } }, - debug: { command: serverModule, args: [serverName], options: { detached: true, cwd: util.getExtensionFilePath("bin") } } - }; - - // The IntelliSense process should automatically detect when AutoPCH is - // not supportable (on platforms that don't support disabling ASLR/PIE). - // We've had reports of issues on arm64 macOS that are addressed by - // disabling the IntelliSense cache, suggesting fallback does not - // always work as expected. It's actually more efficient to disable - // the cache on platforms we know do not support it. We do that here. - let intelliSenseCacheDisabled: boolean = false; - if (os.platform() === "darwin") { - // AutoPCH doesn't work for arm64 macOS. - if (os.arch() === "arm64") { - intelliSenseCacheDisabled = true; - } else { - // AutoPCH doesn't work for older x64 macOS's. - const releaseParts: string[] = os.release().split("."); - if (releaseParts.length >= 1) { - intelliSenseCacheDisabled = parseInt(releaseParts[0]) < 17; - } - } - } else { - // AutoPCH doesn't work for arm64 Windows. - intelliSenseCacheDisabled = os.platform() === "win32" && os.arch() === "arm64"; - } - - const localizedStrings: string[] = []; - for (let i: number = 0; i < localizedStringCount; i++) { - localizedStrings.push(lookupString(i)); - } - - const workspaceSettings: CppSettings = new CppSettings(); - if (workspaceSettings.isCaseSensitiveFileSupportEnabled !== this.currentCaseSensitiveFileSupport.Value) { - resetDatabase = true; - this.currentCaseSensitiveFileSupport.Value = workspaceSettings.isCaseSensitiveFileSupportEnabled; - } - - const cacheStoragePath: string = util.getCacheStoragePath(); - const databaseStoragePath: string = (cacheStoragePath.length > 0) && (workspaceHash.length > 0) ? - path.join(cacheStoragePath, workspaceHash) : ""; - - const cppInitializationParams: CppInitializationParams = { - packageVersion: util.packageJson.version, - extensionPath: util.extensionPath, - databaseStoragePath: databaseStoragePath, - workspaceStoragePath: this.workspaceStoragePath, - cacheStoragePath: cacheStoragePath, - vcpkgRoot: util.getVcpkgRoot(), - intelliSenseCacheDisabled: intelliSenseCacheDisabled, - caseSensitiveFileSupport: workspaceSettings.isCaseSensitiveFileSupportEnabled, - resetDatabase: resetDatabase, - edgeMessagesDirectory: path.join(util.getExtensionFilePath("bin"), "messages", getLocaleId()), - localizedStrings: localizedStrings, - settings: this.getAllSettings() - }; - - this.loggingLevel = util.getNumericLoggingLevel(cppInitializationParams.settings.loggingLevel); - const lspInitializationOptions: LspInitializationOptions = { - loggingLevel: this.loggingLevel - }; - - const clientOptions: LanguageClientOptions = { - documentSelector: [ - { scheme: 'file', language: 'c' }, - { scheme: 'file', language: 'cpp' }, - { scheme: 'file', language: 'cuda-cpp' } - ], - initializationOptions: lspInitializationOptions, - middleware: createProtocolFilter(), - errorHandler: { - error: (_error, _message, _count) => ({ action: ErrorAction.Continue }), - closed: () => { - languageClientCrashTimes.push(Date.now()); - languageClientCrashedNeedsRestart = true; - telemetry.logLanguageServerEvent("languageClientCrash"); - let restart: boolean = true; - if (languageClientCrashTimes.length < 5) { - void clients.recreateClients(); - } else { - const elapsed: number = languageClientCrashTimes[languageClientCrashTimes.length - 1] - languageClientCrashTimes[0]; - if (elapsed <= 3 * 60 * 1000) { - void clients.recreateClients(true); - restart = false; - } else { - languageClientCrashTimes.shift(); - void clients.recreateClients(); - } - } - const message: string = restart ? localize('server.crashed.restart', 'The language server crashed. Restarting...') - : localize('server.crashed2', 'The language server crashed 5 times in the last 3 minutes. It will not be restarted.'); - - // We manually restart the language server so tell the LanguageClient not to do it automatically for us. - return { action: CloseAction.DoNotRestart, message }; - } - } - - // TODO: should I set the output channel? Does this sort output between servers? - }; - - // Create the language client - languageClient = new LanguageClient(`cpptools`, serverOptions, clientOptions); - languageClient.onNotification(DebugProtocolNotification, logDebugProtocol); - languageClient.onNotification(DebugLogNotification, logLocalized); - languageClient.registerProposedFeatures(); - await languageClient.start(); - - if (usesCrashHandler()) { - watchForCrashes(await languageClient.sendRequest(PreInitializationRequest, null)); - } - - // Move initialization to a separate message, so we can see log output from it. - // A request is used in order to wait for completion and ensure that no subsequent - // higher priority message may be processed before the Initialization request. - await languageClient.sendRequest(InitializationRequest, cppInitializationParams); - } - - public async sendDidChangeSettings(): Promise { - // Send settings json to native side - await this.ready; - await this.languageClient.sendNotification(DidChangeSettingsNotification, this.getAllSettings()); - } - - public async onDidChangeSettings(_event: vscode.ConfigurationChangeEvent): Promise> { - const defaultClient: Client = clients.getDefaultClient(); - if (this === defaultClient) { - // Only send the updated settings information once, as it includes values for all folders. - void this.sendDidChangeSettings(); - } - const changedSettings: Record = this.settingsTracker.getChangedSettings(); - - await this.ready; - - if (Object.keys(changedSettings).length > 0) { - if (this === defaultClient) { - if (changedSettings.commentContinuationPatterns) { - updateLanguageConfigurations(); - } - if (changedSettings.loggingLevel) { - const oldLoggingLevelLogged: boolean = this.loggingLevel > 1; - this.loggingLevel = util.getNumericLoggingLevel(changedSettings.loggingLevel); - if (oldLoggingLevelLogged || this.loggingLevel > 1) { - const out: Logger = getOutputChannelLogger(); - out.appendLine(localize({ key: "loggingLevel.changed", comment: ["{0} is the setting name 'loggingLevel', {1} is a string value such as 'Debug'"] }, "{0} has changed to: {1}", "loggingLevel", changedSettings.loggingLevel)); - } - } - const settings: CppSettings = new CppSettings(); - if (changedSettings.enhancedColorization) { - if (settings.isEnhancedColorizationEnabled && semanticTokensLegend) { - this.semanticTokensProvider = new SemanticTokensProvider(); - this.semanticTokensProviderDisposable = vscode.languages.registerDocumentSemanticTokensProvider(util.documentSelector, this.semanticTokensProvider, semanticTokensLegend); - } else if (this.semanticTokensProviderDisposable) { - this.semanticTokensProviderDisposable.dispose(); - this.semanticTokensProviderDisposable = undefined; - this.semanticTokensProvider = undefined; - } - } - - // If an inlay hints setting has changed, force an inlay provider update on the visible documents. - if (["inlayHints.autoDeclarationTypes.enabled", - "inlayHints.autoDeclarationTypes.showOnLeft", - "inlayHints.parameterNames.enabled", - "inlayHints.parameterNames.hideLeadingUnderscores", - "inlayHints.parameterNames.suppressWhenArgumentContainsName", - "inlayHints.referenceOperator.enabled", - "inlayHints.referenceOperator.showSpace"].some(setting => setting in changedSettings)) { - vscode.window.visibleTextEditors.forEach((visibleEditor: vscode.TextEditor) => { - // The exact range doesn't matter. - const visibleRange: vscode.Range | undefined = visibleEditor.visibleRanges.at(0); - if (visibleRange !== undefined) { - void vscode.commands.executeCommand('vscode.executeInlayHintProvider', - visibleEditor.document.uri, visibleRange); - } - }); - } - - const showButtonSender: string = "settingsChanged"; - if (changedSettings["default.configurationProvider"] !== undefined) { - void ui.ShowConfigureIntelliSenseButton(false, this, ConfigurationType.ConfigProvider, showButtonSender); - } else if (changedSettings["default.compileCommands"] !== undefined) { - void ui.ShowConfigureIntelliSenseButton(false, this, ConfigurationType.CompileCommands, showButtonSender); - } if (changedSettings["default.compilerPath"] !== undefined) { - void ui.ShowConfigureIntelliSenseButton(false, this, ConfigurationType.CompilerPath, showButtonSender); - } - } - if (changedSettings.legacyCompilerArgsBehavior) { - this.configuration.handleConfigurationChange(); - } - if (changedSettings["default.compilerPath"] !== undefined || changedSettings["default.compileCommands"] !== undefined || changedSettings["default.configurationProvider"] !== undefined) { - void ui.ShowConfigureIntelliSenseButton(false, this).catch(logAndReturn.undefined); - } - this.configuration.onDidChangeSettings(); - telemetry.logLanguageServerEvent("CppSettingsChange", changedSettings, undefined); - } - - return changedSettings; - } - - private prepareVisibleRanges(editors: readonly vscode.TextEditor[]): { [uri: string]: Range[] } { - const visibleRanges: { [uri: string]: Range[] } = {}; - editors.forEach(editor => { - // Use a map, to account for multiple editors for the same file. - // First, we just concat all ranges for the same file. - const uri: string = editor.document.uri.toString(); - if (!visibleRanges[uri]) { - visibleRanges[uri] = []; - } - visibleRanges[uri] = visibleRanges[uri].concat(editor.visibleRanges.map(makeLspRange)); - }); - - // We may need to merge visible ranges, if there are multiple editors for the same file, - // and some of the ranges overlap. - Object.keys(visibleRanges).forEach(uri => { - visibleRanges[uri] = util.mergeOverlappingRanges(visibleRanges[uri]); - }); - - return visibleRanges; - } - - // Handles changes to visible files/ranges, changes to current selection/position, - // and changes to the active text editor. Should only be called on the primary client. - public async onDidChangeVisibleTextEditors(editors: readonly vscode.TextEditor[]): Promise { - const params: DidChangeVisibleTextEditorsParams = { - visibleRanges: this.prepareVisibleRanges(editors) - }; - if (vscode.window.activeTextEditor) { - if (util.isCpp(vscode.window.activeTextEditor.document)) { - params.activeUri = vscode.window.activeTextEditor.document.uri.toString(); - params.activeSelection = makeLspRange(vscode.window.activeTextEditor.selection); - } - } - - await this.languageClient.sendNotification(DidChangeVisibleTextEditorsNotification, params); - } - - public async onDidChangeTextEditorVisibleRanges(uri: vscode.Uri): Promise { - // VS Code will notify us of a particular editor, but same file may be open in - // multiple editors, so we coalesc those visible ranges. - const editors: vscode.TextEditor[] = vscode.window.visibleTextEditors.filter(editor => editor.document.uri === uri); - - let visibleRanges: Range[] = []; - if (editors.length === 1) { - visibleRanges = editors[0].visibleRanges.map(makeLspRange); - } else { - editors.forEach(editor => { - // Use a map, to account for multiple editors for the same file. - // First, we just concat all ranges for the same file. - visibleRanges = visibleRanges.concat(editor.visibleRanges.map(makeLspRange)); - }); - } - - const params: DidChangeTextEditorVisibleRangesParams = { - uri: uri.toString(), - visibleRanges - }; - - await this.languageClient.sendNotification(DidChangeTextEditorVisibleRangesNotification, params); - } - - public onDidChangeTextDocument(textDocumentChangeEvent: vscode.TextDocumentChangeEvent): void { - if (util.isCpp(textDocumentChangeEvent.document)) { - // If any file has changed, we need to abort the current rename operation - if (workspaceReferences.renamePending) { - workspaceReferences.cancelCurrentReferenceRequest(refs.CancellationSender.User); - } - - const oldVersion: number | undefined = openFileVersions.get(textDocumentChangeEvent.document.uri.toString()); - const newVersion: number = textDocumentChangeEvent.document.version; - if (oldVersion === undefined || newVersion > oldVersion) { - openFileVersions.set(textDocumentChangeEvent.document.uri.toString(), newVersion); - } - } - } - - public onDidOpenTextDocument(document: vscode.TextDocument): void { - if (document.uri.scheme === "file") { - const uri: string = document.uri.toString(); - openFileVersions.set(uri, document.version); - void SessionState.buildAndDebugIsSourceFile.set(util.isCppOrCFile(document.uri)); - void SessionState.buildAndDebugIsFolderOpen.set(util.isFolderOpen(document.uri)); - } else { - void SessionState.buildAndDebugIsSourceFile.set(false); - } - } - - public onDidCloseTextDocument(document: vscode.TextDocument): void { - const uri: string = document.uri.toString(); - if (this.semanticTokensProvider) { - this.semanticTokensProvider.removeFile(uri); - } - if (this.inlayHintsProvider) { - this.inlayHintsProvider.removeFile(uri); - } - this.inactiveRegionsDecorations.delete(uri); - if (diagnosticsCollectionIntelliSense) { - diagnosticsCollectionIntelliSense.delete(document.uri); - } - openFileVersions.delete(uri); - } - - public isProviderRegistered(extensionId: string | undefined): boolean { - if (extensionId === undefined || this.registeredProviders === undefined) { - return false; - } - for (const provider of this.registeredProviders.Value) { - if (provider === extensionId) { - return true; - } - } - return false; - } - - private async onCustomConfigurationProviderRegistered(provider: CustomConfigurationProvider1): Promise { - // version 2 providers control the browse.path. Avoid thrashing the tag parser database by pausing parsing until - // the provider has sent the correct browse.path value. - if (provider.version >= Version.v2) { - return this.pauseParsing(); - } - } - - public async onRegisterCustomConfigurationProvider(provider: CustomConfigurationProvider1): Promise { - await this.ready; - - if (this.registeredProviders === undefined // Shouldn't happen. - // Prevent duplicate processing. - || this.registeredProviders.Value.includes(provider.extensionId)) { - return; - } - this.registeredProviders.Value.push(provider.extensionId); - const rootFolder: vscode.WorkspaceFolder | undefined = this.RootFolder; - if (!rootFolder) { - return; // There is no c_cpp_properties.json to edit because there is no folder open. - } - this.configuration.handleConfigurationChange(); - if (this.configStateReceived.configProviders === undefined) { - this.configStateReceived.configProviders = []; - } - this.configStateReceived.configProviders.push(provider); - const selectedProvider: string | undefined = this.configuration.CurrentConfigurationProvider; - if (!selectedProvider || this.showConfigureIntelliSenseButton) { - void this.handleConfigStatus("configProviders"); - if (!selectedProvider) { - return; - } - } - if (isSameProviderExtensionId(selectedProvider, provider.extensionId)) { - void this.onCustomConfigurationProviderRegistered(provider).catch(logAndReturn.undefined); - telemetry.logLanguageServerEvent("customConfigurationProvider", { "providerId": provider.extensionId }); - } else if (selectedProvider === provider.name) { - void this.onCustomConfigurationProviderRegistered(provider).catch(logAndReturn.undefined); - await this.configuration.updateCustomConfigurationProvider(provider.extensionId); // v0 -> v1 upgrade. Update the configurationProvider in c_cpp_properties.json - } - } - - public async updateCustomConfigurations(requestingProvider?: CustomConfigurationProvider1): Promise { - await this.ready; - - if (!this.configurationProvider) { - return this.clearCustomConfigurations(); - } - const currentProvider: CustomConfigurationProvider1 | undefined = getCustomConfigProviders().get(this.configurationProvider); - if (!currentProvider) { - return this.clearCustomConfigurations(); - } - if (requestingProvider && requestingProvider.extensionId !== currentProvider.extensionId) { - // If we are being called by a configuration provider other than the current one, ignore it. - return; - } - if (!currentProvider.isReady) { - return; - } - - await this.clearCustomConfigurations(); - } - - public async updateCustomBrowseConfiguration(requestingProvider?: CustomConfigurationProvider1): Promise { - await this.ready; - - if (!this.configurationProvider) { - return; - } - - const currentProvider: CustomConfigurationProvider1 | undefined = getCustomConfigProviders().get(this.configurationProvider); - if (!currentProvider || !currentProvider.isReady || (requestingProvider && requestingProvider.extensionId !== currentProvider.extensionId)) { - return; - } - - const tokenSource: vscode.CancellationTokenSource = new vscode.CancellationTokenSource(); - let config: WorkspaceBrowseConfiguration | null = null; - let hasCompleted: boolean = false; - try { - if (this.RootUri && await currentProvider.canProvideBrowseConfigurationsPerFolder(tokenSource.token)) { - config = await currentProvider.provideFolderBrowseConfiguration(this.RootUri, tokenSource.token); - } else if (await currentProvider.canProvideBrowseConfiguration(tokenSource.token)) { - config = await currentProvider.provideBrowseConfiguration(tokenSource.token); - } else if (currentProvider.version >= Version.v2) { - console.warn("failed to provide browse configuration"); - } - } catch { - if (!hasCompleted) { - hasCompleted = true; - if (currentProvider.version >= Version.v2) { - await this.resumeParsing(); - } - } - } - - // Initiate request for custom configuration. - // Resume parsing on either resolve or reject, only if parsing was not resumed due to timeout - - if (config) { - if (currentProvider.version < Version.v3) { - // This is to get around the (fixed) CMake Tools bug: https://github.com/microsoft/vscode-cmake-tools/issues/1073 - for (const c of config.browsePath) { - if (vscode.workspace.getWorkspaceFolder(vscode.Uri.file(c)) === this.RootFolder) { - this.sendCustomBrowseConfiguration(config, currentProvider.extensionId, currentProvider.version); - break; - } - } - } else { - this.sendCustomBrowseConfiguration(config, currentProvider.extensionId, currentProvider.version); - } - if (!hasCompleted) { - hasCompleted = true; - if (currentProvider.version >= Version.v2) { - await this.resumeParsing(); - } - } - } - - // Set up a timeout to use previously received configuration and resume parsing if the provider times out - global.setTimeout(() => { - if (!hasCompleted) { - hasCompleted = true; - this.sendCustomBrowseConfiguration(null, undefined, Version.v0, true); - if (currentProvider.version >= Version.v2) { - console.warn(`Configuration Provider timed out in ${configProviderTimeout}ms.`); - void this.resumeParsing().catch(logAndReturn.undefined); - } - } - }, configProviderTimeout); - - } - - public toggleReferenceResultsView(): void { - workspaceReferences.toggleGroupView(); - } - - public async logDiagnostics(): Promise { - await this.ready; - const response: GetDiagnosticsResult = await this.languageClient.sendRequest(GetDiagnosticsRequest, null); - const diagnosticsChannel: vscode.OutputChannel = getDiagnosticsChannel(); - diagnosticsChannel.clear(); - - const header: string = `-------- Diagnostics - ${new Date().toLocaleString()}\n`; - const version: string = `Version: ${util.packageJson.version}\n`; - let configJson: string = ""; - if (this.configuration.CurrentConfiguration) { - configJson = `Current Configuration:\n${JSON.stringify(this.configuration.CurrentConfiguration, null, 4)}\n`; - } - const userModifiedSettings = Object.entries(this.settingsTracker.getUserModifiedSettings()); - if (userModifiedSettings.length > 0) { - const settings: Record = {}; - for (const [key] of userModifiedSettings) { - // Some settings were renamed during a telemetry change, so we need to undo that here. - const realKey = key.endsWith('2') ? key.slice(0, key.length - 1) : key; - const fullKey = `C_Cpp.${realKey}`; - settings[fullKey] = vscode.workspace.getConfiguration("C_Cpp").get(realKey) ?? ''; - } - configJson += `Modified Settings:\n${JSON.stringify(settings, null, 4)}\n`; - } - - { - const editorSettings = new OtherSettings(this.RootUri); - const settings: Record = {}; - settings.editorTabSize = editorSettings.editorTabSize; - settings.editorInsertSpaces = editorSettings.editorInsertSpaces; - settings.editorAutoClosingBrackets = editorSettings.editorAutoClosingBrackets; - settings.filesEncoding = editorSettings.filesEncoding; - settings.filesAssociations = editorSettings.filesAssociations; - settings.filesExclude = editorSettings.filesExclude; - settings.filesAutoSaveAfterDelay = editorSettings.filesAutoSaveAfterDelay; - settings.editorInlayHintsEnabled = editorSettings.editorInlayHintsEnabled; - settings.editorParameterHintsEnabled = editorSettings.editorParameterHintsEnabled; - settings.searchExclude = editorSettings.searchExclude; - settings.workbenchSettingsEditor = editorSettings.workbenchSettingsEditor; - configJson += `Additional Tracked Settings:\n${JSON.stringify(settings, null, 4)}\n`; - } - - // Get diagnostics for configuration provider info. - let configurationLoggingStr: string = ""; - const tuSearchStart: number = response.diagnostics.indexOf("Translation Unit Mappings:"); - if (tuSearchStart >= 0) { - const tuSearchEnd: number = response.diagnostics.indexOf("Translation Unit Configurations:"); - if (tuSearchEnd >= 0 && tuSearchEnd > tuSearchStart) { - let tuSearchString: string = response.diagnostics.substring(tuSearchStart, tuSearchEnd); - let tuSearchIndex: number = tuSearchString.indexOf("["); - while (tuSearchIndex >= 0) { - const tuMatch: RegExpMatchArray | null = tuSearchString.match(/\[\s(.*)\s\]/); - if (tuMatch && tuMatch.length > 1) { - const tuPath: string = vscode.Uri.file(tuMatch[1]).toString(); - if (this.configurationLogging.has(tuPath)) { - if (configurationLoggingStr.length === 0) { - configurationLoggingStr += "Custom configurations:\n"; - } - configurationLoggingStr += `[ ${tuMatch[1]} ]\n${this.configurationLogging.get(tuPath)}\n`; - } - } - tuSearchString = tuSearchString.substring(tuSearchIndex + 1); - tuSearchIndex = tuSearchString.indexOf("["); - } - } - } - diagnosticsChannel.appendLine(`${header}${version}${configJson}${this.browseConfigurationLogging}${configurationLoggingStr}${response.diagnostics}`); - diagnosticsChannel.show(false); - } - - public async rescanFolder(): Promise { - await this.ready; - return this.languageClient.sendNotification(RescanFolderNotification); - } - - public async provideCustomConfiguration(docUri: vscode.Uri): Promise { - const onFinished: () => void = () => { - void this.languageClient.sendNotification(FinishedRequestCustomConfig, { uri: docUri.toString() }); - }; - try { - const providerId: string | undefined = this.configurationProvider; - if (!providerId) { - return; - } - const provider: CustomConfigurationProvider1 | undefined = getCustomConfigProviders().get(providerId); - if (!provider || !provider.isReady) { - return; - } - const resultCode = await this.provideCustomConfigurationAsync(docUri, provider); - telemetry.logLanguageServerEvent('provideCustomConfiguration', { providerId, resultCode }); - } finally { - onFinished(); - } - } - - private async provideCustomConfigurationAsync(docUri: vscode.Uri, provider: CustomConfigurationProvider1): Promise { - const tokenSource: vscode.CancellationTokenSource = new vscode.CancellationTokenSource(); - - // Need to loop through candidates, to see if we can get a custom configuration from any of them. - // Wrap all lookups in a single task, so we can apply a timeout to the entire duration. - const provideConfigurationAsync: () => Thenable = async () => { - try { - if (!await provider.canProvideConfiguration(docUri, tokenSource.token)) { - return []; - } - } catch (err) { - console.warn("Caught exception from canProvideConfiguration"); - } - let configs: util.Mutable[] = []; - try { - configs = await provider.provideConfigurations([docUri], tokenSource.token); - } catch (err) { - console.warn("Caught exception from provideConfigurations"); - } - - if (configs && configs.length > 0 && configs[0]) { - const fileConfiguration: configs.Configuration | undefined = this.configuration.CurrentConfiguration; - if (fileConfiguration?.mergeConfigurations) { - configs.forEach(config => { - if (fileConfiguration.includePath) { - fileConfiguration.includePath.forEach(p => { - if (!config.configuration.includePath.includes(p)) { - config.configuration.includePath.push(p); - } - }); - } - - if (fileConfiguration.defines) { - fileConfiguration.defines.forEach(d => { - if (!config.configuration.defines.includes(d)) { - config.configuration.defines.push(d); - } - }); - } - - if (!config.configuration.forcedInclude) { - config.configuration.forcedInclude = []; - } - - if (fileConfiguration.forcedInclude) { - fileConfiguration.forcedInclude.forEach(i => { - if (config.configuration.forcedInclude) { - if (!config.configuration.forcedInclude.includes(i)) { - config.configuration.forcedInclude.push(i); - } - } - }); - } - }); - } - return configs as SourceFileConfigurationItem[]; - } - return undefined; - }; - let result: string = "success"; - try { - const configs: SourceFileConfigurationItem[] | undefined = await this.callTaskWithTimeout(provideConfigurationAsync, configProviderTimeout, tokenSource); - if (configs && configs.length > 0) { - this.sendCustomConfigurations(configs, provider.version); - } else { - result = "noConfigurations"; - } - } catch (err) { - result = "timeout"; - const settings: CppSettings = new CppSettings(this.RootUri); - if (settings.isConfigurationWarningsEnabled && !this.isExternalHeader(docUri) && !vscode.debug.activeDebugSession) { - const dismiss: string = localize("dismiss.button", "Dismiss"); - const disable: string = localize("disable.warnings.button", "Disable Warnings"); - const configName: string | undefined = this.configuration.CurrentConfiguration?.name; - if (!configName) { - return "noConfigName"; - } - let message: string = localize("unable.to.provide.configuration", - "{0} is unable to provide IntelliSense configuration information for '{1}'. Settings from the '{2}' configuration will be used instead.", - provider.name, docUri.fsPath, configName); - if (err) { - message += ` (${err})`; - } - - if (await vscode.window.showInformationMessage(message, dismiss, disable) === disable) { - settings.toggleSetting("configurationWarnings", "enabled", "disabled"); - } - } - } - return result; - } - - private handleRequestCustomConfig(file: string): void { - const uri: vscode.Uri = vscode.Uri.file(file); - const client: Client = clients.getClientFor(uri); - if (client instanceof DefaultClient) { - const defaultClient: DefaultClient = client as DefaultClient; - void defaultClient.provideCustomConfiguration(uri).catch(logAndReturn.undefined); - } - } - - private isExternalHeader(uri: vscode.Uri): boolean { - const rootUri: vscode.Uri | undefined = this.RootUri; - return !rootUri || (util.isHeaderFile(uri) && !uri.toString().startsWith(rootUri.toString())); - } - - public async getCurrentConfigName(): Promise { - await this.ready; - return this.configuration.CurrentConfiguration?.name; - } - - public async getCurrentConfigCustomVariable(variableName: string): Promise { - await this.ready; - return this.configuration.CurrentConfiguration?.customConfigurationVariables?.[variableName] ?? ''; - } - - public async setCurrentConfigName(configurationName: string): Promise { - await this.ready; - - const configurations: configs.Configuration[] = this.configuration.Configurations ?? []; - const configurationIndex: number = configurations.findIndex((config) => config.name === configurationName); - - if (configurationIndex === -1) { - throw new Error(localize("config.not.found", "The requested configuration name is not found: {0}", configurationName)); - } - this.configuration.select(configurationIndex); - } - - public async getCurrentCompilerPathAndArgs(): Promise { - const settings: CppSettings = new CppSettings(this.RootUri); - await this.ready; - return util.extractCompilerPathAndArgs(!!settings.legacyCompilerArgsBehavior, - this.configuration.CurrentConfiguration?.compilerPath, - this.configuration.CurrentConfiguration?.compilerArgs); - - } - - public async getVcpkgInstalled(): Promise { - await this.ready; - return this.configuration.VcpkgInstalled; - } - - public getVcpkgEnabled(): Promise { - const cppSettings: CppSettings = new CppSettings(this.RootUri); - return Promise.resolve(cppSettings.vcpkgEnabled); - } - - public async getKnownCompilers(): Promise { - await this.ready; - return this.configuration.KnownCompiler; - } - - /** - * Take ownership of a document that was previously serviced by another client. - * This process involves sending a textDocument/didOpen message to the server so - * that it knows about the file, as well as adding it to this client's set of - * tracked documents. - */ - public takeOwnership(document: vscode.TextDocument): void { - this.trackedDocuments.set(document.uri.toString(), document); - } - - // Only used in crash recovery. Otherwise, VS Code sends didOpen directly to native process (through the protocolFilter). - public async sendDidOpen(document: vscode.TextDocument): Promise { - const params: DidOpenTextDocumentParams = { - textDocument: { - uri: document.uri.toString(), - languageId: document.languageId, - version: document.version, - text: document.getText() - } - }; - await this.ready; - await this.languageClient.sendNotification(DidOpenNotification, params); - } - - public async getIncludes(maxDepth: number, token: vscode.CancellationToken): Promise { - const params: GetIncludesParams = { maxDepth: maxDepth }; - await this.ready; - return DefaultClient.withLspCancellationHandling( - () => this.languageClient.sendRequest(IncludesRequest, params, token), token); - } - - public async getChatContext(token: vscode.CancellationToken): Promise { - await withCancellation(this.ready, token); - return DefaultClient.withLspCancellationHandling( - () => this.languageClient.sendRequest(CppContextRequest, null, token), token); - } - - /** - * a Promise that can be awaited to know when it's ok to proceed. - * - * This is a lighter-weight complement to `enqueue()` - * - * Use `await .ready` when you need to ensure that the client is initialized, and to run in order - * Use `enqueue()` when you want to ensure that subsequent calls are blocked until a critical bit of code is run. - * - * This is lightweight, because if the queue is empty, then the only thing to wait for is the client itself to be initialized - */ - get ready(): Promise { - if (!DefaultClient.dispatching.isCompleted || DefaultClient.queue.length) { - // if the dispatcher has stuff going on, then we need to stick in a promise into the queue so we can - // be notified when it's our turn - const p = new ManualPromise(); - DefaultClient.queue.push([p as ManualPromise]); - return p; - } - - // otherwise, we're only waiting for the client to be in an initialized state, in which case just wait for that. - return DefaultClient.isStarted; - } - - /** - * Enqueue a task to ensure that the order is maintained. The tasks are executed sequentially after the client is ready. - * - * this is a bit more expensive than `.ready` - this ensures the task is absolutely finished executing before allowing - * the dispatcher to move forward. - * - * Use `enqueue()` when you want to ensure that subsequent calls are blocked until a critical bit of code is run. - * Use `await .ready` when you need to ensure that the client is initialized, and still run in order. - */ - enqueue(task: () => Promise) { - ok(this.isSupported, localize("unsupported.client", "Unsupported client")); - - // create a placeholder promise that is resolved when the task is complete. - const result = new ManualPromise(); - - // add the task to the queue - DefaultClient.queue.push([result, task]); - - // if we're not already dispatching, start - if (DefaultClient.dispatching.isSet) { - // start dispatching - void DefaultClient.dispatch(); - } - - // return the placeholder promise to the caller. - return result as Promise; - } - - /** - * The dispatch loop asynchronously processes items in the async queue in order, and ensures that tasks are dispatched in the - * order they were inserted. - */ - private static async dispatch() { - // reset the promise for the dispatcher - DefaultClient.dispatching.reset(); - - do { - // ensure that this is OK to start working - await this.isStarted; - - // pick items up off the queue and run then one at a time until the queue is empty - const [promise, task] = DefaultClient.queue.shift() ?? []; - if (is.promise(promise)) { - try { - promise.resolve(task ? await task() : undefined); - } catch (e) { - console.log(e); - promise.reject(e); - } - } - } while (DefaultClient.queue.length); - - // unblock anything that is waiting for the dispatcher to empty - this.dispatching.resolve(); - } - - private static async withLspCancellationHandling(task: () => Promise, token: vscode.CancellationToken): Promise { - let result: T; - - try { - result = await task(); - } catch (e: any) { - if (e instanceof ResponseError && (e.code === RequestCancelled || e.code === ServerCancelled)) { - throw new vscode.CancellationError(); - } else { - throw e; - } - } - - if (token.isCancellationRequested) { - throw new vscode.CancellationError(); - } - - return result; - } - - private callTaskWithTimeout(task: () => Thenable, ms: number, cancelToken?: vscode.CancellationTokenSource): Promise { - let timer: NodeJS.Timeout; - - // Create a promise that rejects in milliseconds - const timeout: () => Promise = () => new Promise((resolve, reject) => { - timer = global.setTimeout(() => { - clearTimeout(timer); - if (cancelToken) { - cancelToken.cancel(); - } - reject(localize("timed.out", "Timed out in {0}ms.", ms)); - }, ms); - }); - - // Returns a race between our timeout and the passed in promise - return Promise.race([task(), timeout()]).then( - (result: any) => { - clearTimeout(timer); - return result; - }, - (error: any) => { - clearTimeout(timer); - throw error; - }); - } - - /** - * listen for notifications from the language server. - */ - private registerNotifications(): void { - console.assert(this.languageClient !== undefined, "This method must not be called until this.languageClient is set in \"onReady\""); - - this.languageClient.onNotification(ReloadWindowNotification, () => void util.promptForReloadWindowDueToSettingsChange()); - this.languageClient.onNotification(UpdateTrustedCompilersNotification, (e) => void this.addTrustedCompiler(e.compilerPath)); - this.languageClient.onNotification(LogTelemetryNotification, (e) => this.logTelemetry(e)); - this.languageClient.onNotification(ReportStatusNotification, (e) => void this.updateStatus(e)); - this.languageClient.onNotification(ReportTagParseStatusNotification, (e) => this.updateTagParseStatus(e)); - this.languageClient.onNotification(CompileCommandsPathsNotification, (e) => void this.promptCompileCommands(e)); - this.languageClient.onNotification(ReferencesNotification, (e) => this.processReferencesPreview(e)); - this.languageClient.onNotification(ReportReferencesProgressNotification, (e) => this.handleReferencesProgress(e)); - this.languageClient.onNotification(RequestCustomConfig, (e) => this.handleRequestCustomConfig(e)); - this.languageClient.onNotification(IntelliSenseResultNotification, (e) => this.handleIntelliSenseResult(e)); - this.languageClient.onNotification(PublishRefactorDiagnosticsNotification, publishRefactorDiagnostics); - RegisterCodeAnalysisNotifications(this.languageClient); - this.languageClient.onNotification(ShowMessageWindowNotification, showMessageWindow); - this.languageClient.onNotification(ShowWarningNotification, showWarning); - this.languageClient.onNotification(ReportTextDocumentLanguage, (e) => this.setTextDocumentLanguage(e)); - this.languageClient.onNotification(IntelliSenseSetupNotification, (e) => this.logIntelliSenseSetupTime(e)); - this.languageClient.onNotification(SetTemporaryTextDocumentLanguageNotification, (e) => void this.setTemporaryTextDocumentLanguage(e)); - this.languageClient.onNotification(ReportCodeAnalysisProcessedNotification, (e) => this.updateCodeAnalysisProcessed(e)); - this.languageClient.onNotification(ReportCodeAnalysisTotalNotification, (e) => this.updateCodeAnalysisTotal(e)); - this.languageClient.onNotification(DoxygenCommentGeneratedNotification, (e) => void this.insertDoxygenComment(e)); - this.languageClient.onNotification(CanceledReferencesNotification, this.serverCanceledReferences); - } - - private handleIntelliSenseResult(intelliSenseResult: IntelliSenseResult): void { - const fileVersion: number | undefined = openFileVersions.get(intelliSenseResult.uri); - if (fileVersion !== undefined && fileVersion !== intelliSenseResult.fileVersion) { - return; - } - - if (this.semanticTokensProvider) { - this.semanticTokensProvider.deliverTokens(intelliSenseResult.uri, intelliSenseResult.semanticTokens, intelliSenseResult.clearExistingSemanticTokens); - } - if (this.inlayHintsProvider) { - this.inlayHintsProvider.deliverInlayHints(intelliSenseResult.uri, intelliSenseResult.inlayHints, intelliSenseResult.clearExistingInlayHint); - } - - this.updateInactiveRegions(intelliSenseResult.uri, intelliSenseResult.inactiveRegions, intelliSenseResult.clearExistingInactiveRegions, intelliSenseResult.isCompletePass); - if (intelliSenseResult.clearExistingDiagnostics || intelliSenseResult.diagnostics.length > 0) { - this.updateSquiggles(intelliSenseResult.uri, intelliSenseResult.diagnostics, intelliSenseResult.clearExistingDiagnostics); - } - } - - private updateSquiggles(uriString: string, diagnostics: IntelliSenseDiagnostic[], startNewSet: boolean): void { - - if (!diagnosticsCollectionIntelliSense) { - diagnosticsCollectionIntelliSense = vscode.languages.createDiagnosticCollection(configPrefix + "IntelliSense"); - } - - // Convert from our Diagnostic objects to vscode Diagnostic objects - - const diagnosticsIntelliSense: vscode.Diagnostic[] = []; - diagnostics.forEach((d) => { - const message: string = getLocalizedString(d.localizeStringParams); - const diagnostic: vscode.Diagnostic = new vscode.Diagnostic(makeVscodeRange(d.range), message, d.severity); - diagnostic.code = d.code; - diagnostic.source = CppSourceStr; - if (d.relatedInformation) { - diagnostic.relatedInformation = []; - for (const info of d.relatedInformation) { - diagnostic.relatedInformation.push(new vscode.DiagnosticRelatedInformation(makeVscodeLocation(info.location), info.message)); - } - } - - diagnosticsIntelliSense.push(diagnostic); - }); - - const realUri: vscode.Uri = vscode.Uri.parse(uriString); - if (!startNewSet) { - const existingDiagnostics: readonly vscode.Diagnostic[] | undefined = diagnosticsCollectionIntelliSense.get(realUri); - if (existingDiagnostics) { - // Note: The spread operator puts every element on the stack, so it should be avoided for large arrays. - Array.prototype.push.apply(diagnosticsIntelliSense, existingDiagnostics as any[]); - } - } - diagnosticsCollectionIntelliSense.set(realUri, diagnosticsIntelliSense); - - clients.timeTelemetryCollector.setUpdateRangeTime(realUri); - } - - private setTextDocumentLanguage(languageStr: string): void { - const cppSettings: CppSettings = new CppSettings(); - if (cppSettings.autoAddFileAssociations) { - const is_c: boolean = languageStr.startsWith("c;"); - const is_cuda: boolean = languageStr.startsWith("cu;"); - languageStr = languageStr.substring(is_c ? 2 : is_cuda ? 3 : 1); - this.addFileAssociations(languageStr, is_c ? "c" : is_cuda ? "cuda-cpp" : "cpp"); - } - } - - private async setTemporaryTextDocumentLanguage(params: SetTemporaryTextDocumentLanguageParams): Promise { - const languageId: string = params.isC ? "c" : params.isCuda ? "cuda-cpp" : "cpp"; - const uri: vscode.Uri = vscode.Uri.parse(params.uri); - const client: Client = clients.getClientFor(uri); - const document: vscode.TextDocument | undefined = client.TrackedDocuments.get(params.uri); - if (!!document && document.languageId !== languageId) { - if (document.languageId === "cpp" && languageId === "c") { - handleChangedFromCppToC(document); - } - await vscode.languages.setTextDocumentLanguage(document, languageId); - } - } - - private associations_for_did_change?: Set; - - /** - * listen for file created/deleted events under the ${workspaceFolder} folder - */ - private registerFileWatcher(): void { - console.assert(this.languageClient !== undefined, "This method must not be called until this.languageClient is set in \"onReady\""); - - if (this.rootFolder) { - // WARNING: The default limit on Linux is 8k, so for big directories, this can cause file watching to fail. - this.rootPathFileWatcher = vscode.workspace.createFileSystemWatcher( - "**/*", - false /* ignoreCreateEvents */, - false /* ignoreChangeEvents */, - false /* ignoreDeleteEvents */); - - this.rootPathFileWatcher.onDidCreate(async (uri) => { - if (uri.scheme !== 'file') { - return; - } - const fileName: string = path.basename(uri.fsPath).toLowerCase(); - if (fileName === ".editorconfig") { - cachedEditorConfigSettings.clear(); - cachedEditorConfigLookups.clear(); - this.updateActiveDocumentTextOptions(); - } - if (fileName === ".clang-format" || fileName === "_clang-format") { - cachedEditorConfigLookups.clear(); - } - - void this.languageClient.sendNotification(FileCreatedNotification, { uri: uri.toString() }).catch(logAndReturn.undefined); - }); - - // TODO: Handle new associations without a reload. - this.associations_for_did_change = new Set(["cu", "cuh", "c", "i", "cpp", "cc", "cxx", "c++", "cp", "hpp", "hh", "hxx", "h++", "hp", "h", "ii", "ino", "inl", "ipp", "tcc", "idl"]); - const assocs: any = new OtherSettings().filesAssociations; - for (const assoc in assocs) { - const dotIndex: number = assoc.lastIndexOf('.'); - if (dotIndex !== -1) { - const ext: string = assoc.substring(dotIndex + 1); - this.associations_for_did_change.add(ext); - } - } - this.rootPathFileWatcher.onDidChange(async (uri) => { - if (uri.scheme !== 'file') { - return; - } - const dotIndex: number = uri.fsPath.lastIndexOf('.'); - const fileName: string = path.basename(uri.fsPath).toLowerCase(); - if (fileName === ".editorconfig") { - cachedEditorConfigSettings.clear(); - cachedEditorConfigLookups.clear(); - this.updateActiveDocumentTextOptions(); - } - if (dotIndex !== -1) { - const ext: string = uri.fsPath.substring(dotIndex + 1); - if (this.associations_for_did_change?.has(ext)) { - // 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. - // We workaround this via only sending the change message if the modified time is within 10 seconds. - const mtime: Date = fs.statSync(uri.fsPath).mtime; - const duration: number = Date.now() - mtime.getTime(); - if (duration < 10000) { - void this.languageClient.sendNotification(FileChangedNotification, { uri: uri.toString() }).catch(logAndReturn.undefined); - } - } - } - }); - - this.rootPathFileWatcher.onDidDelete((uri) => { - if (uri.scheme !== 'file') { - return; - } - const fileName: string = path.basename(uri.fsPath).toLowerCase(); - if (fileName === ".editorconfig") { - cachedEditorConfigSettings.clear(); - cachedEditorConfigLookups.clear(); - } - if (fileName === ".clang-format" || fileName === "_clang-format") { - cachedEditorConfigLookups.clear(); - } - void this.languageClient.sendNotification(FileDeletedNotification, { uri: uri.toString() }).catch(logAndReturn.undefined); - }); - - this.disposables.push(this.rootPathFileWatcher); - } else { - this.rootPathFileWatcher = undefined; - } - } - - /** - * handle notifications coming from the language server - */ - - public addFileAssociations(fileAssociations: string, languageId: string): void { - const settings: OtherSettings = new OtherSettings(); - const assocs: any = settings.filesAssociations; - - let foundNewAssociation: boolean = false; - const filesAndPaths: string[] = fileAssociations.split(";"); - for (let i: number = 0; i < filesAndPaths.length; ++i) { - const fileAndPath: string[] = filesAndPaths[i].split("@"); - // Skip empty or malformed - if (fileAndPath.length === 2) { - const file: string = fileAndPath[0]; - const filePath: string = fileAndPath[1]; - if ((file in assocs) || (("**/" + file) in assocs)) { - continue; // File already has an association. - } - const j: number = file.lastIndexOf('.'); - if (j !== -1) { - const ext: string = file.substring(j); - if ((("*" + ext) in assocs) || (("**/*" + ext) in assocs)) { - continue; // Extension already has an association. - } - } - let foundGlobMatch: boolean = false; - for (const assoc in assocs) { - const matcher = new minimatch.Minimatch(assoc); - if (matcher.match(filePath)) { - foundGlobMatch = true; - break; // Assoc matched a glob pattern. - } - } - if (foundGlobMatch) { - continue; - } - assocs[file] = languageId; - foundNewAssociation = true; - } - } - if (foundNewAssociation) { - settings.filesAssociations = assocs; - } - } - - private logTelemetry(notificationBody: TelemetryPayload): void { - if (notificationBody.event === "includeSquiggles" && this.configurationProvider && notificationBody.properties) { - notificationBody.properties["providerId"] = this.configurationProvider; - } - telemetry.logLanguageServerEvent(notificationBody.event, notificationBody.properties, notificationBody.metrics); - } - - private async updateStatus(notificationBody: ReportStatusNotificationBody): Promise { - const message: string = notificationBody.status; - util.setProgress(util.getProgressExecutableSuccess()); - const testHook: TestHook = getTestHook(); - if (message.endsWith("Idle")) { - // nothing to do - } else if (message.endsWith("Parsing")) { - this.model.isParsingWorkspace.Value = true; - this.model.isInitializingWorkspace.Value = false; - this.model.isIndexingWorkspace.Value = false; - const status: IntelliSenseStatus = { status: Status.TagParsingBegun }; - testHook.updateStatus(status); - } else if (message.endsWith("Initializing")) { - this.model.isInitializingWorkspace.Value = true; - this.model.isIndexingWorkspace.Value = false; - this.model.isParsingWorkspace.Value = false; - } else if (message.endsWith("Indexing")) { - this.model.isIndexingWorkspace.Value = true; - this.model.isInitializingWorkspace.Value = false; - this.model.isParsingWorkspace.Value = false; - } else if (message.endsWith("files")) { - this.model.isParsingFiles.Value = true; - } else if (message.endsWith("IntelliSense")) { - timeStamp = Date.now(); - this.model.isUpdatingIntelliSense.Value = true; - const status: IntelliSenseStatus = { status: Status.IntelliSenseCompiling }; - testHook.updateStatus(status); - } else if (message.endsWith("IntelliSense done")) { - const settings: CppSettings = new CppSettings(); - if (util.getNumericLoggingLevel(settings.loggingLevel) >= 6) { - const out: Logger = getOutputChannelLogger(); - const duration: number = Date.now() - timeStamp; - out.appendLine(localize("update.intellisense.time", "Update IntelliSense time (sec): {0}", duration / 1000)); - } - this.model.isUpdatingIntelliSense.Value = false; - const status: IntelliSenseStatus = { status: Status.IntelliSenseReady }; - testHook.updateStatus(status); - } else if (message.endsWith("Parsing done")) { // Tag Parser Ready - this.model.isParsingWorkspace.Value = false; - const status: IntelliSenseStatus = { status: Status.TagParsingDone }; - testHook.updateStatus(status); - util.setProgress(util.getProgressParseRootSuccess()); - } else if (message.endsWith("files done")) { - this.model.isParsingFiles.Value = false; - } else if (message.endsWith("Analysis")) { - this.model.isRunningCodeAnalysis.Value = true; - this.model.codeAnalysisTotal.Value = 1; - this.model.codeAnalysisProcessed.Value = 0; - } else if (message.endsWith("Analysis done")) { - this.model.isRunningCodeAnalysis.Value = false; - } else if (message.includes("Squiggles Finished - File name:")) { - const index: number = message.lastIndexOf(":"); - const name: string = message.substring(index + 2); - const status: IntelliSenseStatus = { status: Status.IntelliSenseReady, filename: name }; - testHook.updateStatus(status); - } else if (message.endsWith("No Squiggles")) { - util.setIntelliSenseProgress(util.getProgressIntelliSenseNoSquiggles()); - } - } - - private updateTagParseStatus(tagParseStatus: TagParseStatus): void { - this.model.parsingWorkspaceStatus.Value = getLocalizedString(tagParseStatus.localizeStringParams); - this.model.isParsingWorkspacePaused.Value = tagParseStatus.isPaused; - } - - private updateInactiveRegions(uriString: string, inactiveRegions: InputRegion[], startNewSet: boolean, updateFoldingRanges: boolean): void { - if (this.codeFoldingProvider && updateFoldingRanges) { - this.codeFoldingProvider.refresh(); - } - - const client: Client = clients.getClientFor(vscode.Uri.parse(uriString)); - if (!(client instanceof DefaultClient) || (!startNewSet && inactiveRegions.length === 0)) { - return; - } - const settings: CppSettings = new CppSettings(client.RootUri); - const dimInactiveRegions: boolean = settings.dimInactiveRegions; - let currentSet: DecorationRangesPair | undefined = this.inactiveRegionsDecorations.get(uriString); - if (startNewSet || !dimInactiveRegions) { - if (currentSet) { - currentSet.decoration.dispose(); - this.inactiveRegionsDecorations.delete(uriString); - } - if (!dimInactiveRegions) { - return; - } - currentSet = undefined; - } - if (currentSet === undefined) { - const opacity: number | undefined = settings.inactiveRegionOpacity; - currentSet = { - decoration: vscode.window.createTextEditorDecorationType({ - opacity: (opacity === undefined) ? "0.55" : opacity.toString(), - backgroundColor: settings.inactiveRegionBackgroundColor, - color: settings.inactiveRegionForegroundColor, - rangeBehavior: vscode.DecorationRangeBehavior.OpenOpen - }), - ranges: [] - }; - this.inactiveRegionsDecorations.set(uriString, currentSet); - } - - Array.prototype.push.apply(currentSet.ranges, inactiveRegions.map(element => new vscode.Range(element.startLine, 0, element.endLine, 0))); - - // Apply the decorations to all *visible* text editors - const editors: vscode.TextEditor[] = vscode.window.visibleTextEditors.filter(e => e.document.uri.toString() === uriString); - for (const e of editors) { - e.setDecorations(currentSet.decoration, currentSet.ranges); - } - } - - public logIntelliSenseSetupTime(notification: IntelliSenseSetup): void { - clients.timeTelemetryCollector.setSetupTime(vscode.Uri.parse(notification.uri)); - } - - private compileCommandsPaths: string[] = []; - private async promptCompileCommands(params: CompileCommandsPaths): Promise { - if (!params.workspaceFolderUri) { - return; - } - const potentialClient: Client = clients.getClientFor(vscode.Uri.file(params.workspaceFolderUri)); - const client: DefaultClient = potentialClient as DefaultClient; - if (!client) { - return; - } - if (client.configStateReceived.compileCommands) { - return; - } - - client.compileCommandsPaths = params.paths; - client.configStateReceived.compileCommands = true; - await client.handleConfigStatus("compileCommands"); - } - - public async handleConfigStatus(sender?: string): Promise { - if (!this.configStateReceived.timeout - && (!this.configStateReceived.compilers || !this.configStateReceived.compileCommands || !this.configStateReceived.configProviders)) { - return; // Wait till the config state is recevied or timed out. - } - - const rootFolder: vscode.WorkspaceFolder | undefined = this.RootFolder; - const settings: CppSettings = new CppSettings(this.RootUri); - const configProviderNotSet: boolean = !settings.defaultConfigurationProvider && !this.configuration.CurrentConfiguration?.configurationProvider && - !this.configuration.CurrentConfiguration?.configurationProviderInCppPropertiesJson; - const configProviderNotSetAndNoCache: boolean = configProviderNotSet && this.lastCustomBrowseConfigurationProviderId?.Value === undefined; - const compileCommandsNotSet: boolean = !settings.defaultCompileCommands && !this.configuration.CurrentConfiguration?.compileCommands && !this.configuration.CurrentConfiguration?.compileCommandsInCppPropertiesJson; - - // Handle config providers - const provider: CustomConfigurationProvider1 | undefined = - !this.configStateReceived.configProviders ? undefined : - this.configStateReceived.configProviders.length === 0 ? undefined : this.configStateReceived.configProviders[0]; - let showConfigStatus: boolean = false; - if (rootFolder && configProviderNotSetAndNoCache && provider && (sender === "configProviders")) { - const ask: PersistentFolderState = new PersistentFolderState("Client.registerProvider", true, rootFolder); - showConfigStatus = ask.Value; - } - - // Handle compile commands - if (rootFolder && configProviderNotSetAndNoCache && !this.configStateReceived.configProviders && - compileCommandsNotSet && this.compileCommandsPaths.length > 0 && (sender === "compileCommands")) { - const ask: PersistentFolderState = new PersistentFolderState("CPP.showCompileCommandsSelection", true, rootFolder); - showConfigStatus = ask.Value; - } - - const compilerPathNotSet: boolean = settings.defaultCompilerPath === null && this.configuration.CurrentConfiguration?.compilerPath === undefined && this.configuration.CurrentConfiguration?.compilerPathInCppPropertiesJson === undefined; - const configurationNotSet: boolean = configProviderNotSetAndNoCache && compileCommandsNotSet && compilerPathNotSet; - - showConfigStatus = showConfigStatus || (configurationNotSet && - !!compilerDefaults && !compilerDefaults.trustedCompilerFound && trustedCompilerPaths && (trustedCompilerPaths.length !== 1 || trustedCompilerPaths[0] !== "")); - - const configProviderType: ConfigurationType = this.configuration.ConfigProviderAutoSelected ? ConfigurationType.AutoConfigProvider : ConfigurationType.ConfigProvider; - const compilerType: ConfigurationType = this.configuration.CurrentConfiguration?.compilerPathIsExplicit ? ConfigurationType.CompilerPath : ConfigurationType.AutoCompilerPath; - const configType: ConfigurationType = - !configProviderNotSet ? configProviderType : - !compileCommandsNotSet ? ConfigurationType.CompileCommands : - !compilerPathNotSet ? compilerType : - ConfigurationType.NotConfigured; - - this.showConfigureIntelliSenseButton = showConfigStatus; - return ui.ShowConfigureIntelliSenseButton(showConfigStatus, this, configType, "handleConfig"); - } - - /** - * requests to the language server - */ - public async requestSwitchHeaderSource(rootUri: vscode.Uri, fileName: string): Promise { - const params: SwitchHeaderSourceParams = { - switchHeaderSourceFileName: fileName, - workspaceFolderUri: rootUri.toString() - }; - return this.enqueue(async () => this.languageClient.sendRequest(SwitchHeaderSourceRequest, params)); - } - - public async requestCompiler(newCompilerPath?: string): Promise { - const params: QueryDefaultCompilerParams = { - newTrustedCompilerPath: newCompilerPath ?? "" - }; - const results: configs.CompilerDefaults = await this.languageClient.sendRequest(QueryCompilerDefaultsRequest, params); - void SessionState.scanForCompilersDone.set(true); - void SessionState.scanForCompilersEmpty.set(results.knownCompilers === undefined || !results.knownCompilers.length); - void SessionState.trustedCompilerFound.set(results.trustedCompilerFound); - return results; - } - - public updateActiveDocumentTextOptions(): void { - const editor: vscode.TextEditor | undefined = vscode.window.activeTextEditor; - if (editor && util.isCpp(editor.document)) { - void SessionState.buildAndDebugIsSourceFile.set(util.isCppOrCFile(editor.document.uri)); - void SessionState.buildAndDebugIsFolderOpen.set(util.isFolderOpen(editor.document.uri)); - // If using vcFormat, check for a ".editorconfig" file, and apply those text options to the active document. - const settings: CppSettings = new CppSettings(this.RootUri); - if (settings.useVcFormat(editor.document)) { - const editorConfigSettings: any = getEditorConfigSettings(editor.document.uri.fsPath); - if (editorConfigSettings.indent_style === "space" || editorConfigSettings.indent_style === "tab") { - editor.options.insertSpaces = editorConfigSettings.indent_style === "space"; - if (editorConfigSettings.indent_size === "tab") { - if (!editorConfigSettings.tab_width !== undefined) { - editor.options.tabSize = editorConfigSettings.tab_width; - } - } else if (editorConfigSettings.indent_size !== undefined) { - editor.options.tabSize = editorConfigSettings.indent_size; - } - } - if (editorConfigSettings.end_of_line !== undefined) { - void editor.edit((edit) => { - edit.setEndOfLine(editorConfigSettings.end_of_line === "lf" ? vscode.EndOfLine.LF : vscode.EndOfLine.CRLF); - }).then(undefined, logAndReturn.undefined); - } - } - } else { - void SessionState.buildAndDebugIsSourceFile.set(false); - } - } - - /** - * notifications to the language server - */ - public async didChangeActiveEditor(editor?: vscode.TextEditor): Promise { - // For now, we ignore deactivation events. - // VS will refresh IntelliSense on activation, as a catch-all for file changes in - // other applications. But VS Code will deactivate the document when focus is moved - // to another control, such as the Output window. So, to avoid costly updates, we - // only trigger that update when focus moves from one C++ document to another. - // Fortunately, VS Code generates file-change notifications for all files - // in the workspace, so we should trigger appropriate updates for most changes - // made in other applications. - if (!editor || !util.isCpp(editor.document)) { - return; - } - - this.updateActiveDocumentTextOptions(); - - const params: DidChangeActiveEditorParams = { - uri: editor?.document?.uri.toString(), - selection: editor ? makeLspRange(editor.selection) : undefined - }; - - return this.languageClient.sendNotification(DidChangeActiveEditorNotification, params).catch(logAndReturn.undefined); - } - - /** - * send notifications to the language server to restart IntelliSense for the selected file. - */ - public async restartIntelliSenseForFile(document: vscode.TextDocument): Promise { - await this.ready; - return this.languageClient.sendNotification(RestartIntelliSenseForFileNotification, this.languageClient.code2ProtocolConverter.asTextDocumentIdentifier(document)).catch(logAndReturn.undefined); - } - - /** - * enable UI updates from this client and resume tag parsing on the server. - */ - public activate(): void { - this.model.activate(); - void this.resumeParsing().catch(logAndReturn.undefined); - } - - public async selectionChanged(selection: Range): Promise { - return this.languageClient.sendNotification(DidChangeTextEditorSelectionNotification, selection); - } - - public async resetDatabase(): Promise { - await this.ready; - return this.languageClient.sendNotification(ResetDatabaseNotification); - } - - /** - * disable UI updates from this client and pause tag parsing on the server. - */ - public deactivate(): void { - this.model.deactivate(); - } - - public async pauseParsing(): Promise { - await this.ready; - return this.languageClient.sendNotification(PauseParsingNotification); - } - - public async resumeParsing(): Promise { - await this.ready; - return this.languageClient.sendNotification(ResumeParsingNotification); - } - - public async PauseCodeAnalysis(): Promise { - await this.ready; - this.model.isCodeAnalysisPaused.Value = true; - return this.languageClient.sendNotification(PauseCodeAnalysisNotification); - } - - public async ResumeCodeAnalysis(): Promise { - await this.ready; - this.model.isCodeAnalysisPaused.Value = false; - return this.languageClient.sendNotification(ResumeCodeAnalysisNotification); - } - - public async CancelCodeAnalysis(): Promise { - await this.ready; - return this.languageClient.sendNotification(CancelCodeAnalysisNotification); - } - - private updateCodeAnalysisProcessed(processed: number): void { - this.model.codeAnalysisProcessed.Value = processed; - } - - private updateCodeAnalysisTotal(total: number): void { - this.model.codeAnalysisTotal.Value = total; - } - - private async insertDoxygenComment(result: GenerateDoxygenCommentResult): Promise { - const editor: vscode.TextEditor | undefined = vscode.window.activeTextEditor; - if (!editor) { - return; - } - const currentFileVersion: number | undefined = openFileVersions.get(editor.document.uri.toString()); - // Insert the comment only if the cursor has not moved - if (result.fileVersion === currentFileVersion && - result.initPosition.line === editor.selection.start.line && - result.initPosition.character === editor.selection.start.character && - result.contents.length > 1) { - const workspaceEdit: vscode.WorkspaceEdit = new vscode.WorkspaceEdit(); - const edits: vscode.TextEdit[] = []; - const maxColumn: number = 99999999; - const newRange: vscode.Range = new vscode.Range(editor.selection.start.line, 0, editor.selection.end.line, maxColumn); - edits.push(new vscode.TextEdit(newRange, result.contents)); - workspaceEdit.set(editor.document.uri, edits); - await vscode.workspace.applyEdit(workspaceEdit); - - // Set the cursor position after @brief - const newPosition: vscode.Position = new vscode.Position(result.finalCursorPosition.line, result.finalCursorPosition.character); - const newSelection: vscode.Selection = new vscode.Selection(newPosition, newPosition); - editor.selection = newSelection; - } - } - - private doneInitialCustomBrowseConfigurationCheck: boolean = false; - - private async onConfigurationsChanged(cppProperties: configs.CppProperties): Promise { - if (!cppProperties.Configurations) { - return; - } - const configurations: configs.Configuration[] = cppProperties.Configurations; - const params: CppPropertiesParams = { - configurations: [], - currentConfiguration: this.configuration.CurrentConfigurationIndex, - workspaceFolderUri: this.RootUri?.toString(), - isReady: true - }; - const settings: CppSettings = new CppSettings(this.RootUri); - // Clone each entry, as we make modifications before sending it, and don't - // want to add those modifications to the original objects. - configurations.forEach((c) => { - const modifiedConfig: configs.Configuration = deepCopy(c); - // Separate compiler path and args before sending to language client - const compilerPathAndArgs: util.CompilerPathAndArgs = - util.extractCompilerPathAndArgs(!!settings.legacyCompilerArgsBehavior, c.compilerPath, c.compilerArgs); - modifiedConfig.compilerPath = compilerPathAndArgs.compilerPath; - if (settings.legacyCompilerArgsBehavior) { - modifiedConfig.compilerArgsLegacy = compilerPathAndArgs.allCompilerArgs; - modifiedConfig.compilerArgs = undefined; - } else { - modifiedConfig.compilerArgs = compilerPathAndArgs.allCompilerArgs; - } - - params.configurations.push(modifiedConfig); - }); - - await this.languageClient.sendRequest(ChangeCppPropertiesRequest, params); - if (!!this.lastCustomBrowseConfigurationProviderId && !!this.lastCustomBrowseConfiguration && !!this.lastCustomBrowseConfigurationProviderVersion) { - if (!this.doneInitialCustomBrowseConfigurationCheck) { - // Send the last custom browse configuration we received from this provider. - // This ensures we don't start tag parsing without it, and undo'ing work we have to re-do when the (likely same) browse config arrives - // Should only execute on launch, for the initial delivery of configurations - if (this.lastCustomBrowseConfiguration.Value) { - this.sendCustomBrowseConfiguration(this.lastCustomBrowseConfiguration.Value, this.lastCustomBrowseConfigurationProviderId.Value, this.lastCustomBrowseConfigurationProviderVersion.Value); - params.isReady = false; - } - this.doneInitialCustomBrowseConfigurationCheck = true; - } - } - const configName: string | undefined = configurations[params.currentConfiguration].name ?? ""; - this.model.activeConfigName.setValueIfActive(configName); - const newProvider: string | undefined = this.configuration.CurrentConfigurationProvider; - if (!isSameProviderExtensionId(newProvider, this.configurationProvider)) { - if (this.configurationProvider) { - void this.clearCustomBrowseConfiguration().catch(logAndReturn.undefined); - } - this.configurationProvider = newProvider; - void this.updateCustomBrowseConfiguration().catch(logAndReturn.undefined); - void this.updateCustomConfigurations().catch(logAndReturn.undefined); - } - } - - private async onSelectedConfigurationChanged(index: number): Promise { - const params: FolderSelectedSettingParams = { - currentConfiguration: index, - workspaceFolderUri: this.RootUri?.toString() - }; - await this.ready; - await this.languageClient.sendNotification(ChangeSelectedSettingNotification, params); - - let configName: string = ""; - if (this.configuration.ConfigurationNames) { - configName = this.configuration.ConfigurationNames[index]; - } - this.model.activeConfigName.Value = configName; - this.configuration.onDidChangeSettings(); - } - - private async onCompileCommandsChanged(path: string): Promise { - const params: FileChangedParams = { - uri: vscode.Uri.file(path).toString(), - workspaceFolderUri: this.RootUri?.toString() - }; - await this.ready; - return this.languageClient.sendNotification(ChangeCompileCommandsNotification, params); - } - - private isSourceFileConfigurationItem(input: any, providerVersion: Version): input is SourceFileConfigurationItem { - // IntelliSenseMode and standard are optional for version 5+. - let areOptionalsValid: boolean = false; - if (providerVersion < Version.v5) { - areOptionalsValid = util.isString(input.configuration.intelliSenseMode) && util.isString(input.configuration.standard); - } else { - areOptionalsValid = util.isOptionalString(input.configuration.intelliSenseMode) && util.isOptionalString(input.configuration.standard); - } - return input && (util.isString(input.uri) || util.isUri(input.uri)) && - input.configuration && - areOptionalsValid && - util.isArrayOfString(input.configuration.includePath) && - util.isArrayOfString(input.configuration.defines) && - util.isOptionalArrayOfString(input.configuration.compilerArgs) && - util.isOptionalArrayOfString(input.configuration.forcedInclude); - } - - private sendCustomConfigurations(configs: any, providerVersion: Version): void { - // configs is marked as 'any' because it is untrusted data coming from a 3rd-party. We need to sanitize it before sending it to the language server. - if (!configs || !(configs instanceof Array)) { - console.warn("discarding invalid SourceFileConfigurationItems[]: " + configs); - return; - } - - const settings: CppSettings = new CppSettings(); - const out: Logger = getOutputChannelLogger(); - if (util.getNumericLoggingLevel(settings.loggingLevel) >= 6) { - out.appendLine(localize("configurations.received", "Custom configurations received:")); - } - const sanitized: SourceFileConfigurationItemAdapter[] = []; - configs.forEach(item => { - if (this.isSourceFileConfigurationItem(item, providerVersion)) { - let uri: string; - if (util.isString(item.uri) && !item.uri.startsWith("file://")) { - // If the uri field is a string, it may actually contain an fsPath. - uri = vscode.Uri.file(item.uri).toString(); - } else { - uri = item.uri.toString(); - } - this.configurationLogging.set(uri, JSON.stringify(item.configuration, null, 4)); - if (util.getNumericLoggingLevel(settings.loggingLevel) >= 6) { - out.appendLine(` uri: ${uri}`); - out.appendLine(` config: ${JSON.stringify(item.configuration, null, 2)}`); - } - if (item.configuration.includePath.some(path => path.endsWith('**'))) { - console.warn("custom include paths should not use recursive includes ('**')"); - } - // Separate compiler path and args before sending to language client - const itemConfig: util.Mutable = deepCopy(item.configuration); - if (util.isString(itemConfig.compilerPath)) { - const compilerPathAndArgs: util.CompilerPathAndArgs = util.extractCompilerPathAndArgs( - providerVersion < Version.v6, - itemConfig.compilerPath, - util.isArrayOfString(itemConfig.compilerArgs) ? itemConfig.compilerArgs : undefined); - itemConfig.compilerPath = compilerPathAndArgs.compilerPath ?? undefined; - if (itemConfig.compilerPath !== undefined) { - void this.addTrustedCompiler(itemConfig.compilerPath).catch(logAndReturn.undefined); - } - if (providerVersion < Version.v6) { - itemConfig.compilerArgsLegacy = compilerPathAndArgs.allCompilerArgs; - itemConfig.compilerArgs = undefined; - } else { - itemConfig.compilerArgs = compilerPathAndArgs.allCompilerArgs; - } - } - sanitized.push({ - uri, - configuration: itemConfig - }); - } else { - console.warn("discarding invalid SourceFileConfigurationItem: " + JSON.stringify(item)); - } - }); - - if (sanitized.length === 0) { - return; - } - - const params: CustomConfigurationParams = { - configurationItems: sanitized, - workspaceFolderUri: this.RootUri?.toString() - }; - - // We send the higher priority notification to ensure we don't deadlock if the request is blocking the queue. - // We send the normal priority notification to avoid a race that could result in a redundant request when racing with - // the reset of custom configurations. - void this.languageClient.sendNotification(CustomConfigurationHighPriorityNotification, params).catch(logAndReturn.undefined); - void this.languageClient.sendNotification(CustomConfigurationNotification, params).catch(logAndReturn.undefined); - } - - private browseConfigurationLogging: string = ""; - private configurationLogging: Map = new Map(); - - private isWorkspaceBrowseConfiguration(input: any): boolean { - return util.isArrayOfString(input.browsePath) && - util.isOptionalString(input.compilerPath) && - util.isOptionalString(input.standard) && - util.isOptionalArrayOfString(input.compilerArgs) && - util.isOptionalString(input.windowsSdkVersion); - } - - private sendCustomBrowseConfiguration(config: any, providerId: string | undefined, providerVersion: Version, timeoutOccured?: boolean): void { - const rootFolder: vscode.WorkspaceFolder | undefined = this.RootFolder; - if (!rootFolder - || !this.lastCustomBrowseConfiguration - || !this.lastCustomBrowseConfigurationProviderId) { - return; - } - - let sanitized: util.Mutable; - - this.browseConfigurationLogging = ""; - - // This while (true) is here just so we can break out early if the config is set on error - // eslint-disable-next-line no-constant-condition - while (true) { - // config is marked as 'any' because it is untrusted data coming from a 3rd-party. We need to sanitize it before sending it to the language server. - if (timeoutOccured || !config || config instanceof Array) { - if (!timeoutOccured) { - console.log("Received an invalid browse configuration from configuration provider."); - } - const configValue: WorkspaceBrowseConfiguration | undefined = this.lastCustomBrowseConfiguration.Value; - if (configValue) { - sanitized = configValue; - if (sanitized.browsePath.length === 0) { - sanitized.browsePath = ["${workspaceFolder}/**"]; - } - break; - } - return; - } - - const browseConfig: InternalWorkspaceBrowseConfiguration = config as InternalWorkspaceBrowseConfiguration; - sanitized = deepCopy(browseConfig); - if (!this.isWorkspaceBrowseConfiguration(sanitized) || sanitized.browsePath.length === 0) { - console.log("Received an invalid browse configuration from configuration provider: " + JSON.stringify(sanitized)); - const configValue: WorkspaceBrowseConfiguration | undefined = this.lastCustomBrowseConfiguration.Value; - if (configValue) { - sanitized = configValue; - if (sanitized.browsePath.length === 0) { - sanitized.browsePath = ["${workspaceFolder}/**"]; - } - break; - } - return; - } - - const settings: CppSettings = new CppSettings(); - if (util.getNumericLoggingLevel(settings.loggingLevel) >= 6) { - const out: Logger = getOutputChannelLogger(); - out.appendLine(localize("browse.configuration.received", "Custom browse configuration received: {0}", JSON.stringify(sanitized, null, 2))); - } - - // Separate compiler path and args before sending to language client - if (util.isString(sanitized.compilerPath)) { - const compilerPathAndArgs: util.CompilerPathAndArgs = util.extractCompilerPathAndArgs( - providerVersion < Version.v6, - sanitized.compilerPath, - util.isArrayOfString(sanitized.compilerArgs) ? sanitized.compilerArgs : undefined); - sanitized.compilerPath = compilerPathAndArgs.compilerPath ?? undefined; - if (sanitized.compilerPath !== undefined) { - void this.addTrustedCompiler(sanitized.compilerPath).catch(logAndReturn.undefined); - } - if (providerVersion < Version.v6) { - sanitized.compilerArgsLegacy = compilerPathAndArgs.allCompilerArgs; - sanitized.compilerArgs = undefined; - } else { - sanitized.compilerArgs = compilerPathAndArgs.allCompilerArgs; - } - } - - this.lastCustomBrowseConfiguration.Value = sanitized; - if (!providerId) { - this.lastCustomBrowseConfigurationProviderId.setDefault(); - } else { - this.lastCustomBrowseConfigurationProviderId.Value = providerId; - } - break; - } - - this.browseConfigurationLogging = `Custom browse configuration: \n${JSON.stringify(sanitized, null, 4)}\n`; - - const params: CustomBrowseConfigurationParams = { - browseConfiguration: sanitized, - workspaceFolderUri: this.RootUri?.toString() - }; - - void this.languageClient.sendNotification(CustomBrowseConfigurationNotification, params).catch(logAndReturn.undefined); - } - - private async clearCustomConfigurations(): Promise { - this.configurationLogging.clear(); - const params: WorkspaceFolderParams = { - workspaceFolderUri: this.RootUri?.toString() - }; - await this.ready; - return this.languageClient.sendNotification(ClearCustomConfigurationsNotification, params); - } - - private async clearCustomBrowseConfiguration(): Promise { - this.browseConfigurationLogging = ""; - const params: WorkspaceFolderParams = { - workspaceFolderUri: this.RootUri?.toString() - }; - await this.ready; - return this.languageClient.sendNotification(ClearCustomBrowseConfigurationNotification, params); - } - - /** - * command handlers - */ - public async handleConfigurationSelectCommand(): Promise { - await this.ready; - const configNames: string[] | undefined = this.configuration.ConfigurationNames; - if (configNames) { - const index: number = await ui.showConfigurations(configNames); - if (index < 0) { - return; - } - this.configuration.select(index); - } - } - - public async handleConfigurationProviderSelectCommand(): Promise { - await this.ready; - const extensionId: string | undefined = await ui.showConfigurationProviders(this.configuration.CurrentConfigurationProvider); - if (extensionId === undefined) { - // operation was canceled. - return; - } - await this.configuration.updateCustomConfigurationProvider(extensionId); - if (extensionId) { - const provider: CustomConfigurationProvider1 | undefined = getCustomConfigProviders().get(extensionId); - void this.updateCustomBrowseConfiguration(provider).catch(logAndReturn.undefined); - void this.updateCustomConfigurations(provider).catch(logAndReturn.undefined); - telemetry.logLanguageServerEvent("customConfigurationProvider", { "providerId": extensionId }); - } else { - void this.clearCustomConfigurations().catch(logAndReturn.undefined); - void this.clearCustomBrowseConfiguration().catch(logAndReturn.undefined); - } - } - - public async handleShowActiveCodeAnalysisCommands(): Promise { - await this.ready; - const index: number = await ui.showActiveCodeAnalysisCommands(); - switch (index) { - case 0: return this.CancelCodeAnalysis(); - case 1: return this.PauseCodeAnalysis(); - case 2: return this.ResumeCodeAnalysis(); - case 3: return this.handleShowIdleCodeAnalysisCommands(); - } - } - - public async handleShowIdleCodeAnalysisCommands(): Promise { - await this.ready; - const index: number = await ui.showIdleCodeAnalysisCommands(); - switch (index) { - case 0: return this.handleRunCodeAnalysisOnActiveFile(); - case 1: return this.handleRunCodeAnalysisOnAllFiles(); - case 2: return this.handleRunCodeAnalysisOnOpenFiles(); - } - } - - public async handleConfigurationEditCommand(viewColumn: vscode.ViewColumn = vscode.ViewColumn.Active): Promise { - await this.ready; - return this.configuration.handleConfigurationEditCommand(undefined, vscode.window.showTextDocument, viewColumn); - } - - public async handleConfigurationEditJSONCommand(viewColumn: vscode.ViewColumn = vscode.ViewColumn.Active): Promise { - await this.ready; - return this.configuration.handleConfigurationEditJSONCommand(undefined, vscode.window.showTextDocument, viewColumn); - } - - public async handleConfigurationEditUICommand(viewColumn: vscode.ViewColumn = vscode.ViewColumn.Active): Promise { - await this.ready; - return this.configuration.handleConfigurationEditUICommand(undefined, vscode.window.showTextDocument, viewColumn); - } - - public async handleAddToIncludePathCommand(path: string): Promise { - await this.ready; - return this.configuration.addToIncludePathCommand(path); - } - - public async handleGoToDirectiveInGroup(next: boolean): Promise { - const editor: vscode.TextEditor | undefined = vscode.window.activeTextEditor; - if (editor) { - const params: GoToDirectiveInGroupParams = { - uri: editor.document.uri.toString(), - position: editor.selection.active, - next: next - }; - await this.ready; - const response: Position | undefined = await this.languageClient.sendRequest(GoToDirectiveInGroupRequest, params); - if (response) { - const p: vscode.Position = new vscode.Position(response.line, response.character); - const r: vscode.Range = new vscode.Range(p, p); - - // Check if still the active document. - const currentEditor: vscode.TextEditor | undefined = vscode.window.activeTextEditor; - if (currentEditor && editor.document.uri === currentEditor.document.uri) { - currentEditor.selection = new vscode.Selection(r.start, r.end); - currentEditor.revealRange(r); - } - } - } - } - - public async handleGenerateDoxygenComment(args: DoxygenCodeActionCommandArguments | vscode.Uri | undefined): Promise { - const editor: vscode.TextEditor | undefined = vscode.window.activeTextEditor; - if (!editor || !util.isCpp(editor.document)) { - return; - } - - let codeActionArguments: DoxygenCodeActionCommandArguments | undefined; - if (args !== undefined && !(args instanceof vscode.Uri)) { - codeActionArguments = args; - } - const initCursorPosition: vscode.Position = (codeActionArguments !== undefined) ? new vscode.Position(codeActionArguments.initialCursor.line, codeActionArguments.initialCursor.character) : editor.selection.start; - const params: GenerateDoxygenCommentParams = { - uri: editor.document.uri.toString(), - position: (codeActionArguments !== undefined) ? new vscode.Position(codeActionArguments.adjustedCursor.line, codeActionArguments.adjustedCursor.character) : editor.selection.start, - isCodeAction: codeActionArguments !== undefined, - isCursorAboveSignatureLine: codeActionArguments?.isCursorAboveSignatureLine - }; - await this.ready; - const currentFileVersion: number | undefined = openFileVersions.get(params.uri); - if (currentFileVersion === undefined) { - return; - } - const result: GenerateDoxygenCommentResult | undefined = await this.languageClient.sendRequest(GenerateDoxygenCommentRequest, params); - // Insert the comment only if the comment has contents and the cursor has not moved - if (result !== undefined && - initCursorPosition.line === editor.selection.start.line && - initCursorPosition.character === editor.selection.start.character && - result.fileVersion !== undefined && - result.fileVersion === currentFileVersion && - result.contents && result.contents.length > 1) { - const workspaceEdit: vscode.WorkspaceEdit = new vscode.WorkspaceEdit(); - const edits: vscode.TextEdit[] = []; - const maxColumn: number = 99999999; - let newRange: vscode.Range; - const cursorOnEmptyLineAboveSignature: boolean = result.isCursorAboveSignatureLine; - // The reason why we need to set different range is because if cursor is immediately above the signature line, we want the comments to be inserted at the line of cursor and to replace everything on the line. - // If the cursor is on the signature line or is inside the boby, the comment will be inserted on the same line of the signature and it shouldn't replace the content of the signature line. - if (cursorOnEmptyLineAboveSignature) { - if (codeActionArguments !== undefined) { - // The reason why we cannot use finalInsertionLine is because the line number sent from the result is not correct. - // In most cases, the finalInsertionLine is the line of the signature line. - newRange = new vscode.Range(initCursorPosition.line, 0, initCursorPosition.line, maxColumn); - } else { - newRange = new vscode.Range(result.finalInsertionLine, 0, result.finalInsertionLine, maxColumn); - } - } else { - newRange = new vscode.Range(result.finalInsertionLine, 0, result.finalInsertionLine, 0); - } - edits.push(new vscode.TextEdit(newRange, result.contents)); - workspaceEdit.set(editor.document.uri, edits); - await vscode.workspace.applyEdit(workspaceEdit); - // Set the cursor position after @brief - let newPosition: vscode.Position; - if (cursorOnEmptyLineAboveSignature && codeActionArguments !== undefined) { - newPosition = new vscode.Position(result.finalCursorPosition.line - 1, result.finalCursorPosition.character); - } else { - newPosition = new vscode.Position(result.finalCursorPosition.line, result.finalCursorPosition.character); - } - const newSelection: vscode.Selection = new vscode.Selection(newPosition, newPosition); - editor.selection = newSelection; - } - } - - public async handleRunCodeAnalysisOnActiveFile(): Promise { - await this.ready; - return this.languageClient.sendNotification(CodeAnalysisNotification, { scope: CodeAnalysisScope.ActiveFile }); - } - - public async handleRunCodeAnalysisOnOpenFiles(): Promise { - await this.ready; - return this.languageClient.sendNotification(CodeAnalysisNotification, { scope: CodeAnalysisScope.OpenFiles }); - } - - public async handleRunCodeAnalysisOnAllFiles(): Promise { - await this.ready; - return this.languageClient.sendNotification(CodeAnalysisNotification, { scope: CodeAnalysisScope.AllFiles }); - } - - public async handleRemoveAllCodeAnalysisProblems(): Promise { - await this.ready; - if (removeAllCodeAnalysisProblems()) { - return this.languageClient.sendNotification(CodeAnalysisNotification, { scope: CodeAnalysisScope.ClearSquiggles }); - } - } - - public async handleFixCodeAnalysisProblems(workspaceEdit: vscode.WorkspaceEdit, refreshSquigglesOnSave: boolean, identifiersAndUris: CodeAnalysisDiagnosticIdentifiersAndUri[]): Promise { - if (await vscode.workspace.applyEdit(workspaceEdit)) { - const settings: CppSettings = new CppSettings(this.RootUri); - if (settings.clangTidyCodeActionFormatFixes) { - const editedFiles: Set = new Set(); - for (const entry of workspaceEdit.entries()) { - editedFiles.add(entry[0]); - } - const formatEdits: vscode.WorkspaceEdit = new vscode.WorkspaceEdit(); - for (const uri of editedFiles) { - const formatTextEdits: vscode.TextEdit[] | undefined = await vscode.commands.executeCommand( - "vscode.executeFormatDocumentProvider", uri, { onChanges: true, preserveFocus: false }); - if (formatTextEdits && formatTextEdits.length > 0) { - formatEdits.set(uri, formatTextEdits); - } - } - if (formatEdits.size > 0) { - await vscode.workspace.applyEdit(formatEdits); - } - } - return this.handleRemoveCodeAnalysisProblems(refreshSquigglesOnSave, identifiersAndUris); - } - } - - public async handleRemoveCodeAnalysisProblems(refreshSquigglesOnSave: boolean, identifiersAndUris: CodeAnalysisDiagnosticIdentifiersAndUri[]): Promise { - await this.ready; - - // A deep copy is needed because the call to identifiers.splice below can - // remove elements in identifiersAndUris[...].identifiers. - const identifiersAndUrisCopy: CodeAnalysisDiagnosticIdentifiersAndUri[] = []; - for (const identifiersAndUri of identifiersAndUris) { - identifiersAndUrisCopy.push({ uri: identifiersAndUri.uri, identifiers: [...identifiersAndUri.identifiers] }); - } - - if (removeCodeAnalysisProblems(identifiersAndUris)) { - // Need to notify the language client of the removed diagnostics so it doesn't re-send them. - return this.languageClient.sendNotification(RemoveCodeAnalysisProblemsNotification, { - identifiersAndUris: identifiersAndUrisCopy, refreshSquigglesOnSave: refreshSquigglesOnSave - }); - } - } - - public async handleDisableAllTypeCodeAnalysisProblems(code: string, - identifiersAndUris: CodeAnalysisDiagnosticIdentifiersAndUri[]): Promise { - const settings: CppSettings = new CppSettings(this.RootUri); - const codes: string[] = code.split(','); - for (const code of codes) { - settings.addClangTidyChecksDisabled(code); - } - return this.handleRemoveCodeAnalysisProblems(false, identifiersAndUris); - } - - public async handleCreateDeclarationOrDefinition(isCopyToClipboard: boolean, codeActionRange?: Range): Promise { - let range: vscode.Range | undefined; - let uri: vscode.Uri | undefined; - const editor: vscode.TextEditor | undefined = vscode.window.activeTextEditor; - - const editorSettings: OtherSettings = new OtherSettings(uri); - const cppSettings: CppSettings = new CppSettings(uri); - - if (editor) { - uri = editor.document.uri; - if (codeActionRange !== undefined) { - // Request is from a code action command which provides range from code actions args. - range = makeVscodeRange(codeActionRange); - } else { - // Request is from context menu or command palette. Use range from cursor position. - if (editor.selection.isEmpty) { - range = new vscode.Range(editor.selection.active, editor.selection.active); - } else if (editor.selection.isReversed) { - range = new vscode.Range(editor.selection.active, editor.selection.anchor); - } else { - range = new vscode.Range(editor.selection.anchor, editor.selection.active); - } - } - } - - if (uri === undefined || range === undefined || editor === undefined) { - return; - } - - let formatParams: FormatParams | undefined; - if (cppSettings.useVcFormat(editor.document)) { - const editorConfigSettings: any = getEditorConfigSettings(uri.fsPath); - formatParams = { - editorConfigSettings: editorConfigSettings, - useVcFormat: true, - insertSpaces: editorConfigSettings.indent_style !== undefined ? editorConfigSettings.indent_style === "space" ? true : false : true, - tabSize: editorConfigSettings.tab_width !== undefined ? editorConfigSettings.tab_width : 4, - character: "", - range: { - start: { - character: 0, - line: 0 - }, - end: { - character: 0, - line: 0 - } - }, - onChanges: false, - uri: '' - }; - } else { - formatParams = { - editorConfigSettings: {}, - useVcFormat: false, - insertSpaces: editorSettings.editorInsertSpaces !== undefined ? editorSettings.editorInsertSpaces : true, - tabSize: editorSettings.editorTabSize !== undefined ? editorSettings.editorTabSize : 4, - character: "", - range: { - start: { - character: 0, - line: 0 - }, - end: { - character: 0, - line: 0 - } - }, - onChanges: false, - uri: '' - }; - } - - const params: CreateDeclarationOrDefinitionParams = { - uri: uri.toString(), - range: { - start: { - character: range.start.character, - line: range.start.line - }, - end: { - character: range.end.character, - line: range.end.line - } - }, - formatParams: formatParams, - copyToClipboard: isCopyToClipboard - }; - - const result: CreateDeclarationOrDefinitionResult = await this.languageClient.sendRequest(CreateDeclarationOrDefinitionRequest, params); - // Create/Copy returned no result. - if (result.workspaceEdits === undefined) { - // The only condition in which result.edit would be undefined is a - // server-initiated cancellation, in which case the object is actually - // a ResponseError. https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#responseMessage - return; - } - - // Handle CDD error messaging - if (result.errorText) { - let copiedToClipboard: boolean = false; - if (result.clipboardText && !params.copyToClipboard) { - await vscode.env.clipboard.writeText(result.clipboardText); - copiedToClipboard = true; - } - void vscode.window.showInformationMessage(result.errorText + (copiedToClipboard ? localize("fallback.clipboard", " Declaration/definition was copied.") : "")); - return; - } - - if (result.clipboardText && params.copyToClipboard) { - return vscode.env.clipboard.writeText(result.clipboardText); - } - - let workspaceEdits: vscode.WorkspaceEdit = new vscode.WorkspaceEdit(); - let modifiedDocument: vscode.Uri | undefined; - let lastEdit: vscode.TextEdit | undefined; - for (const workspaceEdit of result.workspaceEdits) { - const uri: vscode.Uri = vscode.Uri.file(workspaceEdit.file); - // At most, there will only be two text edits: - // 1.) an edit for: #include header file - // 2.) an edit for: definition or declaration - for (const edit of workspaceEdit.edits) { - let range: vscode.Range = makeVscodeRange(edit.range); - // Get new lines from an edit for: #include header file. - if (lastEdit && lastEdit.newText.length < 300 && lastEdit.newText.includes("#include") && lastEdit.range.isEqual(range)) { - // Destination file is empty. - // The edit positions for #include header file and definition or declaration are the same. - const selectionPositionAdjustment = (lastEdit.newText.match(/\n/g) ?? []).length; - range = new vscode.Range(new vscode.Position(range.start.line + selectionPositionAdjustment, range.start.character), - new vscode.Position(range.end.line + selectionPositionAdjustment, range.end.character)); - } - lastEdit = new vscode.TextEdit(range, edit.newText); - workspaceEdits.insert(uri, range.start, edit.newText); - if (edit.newText.length < 300 && edit.newText.includes("#pragma once")) { - // Commit this so that it can be undone separately, to avoid leaving an empty file, - // which causes the next refactor to not add the #pragma once. - await vscode.workspace.applyEdit(workspaceEdits); - workspaceEdits = new vscode.WorkspaceEdit(); - } - } - modifiedDocument = uri; - } - - if (modifiedDocument === undefined || lastEdit === undefined) { - return; - } - - // Apply the create declaration/definition text edits. - await vscode.workspace.applyEdit(workspaceEdits); - - // Move the cursor to the new declaration/definition edit, accounting for \n or \n\n at the start. - let startLine: number = lastEdit.range.start.line; - let numNewlines: number = (lastEdit.newText.match(/\n/g) ?? []).length; - if (lastEdit.newText.startsWith("\r\n\r\n") || lastEdit.newText.startsWith("\n\n")) { - startLine += 2; - numNewlines -= 2; - } else if (lastEdit.newText.startsWith("\r\n") || lastEdit.newText.startsWith("\n")) { - startLine += 1; - numNewlines -= 1; - } - if (!lastEdit.newText.endsWith("\n")) { - numNewlines++; // Increase the format range. - } - - const selectionPosition: vscode.Position = new vscode.Position(startLine, 0); - const selectionRange: vscode.Range = new vscode.Range(selectionPosition, selectionPosition); - await vscode.window.showTextDocument(modifiedDocument, { selection: selectionRange }); - - // Format the new text edits. - const formatEdits: vscode.WorkspaceEdit = new vscode.WorkspaceEdit(); - const formatRange: vscode.Range = new vscode.Range(selectionRange.start, new vscode.Position(selectionRange.start.line + numNewlines, 0)); - const settings: OtherSettings = new OtherSettings(vscode.workspace.getWorkspaceFolder(modifiedDocument)?.uri); - const formatOptions: vscode.FormattingOptions = { - insertSpaces: settings.editorInsertSpaces ?? true, - tabSize: settings.editorTabSize ?? 4 - }; - const versionBeforeFormatting: number | undefined = openFileVersions.get(modifiedDocument.toString()); - if (versionBeforeFormatting === undefined) { - return; - } - const formatTextEdits: vscode.TextEdit[] | undefined = await vscode.commands.executeCommand("vscode.executeFormatRangeProvider", modifiedDocument, formatRange, formatOptions); - if (formatTextEdits && formatTextEdits.length > 0) { - formatEdits.set(modifiedDocument, formatTextEdits); - } - if (formatEdits.size === 0 || versionBeforeFormatting === undefined) { - return; - } - // Only apply formatting if the document version hasn't changed to prevent - // stale formatting results from being applied. - const versionAfterFormatting: number | undefined = openFileVersions.get(modifiedDocument.toString()); - if (versionAfterFormatting === undefined || versionAfterFormatting > versionBeforeFormatting) { - return; - } - await vscode.workspace.applyEdit(formatEdits); - } - - public async handleExtractToFunction(extractAsGlobal: boolean): Promise { - const editor: vscode.TextEditor | undefined = vscode.window.activeTextEditor; - if (!editor || editor.selection.isEmpty) { - return; - } - - let functionName: string | undefined = await vscode.window.showInputBox({ - title: localize('handle.extract.name', 'Name the extracted function'), - placeHolder: localize('handle.extract.new.function', 'NewFunction') - }); - - if (functionName === undefined || functionName === "") { - functionName = "NewFunction"; - } - - const params: ExtractToFunctionParams = { - uri: editor.document.uri.toString(), - range: { - start: { - character: editor.selection.start.character, - line: editor.selection.start.line - }, - end: { - character: editor.selection.end.character, - line: editor.selection.end.line - } - }, - extractAsGlobal, - name: functionName - }; - - const result: WorkspaceEditResult = await this.languageClient.sendRequest(ExtractToFunctionRequest, params); - if (result.workspaceEdits === undefined) { - // The only condition in which result.edit would be undefined is a - // server-initiated cancellation, in which case the object is actually - // a ResponseError. https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#responseMessage - return; - } - - // Handle error messaging - if (result.errorText) { - void vscode.window.showErrorMessage(`${localize("handle.extract.error", - "Extract to function failed: {0}", result.errorText)}`); - return; - } - - let workspaceEdits: vscode.WorkspaceEdit = new vscode.WorkspaceEdit(); - - // NOTE: References to source/header are in reference to the more common case when it's - // invoked on the source file (alternatively named the first file). When invoked on the header file, - // the header file operates as if it were the source file (isSourceFile stays true). - let sourceReplaceEditRange: vscode.Range | undefined; - let headerReplaceEditRange: vscode.Range | undefined; - let hasProcessedReplace: boolean = false; - const sourceFormatUriAndRanges: VsCodeUriAndRange[] = []; - const headerFormatUriAndRanges: VsCodeUriAndRange[] = []; - let lineOffset: number = 0; - let headerFileLineOffset: number = 0; - let isSourceFile: boolean = true; - // There will be 4-5 text edits: - // - A #pragma once added to a new header file (optional) - // - Add #include for header file (optional) - // - Add the new function declaration (in the source or header file) - // - Replace the selected code with the new function call, - // plus possibly extra declarations beforehand, - // plus possibly extra return value handling afterwards. - // - Add the new function definition (below the selection) - for (const workspaceEdit of result.workspaceEdits) { - if (hasProcessedReplace) { - isSourceFile = false; - lineOffset = 0; - } - const uri: vscode.Uri = vscode.Uri.file(workspaceEdit.file); - let nextLineOffset: number = 0; - for (const edit of workspaceEdit.edits) { - let range: vscode.Range = makeVscodeRange(edit.range); - if (!isSourceFile && headerFileLineOffset) { - range = new vscode.Range(new vscode.Position(range.start.line + headerFileLineOffset, range.start.character), - new vscode.Position(range.end.line + headerFileLineOffset, range.end.character)); - } - const isReplace: boolean = !range.isEmpty && isSourceFile; - lineOffset += nextLineOffset; - nextLineOffset = (edit.newText.match(/\n/g) ?? []).length; - - // Find the editType. - if (isReplace) { - hasProcessedReplace = true; - workspaceEdits.replace(uri, range, edit.newText); - } else { - workspaceEdits.insert(uri, range.start, edit.newText); - if (edit.newText.length < 300) { // Avoid searching large code edits - if (isSourceFile && !hasProcessedReplace && edit.newText.includes("#include")) { - continue; - } - if (edit.newText.includes("#pragma once")) { - // Commit this so that it can be undone separately, to avoid leaving an empty file, - // which causes the next refactor to not add the #pragma once. - await vscode.workspace.applyEdit(workspaceEdits, { isRefactoring: true }); - headerFileLineOffset = nextLineOffset; - workspaceEdits = new vscode.WorkspaceEdit(); - continue; - } - } - } - const formatRangeStartLine: number = range.start.line + lineOffset; - let rangeStartLine: number = formatRangeStartLine; - let rangeStartCharacter: number = 0; - let startWithNewLine: boolean = true; - if (edit.newText.startsWith("\r\n\r\n")) { - rangeStartCharacter = 4; - rangeStartLine += 2; - } else if (edit.newText.startsWith("\n\n")) { - rangeStartCharacter = 2; - rangeStartLine += 2; - } else if (edit.newText.startsWith("\r\n")) { - rangeStartCharacter = 2; - rangeStartLine += 1; - } else if (edit.newText.startsWith("\n")) { - rangeStartCharacter = 1; - rangeStartLine += 1; - } else { - startWithNewLine = false; - } - const newFormatRange: vscode.Range = new vscode.Range( - new vscode.Position(formatRangeStartLine + (nextLineOffset < 0 ? nextLineOffset : 0), range.start.character), - new vscode.Position(formatRangeStartLine + (nextLineOffset < 0 ? 0 : nextLineOffset), - isReplace ? range.end.character : - ((startWithNewLine ? 0 : range.end.character) + (edit.newText.endsWith("\n") ? 0 : edit.newText.length - rangeStartCharacter)) - ) - ); - if (isSourceFile) { - sourceFormatUriAndRanges.push({ uri, range: newFormatRange }); - } else { - headerFormatUriAndRanges.push({ uri, range: newFormatRange }); - } - if (isReplace || !isSourceFile) { - // Handle additional declaration lines added before the new function call. - let currentText: string = edit.newText.substring(rangeStartCharacter); - let currentTextNextLineStart: number = currentText.indexOf("\n"); - let currentTextNewFunctionStart: number = currentText.indexOf(functionName); - let currentTextNextLineStartUpdated: boolean = false; - while (currentTextNextLineStart !== -1 && currentTextNextLineStart < currentTextNewFunctionStart) { - ++rangeStartLine; - currentText = currentText.substring(currentTextNextLineStart + 1); - currentTextNextLineStart = currentText.indexOf("\n"); - currentTextNewFunctionStart = currentText.indexOf(functionName); - currentTextNextLineStartUpdated = true; - } - rangeStartCharacter = (rangeStartCharacter === 0 && !currentTextNextLineStartUpdated ? range.start.character : 0) + - currentTextNewFunctionStart; - if (rangeStartCharacter < 0) { - // functionName is missing -- unexpected error. - void vscode.window.showErrorMessage(`${localize("invalid.edit", - "Extract to function failed. An invalid edit was generated: '{0}'", edit.newText)}`); - continue; - } - const replaceEditRange = new vscode.Range( - new vscode.Position(rangeStartLine, rangeStartCharacter), - new vscode.Position(rangeStartLine, rangeStartCharacter + functionName.length)); - if (isSourceFile) { - sourceReplaceEditRange = replaceEditRange; - } else { - headerReplaceEditRange = replaceEditRange; - } - nextLineOffset -= range.end.line - range.start.line; - } - } - } - - if (sourceReplaceEditRange === undefined || sourceFormatUriAndRanges.length === 0) { - return; - } - - // Apply the extract to function text edits. - await vscode.workspace.applyEdit(workspaceEdits, { isRefactoring: true }); - - if (headerFormatUriAndRanges.length > 0 && headerReplaceEditRange !== undefined) { - // The header needs to be open and shown or the formatting will fail - // (due to issues/requirements in the cpptools process). - // It also seems strange and undesirable to have the header modified - // without being opened because otherwise users may not realize that - // the header had changed (unless they view source control differences). - await vscode.window.showTextDocument(headerFormatUriAndRanges[0].uri, { - selection: headerReplaceEditRange, preserveFocus: false - }); - } - - // Format the new text edits. - let formatEdits: vscode.WorkspaceEdit = new vscode.WorkspaceEdit(); - const formatRanges = async (formatUriAndRanges: VsCodeUriAndRange[]) => { - if (formatUriAndRanges.length === 0) { - return; - } - const formatUriAndRange: VsCodeUriAndRange = formatUriAndRanges[0]; - const isMultipleFormatRanges: boolean = formatUriAndRanges.length > 1; - const settings: OtherSettings = new OtherSettings(vscode.workspace.getWorkspaceFolder(formatUriAndRange.uri)?.uri); - const formatOptions: vscode.FormattingOptions = { - insertSpaces: settings.editorInsertSpaces ?? true, - tabSize: settings.editorTabSize ?? 4, - onChanges: isMultipleFormatRanges, - preserveFocus: true - }; - - const tryFormat = async () => { - const versionBeforeFormatting: number | undefined = openFileVersions.get(formatUriAndRange.uri.toString()); - if (versionBeforeFormatting === undefined) { - return true; - } - - // Only use document (onChange) formatting when there are multiple ranges. - const formatTextEdits: vscode.TextEdit[] | undefined = isMultipleFormatRanges ? - await vscode.commands.executeCommand( - "vscode.executeFormatDocumentProvider", formatUriAndRange.uri, formatOptions) : - await vscode.commands.executeCommand( - "vscode.executeFormatRangeProvider", formatUriAndRange.uri, formatUriAndRange.range, formatOptions); - - if (!formatTextEdits || formatTextEdits.length === 0 || versionBeforeFormatting === undefined) { - return true; - } - // Only apply formatting if the document version hasn't changed to prevent - // stale formatting results from being applied. - const versionAfterFormatting: number | undefined = openFileVersions.get(formatUriAndRange.uri.toString()); - if (versionAfterFormatting === undefined || versionAfterFormatting > versionBeforeFormatting) { - return false; - } - formatEdits.set(formatUriAndRange.uri, formatTextEdits); - return true; - }; - if (!await tryFormat()) { - await tryFormat(); // Try again; - } - }; - - if (headerFormatUriAndRanges.length > 0 && headerReplaceEditRange !== undefined) { - await formatRanges(headerFormatUriAndRanges); - if (formatEdits.size > 0) { - // This showTextDocument is required in order to get the selection to be - // correct after the formatting edit is applied. It could be a VS Code bug. - await vscode.window.showTextDocument(headerFormatUriAndRanges[0].uri, { - selection: headerReplaceEditRange, preserveFocus: false - }); - await vscode.workspace.applyEdit(formatEdits, { isRefactoring: true }); - formatEdits = new vscode.WorkspaceEdit(); - } - } - - // Select the replaced code. - await vscode.window.showTextDocument(sourceFormatUriAndRanges[0].uri, { - selection: sourceReplaceEditRange, preserveFocus: false - }); - - await formatRanges(sourceFormatUriAndRanges); - if (formatEdits.size > 0) { - await vscode.workspace.applyEdit(formatEdits, { isRefactoring: true }); - } - } - - public onInterval(): void { - // These events can be discarded until the language client is ready. - // Don't queue them up with this.notifyWhenLanguageClientReady calls. - if (this.innerLanguageClient !== undefined && this.configuration !== undefined) { - void this.languageClient.sendNotification(IntervalTimerNotification).catch(logAndReturn.undefined); - this.configuration.checkCppProperties(); - this.configuration.checkCompileCommands(); - } - } - - public dispose(): void { - this.disposables.forEach((d) => d.dispose()); - this.disposables = []; - if (this.documentFormattingProviderDisposable) { - this.documentFormattingProviderDisposable.dispose(); - this.documentFormattingProviderDisposable = undefined; - } - if (this.formattingRangeProviderDisposable) { - this.formattingRangeProviderDisposable.dispose(); - this.formattingRangeProviderDisposable = undefined; - } - if (this.onTypeFormattingProviderDisposable) { - this.onTypeFormattingProviderDisposable.dispose(); - this.onTypeFormattingProviderDisposable = undefined; - } - if (this.codeFoldingProviderDisposable) { - this.codeFoldingProviderDisposable.dispose(); - this.codeFoldingProviderDisposable = undefined; - } - if (this.semanticTokensProviderDisposable) { - this.semanticTokensProviderDisposable.dispose(); - this.semanticTokensProviderDisposable = undefined; - } - this.model.dispose(); - } - - public static stopLanguageClient(): Thenable { - return languageClient ? languageClient.stop() : Promise.resolve(); - } - - public async handleReferencesIcon(): Promise { - await this.ready; - - workspaceReferences.UpdateProgressUICounter(this.model.referencesCommandMode.Value); - - // If the search is find all references, preview partial results. - // This will cause the language server to send partial results to display - // in the "Other References" view or channel. Doing a preview should not complete - // an in-progress request until it is finished or canceled. - if (this.ReferencesCommandMode === refs.ReferencesCommandMode.Find) { - void this.languageClient.sendNotification(PreviewReferencesNotification); - } - - } - - private serverCanceledReferences(): void { - workspaceReferences.cancelCurrentReferenceRequest(refs.CancellationSender.LanguageServer); - } - - private handleReferencesProgress(notificationBody: refs.ReportReferencesProgressNotification): void { - workspaceReferences.handleProgress(notificationBody); - } - - private processReferencesPreview(referencesResult: refs.ReferencesResult): void { - workspaceReferences.showResultsInPanelView(referencesResult); - } - - public setReferencesCommandMode(mode: refs.ReferencesCommandMode): void { - this.model.referencesCommandMode.Value = mode; - } - - public async addTrustedCompiler(path: string): Promise { - if (path === null || path === undefined) { - return; - } - if (trustedCompilerPaths.includes(path)) { - DebugConfigurationProvider.ClearDetectedBuildTasks(); - return; - } - trustedCompilerPaths.push(path); - compilerDefaults = await this.requestCompiler(path); - DebugConfigurationProvider.ClearDetectedBuildTasks(); - } -} - -function getLanguageServerFileName(): string { - let extensionProcessName: string = 'cpptools'; - const plat: NodeJS.Platform = process.platform; - if (plat === 'win32') { - extensionProcessName += '.exe'; - } else if (plat !== 'linux' && plat !== 'darwin') { - throw "Invalid Platform"; - } - return path.resolve(util.getExtensionFilePath("bin"), extensionProcessName); -} - -/* eslint-disable @typescript-eslint/no-unused-vars */ -class NullClient implements Client { - private booleanEvent = new vscode.EventEmitter(); - private numberEvent = new vscode.EventEmitter(); - private stringEvent = new vscode.EventEmitter(); - private referencesCommandModeEvent = new vscode.EventEmitter(); - - readonly ready: Promise = Promise.resolve(); - - async enqueue(task: () => Promise) { - return task(); - } - public get InitializingWorkspaceChanged(): vscode.Event { return this.booleanEvent.event; } - public get IndexingWorkspaceChanged(): vscode.Event { return this.booleanEvent.event; } - public get ParsingWorkspaceChanged(): vscode.Event { return this.booleanEvent.event; } - public get ParsingWorkspacePausedChanged(): vscode.Event { return this.booleanEvent.event; } - public get ParsingFilesChanged(): vscode.Event { return this.booleanEvent.event; } - public get IntelliSenseParsingChanged(): vscode.Event { return this.booleanEvent.event; } - public get RunningCodeAnalysisChanged(): vscode.Event { return this.booleanEvent.event; } - public get CodeAnalysisPausedChanged(): vscode.Event { return this.booleanEvent.event; } - public get CodeAnalysisProcessedChanged(): vscode.Event { return this.numberEvent.event; } - public get CodeAnalysisTotalChanged(): vscode.Event { return this.numberEvent.event; } - public get ReferencesCommandModeChanged(): vscode.Event { return this.referencesCommandModeEvent.event; } - public get TagParserStatusChanged(): vscode.Event { return this.stringEvent.event; } - public get ActiveConfigChanged(): vscode.Event { return this.stringEvent.event; } - RootPath: string = "/"; - RootRealPath: string = "/"; - RootUri?: vscode.Uri = vscode.Uri.file("/"); - Name: string = "(empty)"; - TrackedDocuments = new Map(); - async onDidChangeSettings(event: vscode.ConfigurationChangeEvent): Promise> { return {}; } - onDidOpenTextDocument(document: vscode.TextDocument): void { } - onDidCloseTextDocument(document: vscode.TextDocument): void { } - onDidChangeVisibleTextEditors(editors: readonly vscode.TextEditor[]): Promise { return Promise.resolve(); } - onDidChangeTextEditorVisibleRanges(uri: vscode.Uri): Promise { return Promise.resolve(); } - onDidChangeTextDocument(textDocumentChangeEvent: vscode.TextDocumentChangeEvent): void { } - onRegisterCustomConfigurationProvider(provider: CustomConfigurationProvider1): Thenable { return Promise.resolve(); } - updateCustomConfigurations(requestingProvider?: CustomConfigurationProvider1): Thenable { return Promise.resolve(); } - updateCustomBrowseConfiguration(requestingProvider?: CustomConfigurationProvider1): Thenable { return Promise.resolve(); } - provideCustomConfiguration(docUri: vscode.Uri): Promise { return Promise.resolve(); } - logDiagnostics(): Promise { return Promise.resolve(); } - rescanFolder(): Promise { return Promise.resolve(); } - toggleReferenceResultsView(): void { } - setCurrentConfigName(configurationName: string): Thenable { return Promise.resolve(); } - getCurrentConfigName(): Thenable { return Promise.resolve(""); } - getCurrentConfigCustomVariable(variableName: string): Thenable { return Promise.resolve(""); } - getVcpkgInstalled(): Thenable { return Promise.resolve(false); } - getVcpkgEnabled(): Thenable { return Promise.resolve(false); } - getCurrentCompilerPathAndArgs(): Thenable { return Promise.resolve(undefined); } - 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(""); } - updateActiveDocumentTextOptions(): void { } - didChangeActiveEditor(editor?: vscode.TextEditor): Promise { return Promise.resolve(); } - restartIntelliSenseForFile(document: vscode.TextDocument): Promise { return Promise.resolve(); } - activate(): void { } - selectionChanged(selection: Range): void { } - resetDatabase(): void { } - promptSelectIntelliSenseConfiguration(sender?: any): Promise { return Promise.resolve(); } - rescanCompilers(sender?: any): Promise { return Promise.resolve(); } - deactivate(): void { } - pauseParsing(): void { } - resumeParsing(): void { } - PauseCodeAnalysis(): void { } - ResumeCodeAnalysis(): void { } - CancelCodeAnalysis(): void { } - handleConfigurationSelectCommand(): Promise { return Promise.resolve(); } - handleConfigurationProviderSelectCommand(): Promise { return Promise.resolve(); } - handleShowActiveCodeAnalysisCommands(): Promise { return Promise.resolve(); } - handleShowIdleCodeAnalysisCommands(): Promise { return Promise.resolve(); } - handleReferencesIcon(): void { } - handleConfigurationEditCommand(viewColumn?: vscode.ViewColumn): void { } - handleConfigurationEditJSONCommand(viewColumn?: vscode.ViewColumn): void { } - handleConfigurationEditUICommand(viewColumn?: vscode.ViewColumn): void { } - handleAddToIncludePathCommand(path: string): void { } - handleGoToDirectiveInGroup(next: boolean): Promise { return Promise.resolve(); } - handleGenerateDoxygenComment(args: DoxygenCodeActionCommandArguments | vscode.Uri | undefined): Promise { return Promise.resolve(); } - handleRunCodeAnalysisOnActiveFile(): Promise { return Promise.resolve(); } - handleRunCodeAnalysisOnOpenFiles(): Promise { return Promise.resolve(); } - handleRunCodeAnalysisOnAllFiles(): Promise { return Promise.resolve(); } - handleRemoveAllCodeAnalysisProblems(): Promise { return Promise.resolve(); } - handleRemoveCodeAnalysisProblems(refreshSquigglesOnSave: boolean, identifiersAndUris: CodeAnalysisDiagnosticIdentifiersAndUri[]): Promise { return Promise.resolve(); } - handleFixCodeAnalysisProblems(workspaceEdit: vscode.WorkspaceEdit, refreshSquigglesOnSave: boolean, identifiersAndUris: CodeAnalysisDiagnosticIdentifiersAndUri[]): Promise { return Promise.resolve(); } - handleDisableAllTypeCodeAnalysisProblems(code: string, identifiersAndUris: CodeAnalysisDiagnosticIdentifiersAndUri[]): Promise { return Promise.resolve(); } - handleCreateDeclarationOrDefinition(isCopyToClipboard: boolean, codeActionRange?: Range): Promise { return Promise.resolve(); } - handleExtractToFunction(extractAsGlobal: boolean): Promise { return Promise.resolve(); } - onInterval(): void { } - dispose(): void { - this.booleanEvent.dispose(); - this.stringEvent.dispose(); - } - addFileAssociations(fileAssociations: string, languageId: string): void { } - sendDidChangeSettings(): void { } - isInitialized(): boolean { return true; } - getShowConfigureIntelliSenseButton(): boolean { return false; } - setShowConfigureIntelliSenseButton(show: boolean): void { } - addTrustedCompiler(path: string): Promise { return Promise.resolve(); } - getIncludes(maxDepth: number, token: vscode.CancellationToken): Promise { return Promise.resolve({} as GetIncludesResult); } - getChatContext(token: vscode.CancellationToken): Promise { return Promise.resolve({} as ChatContextResult); } -} +/* -------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All Rights Reserved. + * See 'LICENSE' in the project root for license information. + * ------------------------------------------------------------------------------------------ */ +'use strict'; + +import * as path from 'path'; +import * as vscode from 'vscode'; + +// Start provider imports +import { CallHierarchyProvider } from './Providers/callHierarchyProvider'; +import { CodeActionProvider } from './Providers/codeActionProvider'; +import { DocumentFormattingEditProvider } from './Providers/documentFormattingEditProvider'; +import { DocumentRangeFormattingEditProvider } from './Providers/documentRangeFormattingEditProvider'; +import { DocumentSymbolProvider } from './Providers/documentSymbolProvider'; +import { FindAllReferencesProvider } from './Providers/findAllReferencesProvider'; +import { FoldingRangeProvider } from './Providers/foldingRangeProvider'; +import { CppInlayHint, InlayHintsProvider } from './Providers/inlayHintProvider'; +import { OnTypeFormattingEditProvider } from './Providers/onTypeFormattingEditProvider'; +import { RenameProvider } from './Providers/renameProvider'; +import { SemanticToken, SemanticTokensProvider } from './Providers/semanticTokensProvider'; +import { WorkspaceSymbolProvider } from './Providers/workspaceSymbolProvider'; +// End provider imports + +import { ok } from 'assert'; +import * as fs from 'fs'; +import * as os from 'os'; +import { SourceFileConfiguration, SourceFileConfigurationItem, Version, WorkspaceBrowseConfiguration } from 'vscode-cpptools'; +import { IntelliSenseStatus, Status } from 'vscode-cpptools/out/testApi'; +import { CloseAction, DidOpenTextDocumentParams, ErrorAction, LanguageClientOptions, NotificationType, Position, Range, RequestType, ResponseError, TextDocumentIdentifier, TextDocumentPositionParams } from 'vscode-languageclient'; +import { LanguageClient, ServerOptions } from 'vscode-languageclient/node'; +import * as nls from 'vscode-nls'; +import { DebugConfigurationProvider } from '../Debugger/configurationProvider'; +import { CustomConfigurationProvider1, getCustomConfigProviders, isSameProviderExtensionId } from '../LanguageServer/customProviders'; +import { ManualPromise } from '../Utility/Async/manualPromise'; +import { ManualSignal } from '../Utility/Async/manualSignal'; +import { logAndReturn } from '../Utility/Async/returns'; +import { is } from '../Utility/System/guards'; +import * as util from '../common'; +import { isWindows } from '../constants'; +import { instrument, isInstrumentationEnabled } from '../instrumentation'; +import { DebugProtocolParams, Logger, ShowWarningParams, getDiagnosticsChannel, getOutputChannelLogger, logDebugProtocol, logLocalized, showWarning } from '../logger'; +import { localizedStringCount, lookupString } from '../nativeStrings'; +import { SessionState } from '../sessionState'; +import * as telemetry from '../telemetry'; +import { TestHook, getTestHook } from '../testHook'; +import { HoverProvider } from './Providers/HoverProvider'; +import { + CodeAnalysisDiagnosticIdentifiersAndUri, + RegisterCodeAnalysisNotifications, + RemoveCodeAnalysisProblemsParams, + removeAllCodeAnalysisProblems, + removeCodeAnalysisProblems +} from './codeAnalysis'; +import { Location, TextEdit, WorkspaceEdit } from './commonTypes'; +import * as configs from './configurations'; +import { DataBinding } from './dataBinding'; +import { cachedEditorConfigSettings, getEditorConfigSettings } from './editorConfig'; +import { CppSourceStr, clients, configPrefix, updateLanguageConfigurations, usesCrashHandler, watchForCrashes } from './extension'; +import { LocalizeStringParams, getLocaleId, getLocalizedString } from './localization'; +import { PersistentFolderState, PersistentWorkspaceState } from './persistentState'; +import { RequestCancelled, ServerCancelled, createProtocolFilter } from './protocolFilter'; +import * as refs from './references'; +import { CppSettings, OtherSettings, SettingsParams, WorkspaceFolderSettingsParams } from './settings'; +import { SettingsTracker } from './settingsTracker'; +import { ConfigurationType, LanguageStatusUI, getUI } from './ui'; +import { handleChangedFromCppToC, makeLspRange, makeVscodeLocation, makeVscodeRange, withCancellation } from './utils'; +import minimatch = require("minimatch"); + +function deepCopy(obj: any) { + return JSON.parse(JSON.stringify(obj)); +} +nls.config({ messageFormat: nls.MessageFormat.bundle, bundleFormat: nls.BundleFormat.standalone })(); +const localize: nls.LocalizeFunc = nls.loadMessageBundle(); + +let ui: LanguageStatusUI; +let timeStamp: number = 0; +const configProviderTimeout: number = 2000; +let initializedClientCount: number = 0; + +// Compiler paths that are known to be acceptable to execute. +const trustedCompilerPaths: string[] = []; +export function hasTrustedCompilerPaths(): boolean { + return trustedCompilerPaths.length !== 0; +} + +// Data shared by all clients. +let languageClient: LanguageClient; +let firstClientStarted: Promise; +let languageClientCrashedNeedsRestart: boolean = false; +const languageClientCrashTimes: number[] = []; +let compilerDefaults: configs.CompilerDefaults | undefined; +let diagnosticsCollectionIntelliSense: vscode.DiagnosticCollection; +let diagnosticsCollectionRefactor: vscode.DiagnosticCollection; + +interface ConfigStateReceived { + compilers: boolean; + compileCommands: boolean; + configProviders?: CustomConfigurationProvider1[]; + timeout: boolean; +} + +let workspaceHash: string = ""; + +let workspaceDisposables: vscode.Disposable[] = []; +export let workspaceReferences: refs.ReferencesManager; +export const openFileVersions: Map = new Map(); +export const cachedEditorConfigLookups: Map = new Map(); +export let semanticTokensLegend: vscode.SemanticTokensLegend | undefined; + +export function disposeWorkspaceData(): void { + workspaceDisposables.forEach((d) => d.dispose()); + workspaceDisposables = []; +} + +/** Note: We should not await on the following functions, + * or any function that returns a promise acquired from them, + * vscode.window.showInformationMessage, vscode.window.showWarningMessage, vscode.window.showErrorMessage +*/ +function showMessageWindow(params: ShowMessageWindowParams): void { + const message: string = getLocalizedString(params.localizeStringParams); + switch (params.type) { + case 1: // Error + void vscode.window.showErrorMessage(message); + break; + case 2: // Warning + void vscode.window.showWarningMessage(message); + break; + case 3: // Info + void vscode.window.showInformationMessage(message); + break; + default: + console.assert("Unrecognized type for showMessageWindow"); + break; + } +} + +function publishRefactorDiagnostics(params: PublishRefactorDiagnosticsParams): void { + if (!diagnosticsCollectionRefactor) { + diagnosticsCollectionRefactor = vscode.languages.createDiagnosticCollection(configPrefix + "Refactor"); + } + + const newDiagnostics: vscode.Diagnostic[] = []; + params.diagnostics.forEach((d) => { + const message: string = getLocalizedString(d.localizeStringParams); + const diagnostic: vscode.Diagnostic = new vscode.Diagnostic(makeVscodeRange(d.range), message, d.severity); + diagnostic.code = d.code; + diagnostic.source = CppSourceStr; + if (d.relatedInformation) { + diagnostic.relatedInformation = []; + for (const info of d.relatedInformation) { + diagnostic.relatedInformation.push(new vscode.DiagnosticRelatedInformation(makeVscodeLocation(info.location), info.message)); + } + } + + newDiagnostics.push(diagnostic); + }); + + const fileUri: vscode.Uri = vscode.Uri.parse(params.uri); + diagnosticsCollectionRefactor.set(fileUri, newDiagnostics); +} + +interface WorkspaceFolderParams { + workspaceFolderUri?: string; +} + +interface SelectionParams { + uri: string; + range: Range; +} + +export interface VsCodeUriAndRange { + uri: vscode.Uri; + range: vscode.Range; +} + +interface WorkspaceEditResult { + workspaceEdits: WorkspaceEdit[]; + errorText?: string; +} + +interface TelemetryPayload { + event: string; + properties?: Record; + metrics?: Record; +} + +interface ReportStatusNotificationBody extends WorkspaceFolderParams { + status: string; +} + +interface QueryDefaultCompilerParams { + newTrustedCompilerPath: string; +} + +interface CppPropertiesParams extends WorkspaceFolderParams { + currentConfiguration: number; + configurations: configs.Configuration[]; + isReady?: boolean; +} + +interface FolderSelectedSettingParams extends WorkspaceFolderParams { + currentConfiguration: number; +} + +interface SwitchHeaderSourceParams extends WorkspaceFolderParams { + switchHeaderSourceFileName: string; +} + +interface FileChangedParams extends WorkspaceFolderParams { + uri: string; +} + +interface InputRegion { + startLine: number; + endLine: number; +} + +interface DecorationRangesPair { + decoration: vscode.TextEditorDecorationType; + ranges: vscode.Range[]; +} + +interface InternalSourceFileConfiguration extends SourceFileConfiguration { + compilerArgsLegacy?: string[]; +} + +interface InternalWorkspaceBrowseConfiguration extends WorkspaceBrowseConfiguration { + compilerArgsLegacy?: string[]; +} + +// Need to convert vscode.Uri to a string before sending it to the language server. +interface SourceFileConfigurationItemAdapter { + uri: string; + configuration: InternalSourceFileConfiguration; +} + +interface CustomConfigurationParams extends WorkspaceFolderParams { + configurationItems: SourceFileConfigurationItemAdapter[]; +} + +interface CustomBrowseConfigurationParams extends WorkspaceFolderParams { + browseConfiguration: InternalWorkspaceBrowseConfiguration; +} + +interface CompileCommandsPaths extends WorkspaceFolderParams { + paths: string[]; +} + +interface GetDiagnosticsResult { + diagnostics: string; +} + +interface IntelliSenseDiagnosticRelatedInformation { + location: Location; + message: string; +} + +interface RefactorDiagnosticRelatedInformation { + location: Location; + message: string; +} + +interface IntelliSenseDiagnostic { + range: Range; + code?: number; + severity: vscode.DiagnosticSeverity; + localizeStringParams: LocalizeStringParams; + relatedInformation?: IntelliSenseDiagnosticRelatedInformation[]; +} + +interface RefactorDiagnostic { + range: Range; + code?: number; + severity: vscode.DiagnosticSeverity; + localizeStringParams: LocalizeStringParams; + relatedInformation?: RefactorDiagnosticRelatedInformation[]; +} + +interface PublishRefactorDiagnosticsParams { + uri: string; + diagnostics: RefactorDiagnostic[]; +} + +export interface CreateDeclarationOrDefinitionParams extends SelectionParams { + formatParams: FormatParams; + copyToClipboard: boolean; +} + +export interface CreateDeclarationOrDefinitionResult extends WorkspaceEditResult { + clipboardText?: string; +} + +export interface ExtractToFunctionParams extends SelectionParams { + extractAsGlobal: boolean; + name: string; +} + +interface ShowMessageWindowParams { + type: number; + localizeStringParams: LocalizeStringParams; +} + +export interface GetDocumentSymbolRequestParams { + uri: string; +} + +export interface WorkspaceSymbolParams extends WorkspaceFolderParams { + query: string; + experimentEnabled: boolean; +} + +export enum SymbolScope { + Public = 0, + Protected = 1, + Private = 2 +} + +export interface LocalizeDocumentSymbol { + name: string; + detail: LocalizeStringParams; + kind: vscode.SymbolKind; + scope: SymbolScope; + range: Range; + selectionRange: Range; + children: LocalizeDocumentSymbol[]; +} + +export interface GetDocumentSymbolResult { + symbols: LocalizeDocumentSymbol[]; +} + +export interface LocalizeSymbolInformation { + name: string; + kind: vscode.SymbolKind; + scope: SymbolScope; + location: Location; + containerName: string; + suffix: LocalizeStringParams; +} + +export interface FormatParams extends SelectionParams { + character: string; + insertSpaces: boolean; + tabSize: number; + editorConfigSettings: any; + useVcFormat: boolean; + onChanges: boolean; +} + +export interface FormatResult { + edits: TextEdit[]; +} + +export interface GetFoldingRangesParams { + uri: string; +} + +export enum FoldingRangeKind { + None = 0, + Comment = 1, + Imports = 2, + Region = 3 +} + +export interface CppFoldingRange { + kind: FoldingRangeKind; + range: InputRegion; +} + +export interface GetFoldingRangesResult { + ranges: CppFoldingRange[]; +} + +export interface IntelliSenseResult { + uri: string; + fileVersion: number; + diagnostics: IntelliSenseDiagnostic[]; + inactiveRegions: InputRegion[]; + semanticTokens: SemanticToken[]; + inlayHints: CppInlayHint[]; + clearExistingDiagnostics: boolean; + clearExistingInactiveRegions: boolean; + clearExistingSemanticTokens: boolean; + clearExistingInlayHint: boolean; + isCompletePass: boolean; +} + +enum SemanticTokenTypes { + // These are camelCase as the enum names are used directly as strings in our legend. + macro = 0, + enumMember = 1, + variable = 2, + parameter = 3, + type = 4, + referenceType = 5, + valueType = 6, + function = 7, + method = 8, + property = 9, + cliProperty = 10, + event = 11, + genericType = 12, + templateFunction = 13, + templateType = 14, + namespace = 15, + label = 16, + customLiteral = 17, + numberLiteral = 18, + stringLiteral = 19, + operatorOverload = 20, + memberOperatorOverload = 21, + newOperator = 22 +} + +enum SemanticTokenModifiers { + // These are camelCase as the enum names are used directly as strings in our legend. + static = 0b001, + global = 0b010, + local = 0b100 +} + +interface IntelliSenseSetup { + uri: string; +} + +interface GoToDirectiveInGroupParams { + uri: string; + position: Position; + next: boolean; +} + +export interface GenerateDoxygenCommentParams { + uri: string; + position: Position; + isCodeAction: boolean; + isCursorAboveSignatureLine: boolean | undefined; +} + +export interface GenerateDoxygenCommentResult { + contents: string; + initPosition: Position; + finalInsertionLine: number; + finalCursorPosition: Position; + fileVersion: number; + isCursorAboveSignatureLine: boolean; +} + +export interface IndexableQuickPickItem extends vscode.QuickPickItem { + index: number; +} + +export interface UpdateTrustedCompilerPathsResult { + compilerPath: string; +} + +export interface DoxygenCodeActionCommandArguments { + initialCursor: Position; + adjustedCursor: Position; + isCursorAboveSignatureLine: boolean; +} + +interface SetTemporaryTextDocumentLanguageParams { + uri: string; + isC: boolean; + isCuda: boolean; +} + +enum CodeAnalysisScope { + ActiveFile, + OpenFiles, + AllFiles, + ClearSquiggles +} + +interface CodeAnalysisParams { + scope: CodeAnalysisScope; +} + +interface FinishedRequestCustomConfigParams { + uri: string; +} + +export interface TextDocumentWillSaveParams { + textDocument: TextDocumentIdentifier; + reason: vscode.TextDocumentSaveReason; +} + +interface LspInitializationOptions { + loggingLevel: number; +} + +interface CppInitializationParams { + packageVersion: string; + extensionPath: string; + cacheStoragePath: string; + workspaceStoragePath: string; + databaseStoragePath: string; + vcpkgRoot: string; + intelliSenseCacheDisabled: boolean; + caseSensitiveFileSupport: boolean; + resetDatabase: boolean; + edgeMessagesDirectory: string; + localizedStrings: string[]; + settings: SettingsParams; +} + +interface TagParseStatus { + localizeStringParams: LocalizeStringParams; + isPaused: boolean; +} + +interface DidChangeVisibleTextEditorsParams { + activeUri?: string; + activeSelection?: Range; + visibleRanges?: { [uri: string]: Range[] }; +} + +interface DidChangeTextEditorVisibleRangesParams { + uri: string; + visibleRanges: Range[]; +} + +interface DidChangeActiveEditorParams { + uri?: string; + selection?: Range; +} + +interface GetIncludesParams { + maxDepth: number; +} + +export interface GetIncludesResult { + includedFiles: string[]; +} + +export interface ChatContextResult { + language: string; + standardVersion: string; + compiler: string; + targetPlatform: string; + targetArchitecture: string; +} + +// Requests +const PreInitializationRequest: RequestType = new RequestType('cpptools/preinitialize'); +const InitializationRequest: RequestType = new RequestType('cpptools/initialize'); +const QueryCompilerDefaultsRequest: RequestType = new RequestType('cpptools/queryCompilerDefaults'); +const SwitchHeaderSourceRequest: RequestType = new RequestType('cpptools/didSwitchHeaderSource'); +const GetDiagnosticsRequest: RequestType = new RequestType('cpptools/getDiagnostics'); +export const GetDocumentSymbolRequest: RequestType = new RequestType('cpptools/getDocumentSymbols'); +export const GetSymbolInfoRequest: RequestType = new RequestType('cpptools/getWorkspaceSymbols'); +export const GetFoldingRangesRequest: RequestType = new RequestType('cpptools/getFoldingRanges'); +export const FormatDocumentRequest: RequestType = new RequestType('cpptools/formatDocument'); +export const FormatRangeRequest: RequestType = new RequestType('cpptools/formatRange'); +export const FormatOnTypeRequest: RequestType = new RequestType('cpptools/formatOnType'); +export const HoverRequest: RequestType = new RequestType('cpptools/hover'); +const CreateDeclarationOrDefinitionRequest: RequestType = new RequestType('cpptools/createDeclDef'); +const ExtractToFunctionRequest: RequestType = new RequestType('cpptools/extractToFunction'); +const GoToDirectiveInGroupRequest: RequestType = new RequestType('cpptools/goToDirectiveInGroup'); +const GenerateDoxygenCommentRequest: RequestType = new RequestType('cpptools/generateDoxygenComment'); +const ChangeCppPropertiesRequest: RequestType = new RequestType('cpptools/didChangeCppProperties'); +const IncludesRequest: RequestType = new RequestType('cpptools/getIncludes'); +const CppContextRequest: RequestType = new RequestType('cpptools/getChatContext'); + +// Notifications to the server +const DidOpenNotification: NotificationType = new NotificationType('textDocument/didOpen'); +const FileCreatedNotification: NotificationType = new NotificationType('cpptools/fileCreated'); +const FileChangedNotification: NotificationType = new NotificationType('cpptools/fileChanged'); +const FileDeletedNotification: NotificationType = new NotificationType('cpptools/fileDeleted'); +const ResetDatabaseNotification: NotificationType = new NotificationType('cpptools/resetDatabase'); +const PauseParsingNotification: NotificationType = new NotificationType('cpptools/pauseParsing'); +const ResumeParsingNotification: NotificationType = new NotificationType('cpptools/resumeParsing'); +const DidChangeActiveEditorNotification: NotificationType = new NotificationType('cpptools/didChangeActiveEditor'); +const RestartIntelliSenseForFileNotification: NotificationType = new NotificationType('cpptools/restartIntelliSenseForFile'); +const DidChangeTextEditorSelectionNotification: NotificationType = new NotificationType('cpptools/didChangeTextEditorSelection'); +const ChangeCompileCommandsNotification: NotificationType = new NotificationType('cpptools/didChangeCompileCommands'); +const ChangeSelectedSettingNotification: NotificationType = new NotificationType('cpptools/didChangeSelectedSetting'); +const IntervalTimerNotification: NotificationType = new NotificationType('cpptools/onIntervalTimer'); +const CustomConfigurationHighPriorityNotification: NotificationType = new NotificationType('cpptools/didChangeCustomConfigurationHighPriority'); +const CustomConfigurationNotification: NotificationType = new NotificationType('cpptools/didChangeCustomConfiguration'); +const CustomBrowseConfigurationNotification: NotificationType = new NotificationType('cpptools/didChangeCustomBrowseConfiguration'); +const ClearCustomConfigurationsNotification: NotificationType = new NotificationType('cpptools/clearCustomConfigurations'); +const ClearCustomBrowseConfigurationNotification: NotificationType = new NotificationType('cpptools/clearCustomBrowseConfiguration'); +const PreviewReferencesNotification: NotificationType = new NotificationType('cpptools/previewReferences'); +const RescanFolderNotification: NotificationType = new NotificationType('cpptools/rescanFolder'); +const FinishedRequestCustomConfig: NotificationType = new NotificationType('cpptools/finishedRequestCustomConfig'); +const DidChangeSettingsNotification: NotificationType = new NotificationType('cpptools/didChangeSettings'); +const DidChangeVisibleTextEditorsNotification: NotificationType = new NotificationType('cpptools/didChangeVisibleTextEditors'); +const DidChangeTextEditorVisibleRangesNotification: NotificationType = new NotificationType('cpptools/didChangeTextEditorVisibleRanges'); + +const CodeAnalysisNotification: NotificationType = new NotificationType('cpptools/runCodeAnalysis'); +const PauseCodeAnalysisNotification: NotificationType = new NotificationType('cpptools/pauseCodeAnalysis'); +const ResumeCodeAnalysisNotification: NotificationType = new NotificationType('cpptools/resumeCodeAnalysis'); +const CancelCodeAnalysisNotification: NotificationType = new NotificationType('cpptools/cancelCodeAnalysis'); +const RemoveCodeAnalysisProblemsNotification: NotificationType = new NotificationType('cpptools/removeCodeAnalysisProblems'); + +// Notifications from the server +const ReloadWindowNotification: NotificationType = new NotificationType('cpptools/reloadWindow'); +const UpdateTrustedCompilersNotification: NotificationType = new NotificationType('cpptools/updateTrustedCompilersList'); +const LogTelemetryNotification: NotificationType = new NotificationType('cpptools/logTelemetry'); +const ReportTagParseStatusNotification: NotificationType = new NotificationType('cpptools/reportTagParseStatus'); +const ReportStatusNotification: NotificationType = new NotificationType('cpptools/reportStatus'); +const DebugProtocolNotification: NotificationType = new NotificationType('cpptools/debugProtocol'); +const DebugLogNotification: NotificationType = new NotificationType('cpptools/debugLog'); +const CompileCommandsPathsNotification: NotificationType = new NotificationType('cpptools/compileCommandsPaths'); +const ReferencesNotification: NotificationType = new NotificationType('cpptools/references'); +const ReportReferencesProgressNotification: NotificationType = new NotificationType('cpptools/reportReferencesProgress'); +const RequestCustomConfig: NotificationType = new NotificationType('cpptools/requestCustomConfig'); +const PublishRefactorDiagnosticsNotification: NotificationType = new NotificationType('cpptools/publishRefactorDiagnostics'); +const ShowMessageWindowNotification: NotificationType = new NotificationType('cpptools/showMessageWindow'); +const ShowWarningNotification: NotificationType = new NotificationType('cpptools/showWarning'); +const ReportTextDocumentLanguage: NotificationType = new NotificationType('cpptools/reportTextDocumentLanguage'); +const IntelliSenseSetupNotification: NotificationType = new NotificationType('cpptools/IntelliSenseSetup'); +const SetTemporaryTextDocumentLanguageNotification: NotificationType = new NotificationType('cpptools/setTemporaryTextDocumentLanguage'); +const ReportCodeAnalysisProcessedNotification: NotificationType = new NotificationType('cpptools/reportCodeAnalysisProcessed'); +const ReportCodeAnalysisTotalNotification: NotificationType = new NotificationType('cpptools/reportCodeAnalysisTotal'); +const DoxygenCommentGeneratedNotification: NotificationType = new NotificationType('cpptools/insertDoxygenComment'); +const CanceledReferencesNotification: NotificationType = new NotificationType('cpptools/canceledReferences'); +const IntelliSenseResultNotification: NotificationType = new NotificationType('cpptools/intelliSenseResult'); + +let failureMessageShown: boolean = false; + +class ClientModel { + public isInitializingWorkspace: DataBinding; + public isIndexingWorkspace: DataBinding; + public isParsingWorkspace: DataBinding; + public isParsingWorkspacePaused: DataBinding; + public isParsingFiles: DataBinding; + public isUpdatingIntelliSense: DataBinding; + public isRunningCodeAnalysis: DataBinding; + public isCodeAnalysisPaused: DataBinding; + public codeAnalysisProcessed: DataBinding; + public codeAnalysisTotal: DataBinding; + public referencesCommandMode: DataBinding; + public parsingWorkspaceStatus: DataBinding; + public activeConfigName: DataBinding; + + constructor() { + this.isInitializingWorkspace = new DataBinding(false); + this.isIndexingWorkspace = new DataBinding(false); + + // The following elements add a delay of 500ms before notitfying the UI that the icon can hide itself. + this.isParsingWorkspace = new DataBinding(false, 500, false); + this.isParsingWorkspacePaused = new DataBinding(false, 500, false); + this.isParsingFiles = new DataBinding(false, 500, false); + this.isUpdatingIntelliSense = new DataBinding(false, 500, false); + + this.isRunningCodeAnalysis = new DataBinding(false); + this.isCodeAnalysisPaused = new DataBinding(false); + this.codeAnalysisProcessed = new DataBinding(0); + this.codeAnalysisTotal = new DataBinding(0); + this.referencesCommandMode = new DataBinding(refs.ReferencesCommandMode.None); + this.parsingWorkspaceStatus = new DataBinding(""); + this.activeConfigName = new DataBinding(""); + } + + public activate(): void { + this.isInitializingWorkspace.activate(); + this.isIndexingWorkspace.activate(); + this.isParsingWorkspace.activate(); + this.isParsingWorkspacePaused.activate(); + this.isParsingFiles.activate(); + this.isUpdatingIntelliSense.activate(); + this.isRunningCodeAnalysis.activate(); + this.isCodeAnalysisPaused.activate(); + this.codeAnalysisProcessed.activate(); + this.codeAnalysisTotal.activate(); + this.referencesCommandMode.activate(); + this.parsingWorkspaceStatus.activate(); + this.activeConfigName.activate(); + } + + public deactivate(): void { + this.isInitializingWorkspace.deactivate(); + this.isIndexingWorkspace.deactivate(); + this.isParsingWorkspace.deactivate(); + this.isParsingWorkspacePaused.deactivate(); + this.isParsingFiles.deactivate(); + this.isUpdatingIntelliSense.deactivate(); + this.isRunningCodeAnalysis.deactivate(); + this.isCodeAnalysisPaused.deactivate(); + this.codeAnalysisProcessed.deactivate(); + this.codeAnalysisTotal.deactivate(); + this.referencesCommandMode.deactivate(); + this.parsingWorkspaceStatus.deactivate(); + this.activeConfigName.deactivate(); + } + + public dispose(): void { + this.isInitializingWorkspace.dispose(); + this.isIndexingWorkspace.dispose(); + this.isParsingWorkspace.dispose(); + this.isParsingWorkspacePaused.dispose(); + this.isParsingFiles.dispose(); + this.isUpdatingIntelliSense.dispose(); + this.isRunningCodeAnalysis.dispose(); + this.isCodeAnalysisPaused.dispose(); + this.codeAnalysisProcessed.dispose(); + this.codeAnalysisTotal.dispose(); + this.referencesCommandMode.dispose(); + this.parsingWorkspaceStatus.dispose(); + this.activeConfigName.dispose(); + } +} + +export interface Client { + readonly ready: Promise; + enqueue(task: () => Promise): Promise; + InitializingWorkspaceChanged: vscode.Event; + IndexingWorkspaceChanged: vscode.Event; + ParsingWorkspaceChanged: vscode.Event; + ParsingWorkspacePausedChanged: vscode.Event; + ParsingFilesChanged: vscode.Event; + IntelliSenseParsingChanged: vscode.Event; + RunningCodeAnalysisChanged: vscode.Event; + CodeAnalysisPausedChanged: vscode.Event; + CodeAnalysisProcessedChanged: vscode.Event; + CodeAnalysisTotalChanged: vscode.Event; + ReferencesCommandModeChanged: vscode.Event; + TagParserStatusChanged: vscode.Event; + ActiveConfigChanged: vscode.Event; + RootPath: string; + RootRealPath: string; + RootUri?: vscode.Uri; + RootFolder?: vscode.WorkspaceFolder; + Name: string; + TrackedDocuments: Map; + onDidChangeSettings(event: vscode.ConfigurationChangeEvent): Promise>; + onDidOpenTextDocument(document: vscode.TextDocument): void; + onDidCloseTextDocument(document: vscode.TextDocument): void; + onDidChangeVisibleTextEditors(editors: readonly vscode.TextEditor[]): Promise; + onDidChangeTextEditorVisibleRanges(uri: vscode.Uri): Promise; + onDidChangeTextDocument(textDocumentChangeEvent: vscode.TextDocumentChangeEvent): void; + onRegisterCustomConfigurationProvider(provider: CustomConfigurationProvider1): Thenable; + updateCustomConfigurations(requestingProvider?: CustomConfigurationProvider1): Thenable; + updateCustomBrowseConfiguration(requestingProvider?: CustomConfigurationProvider1): Thenable; + provideCustomConfiguration(docUri: vscode.Uri): Promise; + logDiagnostics(): Promise; + rescanFolder(): Promise; + toggleReferenceResultsView(): void; + setCurrentConfigName(configurationName: string): Thenable; + getCurrentConfigName(): Thenable; + getCurrentConfigCustomVariable(variableName: string): Thenable; + getVcpkgInstalled(): Thenable; + getVcpkgEnabled(): Thenable; + getCurrentCompilerPathAndArgs(): Thenable; + getKnownCompilers(): Thenable; + takeOwnership(document: vscode.TextDocument): void; + sendDidOpen(document: vscode.TextDocument): Promise; + requestSwitchHeaderSource(rootUri: vscode.Uri, fileName: string): Thenable; + updateActiveDocumentTextOptions(): void; + didChangeActiveEditor(editor?: vscode.TextEditor, selection?: Range): Promise; + restartIntelliSenseForFile(document: vscode.TextDocument): Promise; + activate(): void; + selectionChanged(selection: Range): void; + resetDatabase(): void; + deactivate(): void; + promptSelectIntelliSenseConfiguration(sender?: any): Promise; + rescanCompilers(sender?: any): Promise; + pauseParsing(): void; + resumeParsing(): void; + PauseCodeAnalysis(): void; + ResumeCodeAnalysis(): void; + CancelCodeAnalysis(): void; + handleConfigurationSelectCommand(): Promise; + handleConfigurationProviderSelectCommand(): Promise; + handleShowActiveCodeAnalysisCommands(): Promise; + handleShowIdleCodeAnalysisCommands(): Promise; + handleReferencesIcon(): void; + handleConfigurationEditCommand(viewColumn?: vscode.ViewColumn): void; + handleConfigurationEditJSONCommand(viewColumn?: vscode.ViewColumn): void; + handleConfigurationEditUICommand(viewColumn?: vscode.ViewColumn): void; + handleAddToIncludePathCommand(path: string): void; + handleGoToDirectiveInGroup(next: boolean): Promise; + handleGenerateDoxygenComment(args: DoxygenCodeActionCommandArguments | vscode.Uri | undefined): Promise; + handleRunCodeAnalysisOnActiveFile(): Promise; + handleRunCodeAnalysisOnOpenFiles(): Promise; + handleRunCodeAnalysisOnAllFiles(): Promise; + handleRemoveAllCodeAnalysisProblems(): Promise; + handleRemoveCodeAnalysisProblems(refreshSquigglesOnSave: boolean, identifiersAndUris: CodeAnalysisDiagnosticIdentifiersAndUri[]): Promise; + handleFixCodeAnalysisProblems(workspaceEdit: vscode.WorkspaceEdit, refreshSquigglesOnSave: boolean, identifiersAndUris: CodeAnalysisDiagnosticIdentifiersAndUri[]): Promise; + handleDisableAllTypeCodeAnalysisProblems(code: string, identifiersAndUris: CodeAnalysisDiagnosticIdentifiersAndUri[]): Promise; + handleCreateDeclarationOrDefinition(isCopyToClipboard: boolean, codeActionRange?: Range): Promise; + handleExtractToFunction(extractAsGlobal: boolean): Promise; + onInterval(): void; + dispose(): void; + addFileAssociations(fileAssociations: string, languageId: string): void; + sendDidChangeSettings(): void; + isInitialized(): boolean; + getShowConfigureIntelliSenseButton(): boolean; + setShowConfigureIntelliSenseButton(show: boolean): void; + addTrustedCompiler(path: string): Promise; + getIncludes(maxDepth: number, token: vscode.CancellationToken): Promise; + getChatContext(token: vscode.CancellationToken): Promise; +} + +export function createClient(workspaceFolder?: vscode.WorkspaceFolder): Client { + if (isInstrumentationEnabled()) { + instrument(vscode.languages, { name: "languages" }); + instrument(vscode.window, { name: "window" }); + instrument(vscode.workspace, { name: "workspace" }); + instrument(vscode.commands, { name: "commands" }); + instrument(vscode.debug, { name: "debug" }); + instrument(vscode.env, { name: "env" }); + instrument(vscode.extensions, { name: "extensions" }); + } + return instrument(new DefaultClient(workspaceFolder), { ignore: ["enqueue", "onInterval", "logTelemetry"] }); +} + +export function createNullClient(): Client { + return new NullClient(); +} + +export class DefaultClient implements Client { + private innerLanguageClient?: LanguageClient; // The "client" that launches and communicates with our language "server" process. + private disposables: vscode.Disposable[] = []; + private documentFormattingProviderDisposable: vscode.Disposable | undefined; + private formattingRangeProviderDisposable: vscode.Disposable | undefined; + private onTypeFormattingProviderDisposable: vscode.Disposable | undefined; + private codeFoldingProvider: FoldingRangeProvider | undefined; + private codeFoldingProviderDisposable: vscode.Disposable | undefined; + private inlayHintsProvider: InlayHintsProvider | undefined; + private semanticTokensProvider: SemanticTokensProvider | undefined; + private semanticTokensProviderDisposable: vscode.Disposable | undefined; + private innerConfiguration?: configs.CppProperties; + private rootPathFileWatcher?: vscode.FileSystemWatcher; + private rootFolder?: vscode.WorkspaceFolder; + private rootRealPath: string; + private workspaceStoragePath: string; + private trackedDocuments = new Map(); + private isSupported: boolean = true; + private inactiveRegionsDecorations = new Map(); + private settingsTracker: SettingsTracker; + private loggingLevel: number = 1; + private configurationProvider?: string; + + public lastCustomBrowseConfiguration: PersistentFolderState | undefined; + public lastCustomBrowseConfigurationProviderId: PersistentFolderState | undefined; + public lastCustomBrowseConfigurationProviderVersion: PersistentFolderState | undefined; + public currentCaseSensitiveFileSupport: PersistentWorkspaceState | undefined; + private registeredProviders: PersistentFolderState | undefined; + + private configStateReceived: ConfigStateReceived = { compilers: false, compileCommands: false, configProviders: undefined, timeout: false }; + private showConfigureIntelliSenseButton: boolean = false; + + /** A queue of asynchronous tasks that need to be processed befofe ready is considered active. */ + private static queue = new Array<[ManualPromise, () => Promise] | [ManualPromise]>(); + + /** returns a promise that waits initialization and/or a change to configuration to complete (i.e. language client is ready-to-use) */ + private static readonly isStarted = new ManualSignal(true); + + /** + * Indicates if the blocking task dispatcher is currently running + * + * This will be in the Set state when the dispatcher is not running (i.e. if you await this it will be resolved immediately) + * If the dispatcher is running, this will be in the Reset state (i.e. if you await this it will be resolved when the dispatcher is done) + */ + private static readonly dispatching = new ManualSignal(); + + // The "model" that is displayed via the UI (status bar). + private model: ClientModel = new ClientModel(); + + public get InitializingWorkspaceChanged(): vscode.Event { return this.model.isInitializingWorkspace.ValueChanged; } + public get IndexingWorkspaceChanged(): vscode.Event { return this.model.isIndexingWorkspace.ValueChanged; } + public get ParsingWorkspaceChanged(): vscode.Event { return this.model.isParsingWorkspace.ValueChanged; } + public get ParsingWorkspacePausedChanged(): vscode.Event { return this.model.isParsingWorkspacePaused.ValueChanged; } + public get ParsingFilesChanged(): vscode.Event { return this.model.isParsingFiles.ValueChanged; } + public get IntelliSenseParsingChanged(): vscode.Event { return this.model.isUpdatingIntelliSense.ValueChanged; } + public get RunningCodeAnalysisChanged(): vscode.Event { return this.model.isRunningCodeAnalysis.ValueChanged; } + public get CodeAnalysisPausedChanged(): vscode.Event { return this.model.isCodeAnalysisPaused.ValueChanged; } + public get CodeAnalysisProcessedChanged(): vscode.Event { return this.model.codeAnalysisProcessed.ValueChanged; } + public get CodeAnalysisTotalChanged(): vscode.Event { return this.model.codeAnalysisTotal.ValueChanged; } + public get ReferencesCommandModeChanged(): vscode.Event { return this.model.referencesCommandMode.ValueChanged; } + public get TagParserStatusChanged(): vscode.Event { return this.model.parsingWorkspaceStatus.ValueChanged; } + public get ActiveConfigChanged(): vscode.Event { return this.model.activeConfigName.ValueChanged; } + public isInitialized(): boolean { return this.innerLanguageClient !== undefined; } + public getShowConfigureIntelliSenseButton(): boolean { return this.showConfigureIntelliSenseButton; } + public setShowConfigureIntelliSenseButton(show: boolean): void { this.showConfigureIntelliSenseButton = show; } + + /** + * don't use this.rootFolder directly since it can be undefined + */ + public get RootPath(): string { + return this.rootFolder ? this.rootFolder.uri.fsPath : ""; + } + public get RootRealPath(): string { + return this.rootRealPath; + } + public get RootUri(): vscode.Uri | undefined { + return this.rootFolder ? this.rootFolder.uri : undefined; + } + public get RootFolder(): vscode.WorkspaceFolder | undefined { + return this.rootFolder; + } + public get Name(): string { + return this.getName(this.rootFolder); + } + public get TrackedDocuments(): Map { + return this.trackedDocuments; + } + public get IsTagParsing(): boolean { + return this.model.isParsingWorkspace.Value || this.model.isParsingFiles.Value || this.model.isInitializingWorkspace.Value || this.model.isIndexingWorkspace.Value; + } + public get ReferencesCommandMode(): refs.ReferencesCommandMode { + return this.model.referencesCommandMode.Value; + } + + public get languageClient(): LanguageClient { + if (!this.innerLanguageClient) { + throw new Error("Attempting to use languageClient before initialized"); + } + return this.innerLanguageClient; + } + + public get configuration(): configs.CppProperties { + if (!this.innerConfiguration) { + throw new Error("Attempting to use configuration before initialized"); + } + return this.innerConfiguration; + } + + public get AdditionalEnvironment(): Record { + return { + workspaceFolderBasename: this.Name, + workspaceStorage: this.workspaceStoragePath, + execPath: process.execPath, + pathSeparator: (os.platform() === 'win32') ? "\\" : "/" + }; + } + + private getName(workspaceFolder?: vscode.WorkspaceFolder): string { + return workspaceFolder ? workspaceFolder.name : "untitled"; + } + + public static updateClientConfigurations(): void { + clients.forEach(client => { + if (client instanceof DefaultClient) { + const defaultClient: DefaultClient = client as DefaultClient; + if (!client.isInitialized() || !compilerDefaults) { + // This can randomly get hit when adding/removing workspace folders. + return; + } + defaultClient.configuration.CompilerDefaults = compilerDefaults; + defaultClient.configuration.handleConfigurationChange(); + } + }); + } + + private static readonly configurationProvidersLabel: string = "configuration providers"; + private static readonly compileCommandsLabel: string = "compile_commands.json"; + private static readonly compilersLabel: string = "compilers"; + + public async showSelectIntelliSenseConfiguration(paths: string[], compilersOnly?: boolean): Promise { + const options: vscode.QuickPickOptions = {}; + options.placeHolder = compilersOnly || !vscode.workspace.workspaceFolders || !this.RootFolder ? + localize("select.compiler", "Select a compiler to configure for IntelliSense") : + vscode.workspace.workspaceFolders.length > 1 ? + localize("configure.intelliSense.forFolder", "How would you like to configure IntelliSense for the '{0}' folder?", this.RootFolder.name) : + localize("configure.intelliSense.thisFolder", "How would you like to configure IntelliSense for this folder?"); + + const items: IndexableQuickPickItem[] = []; + let isCompilerSection: boolean = false; + for (let i: number = 0; i < paths.length; i++) { + const compilerName: string = path.basename(paths[i]); + const isCompiler: boolean = isCompilerSection && compilerName !== paths[i]; + + if (isCompiler) { + const path: string | undefined = paths[i].replace(compilerName, ""); + const description: string = localize("found.string", "Found at {0}", path); + const label: string = localize("use.compiler", "Use {0}", compilerName); + items.push({ label: label, description: description, index: i }); + } else if (paths[i] === DefaultClient.configurationProvidersLabel) { + items.push({ label: localize("configuration.providers", "configuration providers"), index: i, kind: vscode.QuickPickItemKind.Separator }); + } else if (paths[i] === DefaultClient.compileCommandsLabel) { + items.push({ label: paths[i], index: i, kind: vscode.QuickPickItemKind.Separator }); + } else if (paths[i] === DefaultClient.compilersLabel) { + isCompilerSection = true; + items.push({ label: localize("compilers", "compilers"), index: i, kind: vscode.QuickPickItemKind.Separator }); + } else { + items.push({ label: paths[i], index: i }); + } + } + + const selection: IndexableQuickPickItem | undefined = await vscode.window.showQuickPick(items, options); + return selection ? selection.index : -1; + } + + public async showPrompt(sender?: any): Promise { + const buttonMessage: string = localize("selectIntelliSenseConfiguration.string", "Select IntelliSense Configuration..."); + const value: string | undefined = await vscode.window.showInformationMessage(localize("setCompiler.message", "You do not have IntelliSense configured. Unless you set your own configurations, IntelliSense may not be functional."), buttonMessage); + if (value === buttonMessage) { + return this.handleIntelliSenseConfigurationQuickPick(sender); + } + } + + public async handleIntelliSenseConfigurationQuickPick(sender?: any, showCompilersOnly?: boolean): Promise { + const settings: CppSettings = new CppSettings(showCompilersOnly ? undefined : this.RootUri); + const paths: string[] = []; + const configProviders: CustomConfigurationProvider1[] | undefined = showCompilersOnly ? undefined : this.configStateReceived.configProviders; + if (configProviders && configProviders.length > 0) { + paths.push(DefaultClient.configurationProvidersLabel); + for (const provider of configProviders) { + paths.push(localize("use.provider", "Use {0}", provider.name)); + } + } + const configProvidersIndex: number = paths.length; + const configProviderCount: number = configProvidersIndex === 0 ? 0 : configProvidersIndex - 1; + if (!showCompilersOnly && this.compileCommandsPaths.length > 0) { + paths.push(DefaultClient.compileCommandsLabel); + for (const compileCommandsPath of this.compileCommandsPaths) { + paths.push(localize("use.compileCommands", "Use {0}", compileCommandsPath)); + } + } + const compileCommandsIndex: number = paths.length; + const compileCommandsCount: number = compileCommandsIndex === configProvidersIndex ? 0 : compileCommandsIndex - configProvidersIndex - 1; + paths.push(DefaultClient.compilersLabel); + if (compilerDefaults?.knownCompilers !== undefined) { + const tempPaths: string[] = compilerDefaults.knownCompilers.map(function (a: configs.KnownCompiler): string { return a.path; }); + let clFound: boolean = false; + // Remove all but the first cl path. + for (const path of tempPaths) { + if (clFound) { + if (!util.isCl(path)) { + paths.push(path); + } + } else { + if (util.isCl(path)) { + clFound = true; + } + paths.push(path); + } + } + } + const compilersIndex: number = paths.length; + const compilerCount: number = compilersIndex === compileCommandsIndex ? 0 : compilersIndex - compileCommandsIndex - 1; + paths.push(localize("selectAnotherCompiler.string", "Select another compiler on my machine...")); + let installShown = true; + if (isWindows && util.getSenderType(sender) !== 'walkthrough') { + paths.push(localize("installCompiler.string", "Help me install a compiler")); + } else if (!isWindows) { + paths.push(localize("installCompiler.string.nix", "Install a compiler")); + } else { + installShown = false; + } + paths.push(localize("noConfig.string", "Do not configure with a compiler (not recommended)")); + const index: number = await this.showSelectIntelliSenseConfiguration(paths, showCompilersOnly); + let action: string = ""; + let configurationSelected: boolean = false; + const fromStatusBarButton: boolean = !showCompilersOnly; + try { + if (index === -1) { + action = "escaped"; + return; + } + if (index === paths.length - 1) { + action = "disable"; + settings.defaultCompilerPath = ""; + await this.configuration.updateCompilerPathIfSet(""); + configurationSelected = true; + await this.showPrompt(sender); + return ui.ShowConfigureIntelliSenseButton(false, this, ConfigurationType.CompilerPath, "disablePrompt"); + } + if (installShown && index === paths.length - 2) { + action = "install"; + void vscode.commands.executeCommand('C_Cpp.InstallCompiler', sender); + return; + } + const showButtonSender: string = "quickPick"; + if (index === paths.length - 3 || (!installShown && index === paths.length - 2)) { + const result: vscode.Uri[] | undefined = await vscode.window.showOpenDialog(); + if (result === undefined || result.length === 0) { + action = "browse dismissed"; + return; + } + configurationSelected = true; + action = "compiler browsed"; + settings.defaultCompilerPath = result[0].fsPath; + await this.configuration.updateCompilerPathIfSet(result[0].fsPath); + void SessionState.trustedCompilerFound.set(true); + } else { + configurationSelected = true; + if (index < configProvidersIndex && configProviders) { + action = "select config provider"; + const provider: CustomConfigurationProvider1 = configProviders[index - 1]; + await this.configuration.updateCustomConfigurationProvider(provider.extensionId); + void this.onCustomConfigurationProviderRegistered(provider).catch(logAndReturn.undefined); + telemetry.logLanguageServerEvent("customConfigurationProvider", { "providerId": provider.extensionId }); + + return ui.ShowConfigureIntelliSenseButton(false, this, ConfigurationType.ConfigProvider, showButtonSender); + } else if (index < compileCommandsIndex) { + action = "select compile commands"; + await this.configuration.setCompileCommands(this.compileCommandsPaths[index - configProvidersIndex - 1]); + return ui.ShowConfigureIntelliSenseButton(false, this, ConfigurationType.CompileCommands, showButtonSender); + } else { + action = "select compiler"; + const newCompiler: string = util.isCl(paths[index]) ? "cl.exe" : paths[index]; + settings.defaultCompilerPath = newCompiler; + await this.configuration.updateCompilerPathIfSet(newCompiler); + void SessionState.trustedCompilerFound.set(true); + } + } + + await ui.ShowConfigureIntelliSenseButton(false, this, ConfigurationType.CompilerPath, showButtonSender); + + await this.addTrustedCompiler(settings.defaultCompilerPath); + DefaultClient.updateClientConfigurations(); + } finally { + if (showCompilersOnly) { + telemetry.logLanguageServerEvent('compilerSelection', { action, sender: util.getSenderType(sender) }, + { compilerCount: compilerCount + 3 }); // + 3 is to match what was being incorrectly sent previously + } else { + telemetry.logLanguageServerEvent('configurationSelection', { action, sender: util.getSenderType(sender) }, + { configProviderCount, compileCommandsCount, compilerCount }); + } + + // Clear the prompt state. + // TODO: Add some way to change this state to true. + const rootFolder: vscode.WorkspaceFolder | undefined = this.RootFolder; + if (rootFolder && fromStatusBarButton) { + if (configurationSelected || configProviderCount > 0) { + const ask: PersistentFolderState = new PersistentFolderState("Client.registerProvider", true, rootFolder); + ask.Value = false; + } + if (configurationSelected || compileCommandsCount > 0) { + const ask: PersistentFolderState = new PersistentFolderState("CPP.showCompileCommandsSelection", true, rootFolder); + ask.Value = false; + } + if (!configurationSelected) { + await this.handleConfigStatus(); + } + } + } + } + + public async rescanCompilers(sender?: any): Promise { + compilerDefaults = await this.requestCompiler(); + DefaultClient.updateClientConfigurations(); + if (compilerDefaults.knownCompilers !== undefined && compilerDefaults.knownCompilers.length > 0) { + await this.handleIntelliSenseConfigurationQuickPick(sender, true); + } + } + + async promptSelectIntelliSenseConfiguration(sender?: any): Promise { + if (compilerDefaults === undefined) { + return; + } + if (compilerDefaults.compilerPath !== "") { + const showCompilersOnly: boolean = util.getSenderType(sender) === 'walkthrough'; + return this.handleIntelliSenseConfigurationQuickPick(sender, showCompilersOnly); + } + } + + /** + * All public methods on this class must be guarded by the "ready" promise. Requests and notifications received before the task is + * complete are executed after this promise is resolved. + */ + + constructor(workspaceFolder?: vscode.WorkspaceFolder) { + if (workspaceFolder !== undefined) { + this.lastCustomBrowseConfiguration = new PersistentFolderState("CPP.lastCustomBrowseConfiguration", undefined, workspaceFolder); + this.lastCustomBrowseConfigurationProviderId = new PersistentFolderState("CPP.lastCustomBrowseConfigurationProviderId", undefined, workspaceFolder); + this.lastCustomBrowseConfigurationProviderVersion = new PersistentFolderState("CPP.lastCustomBrowseConfigurationProviderVersion", Version.v5, workspaceFolder); + this.registeredProviders = new PersistentFolderState("CPP.registeredProviders", [], workspaceFolder); + // If this provider did the register in the last session, clear out the cached browse config. + if (!this.isProviderRegistered(this.lastCustomBrowseConfigurationProviderId.Value)) { + this.lastCustomBrowseConfigurationProviderId.Value = undefined; + if (this.lastCustomBrowseConfiguration !== undefined) { + this.lastCustomBrowseConfiguration.Value = undefined; + } + } + if (this.lastCustomBrowseConfigurationProviderId.Value) { + this.configStateReceived.configProviders = []; // avoid waiting for the timeout if it's cached + } + this.registeredProviders.Value = []; + } else { + this.configStateReceived.configProviders = []; + this.configStateReceived.compileCommands = true; + } + if (!semanticTokensLegend) { + // Semantic token types are identified by indexes in this list of types, in the legend. + const tokenTypesLegend: string[] = []; + for (const e in SemanticTokenTypes) { + // An enum is actually a set of mappings from key <=> value. Enumerate over only the names. + // This allow us to represent the constants using an enum, which we can match in native code. + if (isNaN(Number(e))) { + tokenTypesLegend.push(e); + } + } + // Semantic token modifiers are bit indexes corresponding to the indexes in this list of modifiers in the legend. + const tokenModifiersLegend: string[] = []; + for (const e in SemanticTokenModifiers) { + if (isNaN(Number(e))) { + tokenModifiersLegend.push(e); + } + } + semanticTokensLegend = new vscode.SemanticTokensLegend(tokenTypesLegend, tokenModifiersLegend); + } + + this.rootFolder = workspaceFolder; + this.rootRealPath = this.RootPath ? fs.existsSync(this.RootPath) ? fs.realpathSync(this.RootPath) : this.RootPath : ""; + + this.workspaceStoragePath = util.extensionContext?.storageUri?.fsPath ?? ""; + if (this.workspaceStoragePath.length > 0) { + workspaceHash = path.basename(path.dirname(this.workspaceStoragePath)); + } else { + this.workspaceStoragePath = this.RootPath ? path.join(this.RootPath, ".vscode") : ""; + } + + if (workspaceFolder && vscode.workspace.workspaceFolders && vscode.workspace.workspaceFolders.length > 1) { + this.workspaceStoragePath = path.join(this.workspaceStoragePath, util.getUniqueWorkspaceStorageName(workspaceFolder)); + } + + const rootUri: vscode.Uri | undefined = this.RootUri; + this.settingsTracker = new SettingsTracker(rootUri); + + try { + let isFirstClient: boolean = false; + if (firstClientStarted === undefined || languageClientCrashedNeedsRestart) { + if (languageClientCrashedNeedsRestart) { + languageClientCrashedNeedsRestart = false; + // if we're recovering, the isStarted needs to be reset. + // because we're starting the first client again. + DefaultClient.isStarted.reset(); + } + firstClientStarted = this.createLanguageClient(); + util.setProgress(util.getProgressExecutableStarted()); + isFirstClient = true; + } + void this.init(rootUri, isFirstClient).catch(logAndReturn.undefined); + + } catch (errJS) { + const err: NodeJS.ErrnoException = errJS as NodeJS.ErrnoException; + this.isSupported = false; // Running on an OS we don't support yet. + if (!failureMessageShown) { + failureMessageShown = true; + let additionalInfo: string; + if (err.code === "EPERM") { + additionalInfo = localize('check.permissions', "EPERM: Check permissions for '{0}'", getLanguageServerFileName()); + } else { + additionalInfo = String(err); + } + void vscode.window.showErrorMessage(localize("unable.to.start", "Unable to start the C/C++ language server. IntelliSense features will be disabled. Error: {0}", additionalInfo)); + } + } + } + + private async init(rootUri: vscode.Uri | undefined, isFirstClient: boolean) { + ui = getUI(); + ui.bind(this); + await firstClientStarted; + try { + const workspaceFolder: vscode.WorkspaceFolder | undefined = this.rootFolder; + this.innerConfiguration = new configs.CppProperties(this, rootUri, workspaceFolder); + this.innerConfiguration.ConfigurationsChanged((e) => this.onConfigurationsChanged(e)); + this.innerConfiguration.SelectionChanged((e) => this.onSelectedConfigurationChanged(e)); + this.innerConfiguration.CompileCommandsChanged((e) => this.onCompileCommandsChanged(e)); + this.disposables.push(this.innerConfiguration); + + this.innerLanguageClient = languageClient; + telemetry.logLanguageServerEvent("NonDefaultInitialCppSettings", this.settingsTracker.getUserModifiedSettings()); + failureMessageShown = false; + + if (isFirstClient) { + workspaceReferences = new refs.ReferencesManager(this); + // Only register file watchers and providers after the extension has finished initializing, + // e.g. prevents empty c_cpp_properties.json from generation. + this.registerFileWatcher(); + initializedClientCount = 0; + this.inlayHintsProvider = new InlayHintsProvider(); + + this.disposables.push(vscode.languages.registerHoverProvider(util.documentSelector, instrument(new HoverProvider(this)))); + this.disposables.push(vscode.languages.registerInlayHintsProvider(util.documentSelector, instrument(this.inlayHintsProvider))); + this.disposables.push(vscode.languages.registerRenameProvider(util.documentSelector, instrument(new RenameProvider(this)))); + this.disposables.push(vscode.languages.registerReferenceProvider(util.documentSelector, instrument(new FindAllReferencesProvider(this)))); + this.disposables.push(vscode.languages.registerWorkspaceSymbolProvider(instrument(new WorkspaceSymbolProvider(this)))); + this.disposables.push(vscode.languages.registerDocumentSymbolProvider(util.documentSelector, instrument(new DocumentSymbolProvider()), undefined)); + this.disposables.push(vscode.languages.registerCodeActionsProvider(util.documentSelector, instrument(new CodeActionProvider(this)), undefined)); + this.disposables.push(vscode.languages.registerCallHierarchyProvider(util.documentSelector, instrument(new CallHierarchyProvider(this)))); + // Because formatting and codeFolding can vary per folder, we need to register these providers once + // and leave them registered. The decision of whether to provide results needs to be made on a per folder basis, + // within the providers themselves. + this.documentFormattingProviderDisposable = vscode.languages.registerDocumentFormattingEditProvider(util.documentSelector, instrument(new DocumentFormattingEditProvider(this))); + this.formattingRangeProviderDisposable = vscode.languages.registerDocumentRangeFormattingEditProvider(util.documentSelector, instrument(new DocumentRangeFormattingEditProvider(this))); + this.onTypeFormattingProviderDisposable = vscode.languages.registerOnTypeFormattingEditProvider(util.documentSelector, instrument(new OnTypeFormattingEditProvider(this)), ";", "}", "\n"); + + this.codeFoldingProvider = new FoldingRangeProvider(this); + this.codeFoldingProviderDisposable = vscode.languages.registerFoldingRangeProvider(util.documentSelector, instrument(this.codeFoldingProvider)); + + const settings: CppSettings = new CppSettings(); + if (settings.isEnhancedColorizationEnabled && semanticTokensLegend) { + this.semanticTokensProvider = instrument(new SemanticTokensProvider()); + this.semanticTokensProviderDisposable = vscode.languages.registerDocumentSemanticTokensProvider(util.documentSelector, this.semanticTokensProvider, semanticTokensLegend); + } + + // Listen for messages from the language server. + this.registerNotifications(); + + // If a file is already open when we activate, sometimes we don't get any notifications about visible + // or active text editors, visible ranges, or text selection. As a workaround, we trigger + // onDidChangeVisibleTextEditors here. + const cppEditors: vscode.TextEditor[] = vscode.window.visibleTextEditors.filter(e => util.isCpp(e.document)); + await this.onDidChangeVisibleTextEditors(cppEditors); + } + + // update all client configurations + this.configuration.setupConfigurations(); + initializedClientCount++; + // count number of clients, once all clients are configured, check for trusted compiler to display notification to user and add a short delay to account for config provider logic to finish + if ((vscode.workspace.workspaceFolders === undefined) || (initializedClientCount >= vscode.workspace.workspaceFolders.length)) { + // Timeout waiting for compile_commands.json and config providers. + // The quick pick options will update if they're added later on. + clients.forEach(client => { + if (client instanceof DefaultClient) { + global.setTimeout(() => { + client.configStateReceived.timeout = true; + void client.handleConfigStatus(); + }, 15000); + } + }); + // The configurations will not be sent to the language server until the default include paths and frameworks have been set. + // The event handlers must be set before this happens. + compilerDefaults = await this.requestCompiler(); + DefaultClient.updateClientConfigurations(); + clients.forEach(client => { + if (client instanceof DefaultClient) { + client.configStateReceived.compilers = true; + void client.handleConfigStatus(); + } + }); + } + } catch (err) { + this.isSupported = false; // Running on an OS we don't support yet. + if (!failureMessageShown) { + failureMessageShown = true; + void vscode.window.showErrorMessage(localize("unable.to.start", "Unable to start the C/C++ language server. IntelliSense features will be disabled. Error: {0}", String(err))); + } + } + + DefaultClient.isStarted.resolve(); + } + + private getWorkspaceFolderSettings(workspaceFolderUri: vscode.Uri | undefined, settings: CppSettings, otherSettings: OtherSettings): WorkspaceFolderSettingsParams { + const result: WorkspaceFolderSettingsParams = { + uri: workspaceFolderUri?.toString(), + intelliSenseEngine: settings.intelliSenseEngine, + autocomplete: settings.autocomplete, + autocompleteAddParentheses: settings.autocompleteAddParentheses, + errorSquiggles: settings.errorSquiggles, + exclusionPolicy: settings.exclusionPolicy, + preferredPathSeparator: settings.preferredPathSeparator, + intelliSenseCachePath: util.resolveCachePath(settings.intelliSenseCachePath, this.AdditionalEnvironment), + intelliSenseCacheSize: settings.intelliSenseCacheSize, + intelliSenseMemoryLimit: settings.intelliSenseMemoryLimit, + dimInactiveRegions: settings.dimInactiveRegions, + suggestSnippets: settings.suggestSnippets, + legacyCompilerArgsBehavior: settings.legacyCompilerArgsBehavior, + defaultSystemIncludePath: settings.defaultSystemIncludePath, + cppFilesExclude: settings.filesExclude, + clangFormatPath: util.resolveVariables(settings.clangFormatPath, this.AdditionalEnvironment), + clangFormatStyle: settings.clangFormatStyle ? util.resolveVariables(settings.clangFormatStyle, this.AdditionalEnvironment) : undefined, + clangFormatFallbackStyle: settings.clangFormatFallbackStyle, + clangFormatSortIncludes: settings.clangFormatSortIncludes, + codeAnalysisRunAutomatically: settings.codeAnalysisRunAutomatically, + codeAnalysisExclude: settings.codeAnalysisExclude, + clangTidyEnabled: settings.clangTidyEnabled, + clangTidyPath: util.resolveVariables(settings.clangTidyPath, this.AdditionalEnvironment), + clangTidyConfig: settings.clangTidyConfig, + clangTidyFallbackConfig: settings.clangTidyFallbackConfig, + clangTidyHeaderFilter: settings.clangTidyHeaderFilter !== null ? util.resolveVariables(settings.clangTidyHeaderFilter, this.AdditionalEnvironment) : null, + clangTidyArgs: util.resolveVariablesArray(settings.clangTidyArgs, this.AdditionalEnvironment), + clangTidyUseBuildPath: settings.clangTidyUseBuildPath, + clangTidyChecksEnabled: settings.clangTidyChecksEnabled, + clangTidyChecksDisabled: settings.clangTidyChecksDisabled, + markdownInComments: settings.markdownInComments, + hover: settings.hover, + vcFormatIndentBraces: settings.vcFormatIndentBraces, + vcFormatIndentMultiLineRelativeTo: settings.vcFormatIndentMultiLineRelativeTo, + vcFormatIndentWithinParentheses: settings.vcFormatIndentWithinParentheses, + vcFormatIndentPreserveWithinParentheses: settings.vcFormatIndentPreserveWithinParentheses, + vcFormatIndentCaseLabels: settings.vcFormatIndentCaseLabels, + vcFormatIndentCaseContents: settings.vcFormatIndentCaseContents, + vcFormatIndentCaseContentsWhenBlock: settings.vcFormatIndentCaseContentsWhenBlock, + vcFormatIndentLambdaBracesWhenParameter: settings.vcFormatIndentLambdaBracesWhenParameter, + vcFormatIndentGotoLabels: settings.vcFormatIndentGotoLabels, + vcFormatIndentPreprocessor: settings.vcFormatIndentPreprocessor, + vcFormatIndentAccesSpecifiers: settings.vcFormatIndentAccessSpecifiers, + vcFormatIndentNamespaceContents: settings.vcFormatIndentNamespaceContents, + vcFormatIndentPreserveComments: settings.vcFormatIndentPreserveComments, + vcFormatNewLineScopeBracesOnSeparateLines: settings.vcFormatNewlineScopeBracesOnSeparateLines, + vcFormatNewLineBeforeOpenBraceNamespace: settings.vcFormatNewlineBeforeOpenBraceNamespace, + vcFormatNewLineBeforeOpenBraceType: settings.vcFormatNewlineBeforeOpenBraceType, + vcFormatNewLineBeforeOpenBraceFunction: settings.vcFormatNewlineBeforeOpenBraceFunction, + vcFormatNewLineBeforeOpenBraceBlock: settings.vcFormatNewlineBeforeOpenBraceBlock, + vcFormatNewLineBeforeOpenBraceLambda: settings.vcFormatNewlineBeforeOpenBraceLambda, + vcFormatNewLineBeforeCatch: settings.vcFormatNewlineBeforeCatch, + vcFormatNewLineBeforeElse: settings.vcFormatNewlineBeforeElse, + vcFormatNewLineBeforeWhileInDoWhile: settings.vcFormatNewlineBeforeWhileInDoWhile, + vcFormatNewLineCloseBraceSameLineEmptyType: settings.vcFormatNewlineCloseBraceSameLineEmptyType, + vcFormatNewLineCloseBraceSameLineEmptyFunction: settings.vcFormatNewlineCloseBraceSameLineEmptyFunction, + vcFormatSpaceBeforeFunctionOpenParenthesis: settings.vcFormatSpaceBeforeFunctionOpenParenthesis, + vcFormatSpaceWithinParameterListParentheses: settings.vcFormatSpaceWithinParameterListParentheses, + vcFormatSpaceBetweenEmptyParameterListParentheses: settings.vcFormatSpaceBetweenEmptyParameterListParentheses, + vcFormatSpaceAfterKeywordsInControlFlowStatements: settings.vcFormatSpaceAfterKeywordsInControlFlowStatements, + vcFormatSpaceWithinControlFlowStatementParentheses: settings.vcFormatSpaceWithinControlFlowStatementParentheses, + vcFormatSpaceBeforeLambdaOpenParenthesis: settings.vcFormatSpaceBeforeLambdaOpenParenthesis, + vcFormatSpaceWithinCastParentheses: settings.vcFormatSpaceWithinCastParentheses, + vcFormatSpaceAfterCastCloseParenthesis: settings.vcFormatSpaceAfterCastCloseParenthesis, + vcFormatSpaceWithinExpressionParentheses: settings.vcFormatSpaceWithinExpressionParentheses, + vcFormatSpaceBeforeBlockOpenBrace: settings.vcFormatSpaceBeforeBlockOpenBrace, + vcFormatSpaceBetweenEmptyBraces: settings.vcFormatSpaceBetweenEmptyBraces, + vcFormatSpaceBeforeInitializerListOpenBrace: settings.vcFormatSpaceBeforeInitializerListOpenBrace, + vcFormatSpaceWithinInitializerListBraces: settings.vcFormatSpaceWithinInitializerListBraces, + vcFormatSpacePreserveInInitializerList: settings.vcFormatSpacePreserveInInitializerList, + vcFormatSpaceBeforeOpenSquareBracket: settings.vcFormatSpaceBeforeOpenSquareBracket, + vcFormatSpaceWithinSquareBrackets: settings.vcFormatSpaceWithinSquareBrackets, + vcFormatSpaceBeforeEmptySquareBrackets: settings.vcFormatSpaceBeforeEmptySquareBrackets, + vcFormatSpaceBetweenEmptySquareBrackets: settings.vcFormatSpaceBetweenEmptySquareBrackets, + vcFormatSpaceGroupSquareBrackets: settings.vcFormatSpaceGroupSquareBrackets, + vcFormatSpaceWithinLambdaBrackets: settings.vcFormatSpaceWithinLambdaBrackets, + vcFormatSpaceBetweenEmptyLambdaBrackets: settings.vcFormatSpaceBetweenEmptyLambdaBrackets, + vcFormatSpaceBeforeComma: settings.vcFormatSpaceBeforeComma, + vcFormatSpaceAfterComma: settings.vcFormatSpaceAfterComma, + vcFormatSpaceRemoveAroundMemberOperators: settings.vcFormatSpaceRemoveAroundMemberOperators, + vcFormatSpaceBeforeInheritanceColon: settings.vcFormatSpaceBeforeInheritanceColon, + vcFormatSpaceBeforeConstructorColon: settings.vcFormatSpaceBeforeConstructorColon, + vcFormatSpaceRemoveBeforeSemicolon: settings.vcFormatSpaceRemoveBeforeSemicolon, + vcFormatSpaceInsertAfterSemicolon: settings.vcFormatSpaceInsertAfterSemicolon, + vcFormatSpaceRemoveAroundUnaryOperator: settings.vcFormatSpaceRemoveAroundUnaryOperator, + vcFormatSpaceAroundBinaryOperator: settings.vcFormatSpaceAroundBinaryOperator, + vcFormatSpaceAroundAssignmentOperator: settings.vcFormatSpaceAroundAssignmentOperator, + vcFormatSpacePointerReferenceAlignment: settings.vcFormatSpacePointerReferenceAlignment, + vcFormatSpaceAroundTernaryOperator: settings.vcFormatSpaceAroundTernaryOperator, + vcFormatWrapPreserveBlocks: settings.vcFormatWrapPreserveBlocks, + doxygenGenerateOnType: settings.doxygenGenerateOnType, + doxygenGeneratedStyle: settings.doxygenGeneratedCommentStyle, + doxygenSectionTags: settings.doxygenSectionTags, + filesExclude: otherSettings.filesExclude, + filesAutoSaveAfterDelay: otherSettings.filesAutoSaveAfterDelay, + filesEncoding: otherSettings.filesEncoding, + searchExclude: otherSettings.searchExclude, + editorAutoClosingBrackets: otherSettings.editorAutoClosingBrackets, + editorInlayHintsEnabled: otherSettings.editorInlayHintsEnabled, + editorParameterHintsEnabled: otherSettings.editorParameterHintsEnabled, + refactoringIncludeHeader: settings.refactoringIncludeHeader + }; + return result; + } + + private getAllWorkspaceFolderSettings(): WorkspaceFolderSettingsParams[] { + const workspaceSettings: CppSettings = new CppSettings(); + const workspaceOtherSettings: OtherSettings = new OtherSettings(); + const workspaceFolderSettingsParams: WorkspaceFolderSettingsParams[] = []; + if (vscode.workspace.workspaceFolders && vscode.workspace.workspaceFolders.length > 0) { + for (const workspaceFolder of vscode.workspace.workspaceFolders) { + workspaceFolderSettingsParams.push(this.getWorkspaceFolderSettings(workspaceFolder.uri, new CppSettings(workspaceFolder.uri), new OtherSettings(workspaceFolder.uri))); + } + } else { + workspaceFolderSettingsParams.push(this.getWorkspaceFolderSettings(this.RootUri, workspaceSettings, workspaceOtherSettings)); + } + return workspaceFolderSettingsParams; + } + + private getAllSettings(): SettingsParams { + const workspaceSettings: CppSettings = new CppSettings(); + const workspaceOtherSettings: OtherSettings = new OtherSettings(); + const workspaceFolderSettingsParams: WorkspaceFolderSettingsParams[] = this.getAllWorkspaceFolderSettings(); + if (this.currentCaseSensitiveFileSupport && workspaceSettings.isCaseSensitiveFileSupportEnabled !== this.currentCaseSensitiveFileSupport.Value) { + void util.promptForReloadWindowDueToSettingsChange(); + } + return { + filesAssociations: workspaceOtherSettings.filesAssociations, + workspaceFallbackEncoding: workspaceOtherSettings.filesEncoding, + maxConcurrentThreads: workspaceSettings.maxConcurrentThreads, + maxCachedProcesses: workspaceSettings.maxCachedProcesses, + maxMemory: workspaceSettings.maxMemory, + maxSymbolSearchResults: workspaceSettings.maxSymbolSearchResults, + loggingLevel: workspaceSettings.loggingLevel, + workspaceParsingPriority: workspaceSettings.workspaceParsingPriority, + workspaceSymbols: workspaceSettings.workspaceSymbols, + simplifyStructuredComments: workspaceSettings.simplifyStructuredComments, + intelliSenseUpdateDelay: workspaceSettings.intelliSenseUpdateDelay, + experimentalFeatures: workspaceSettings.experimentalFeatures, + enhancedColorization: workspaceSettings.isEnhancedColorizationEnabled, + intellisenseMaxCachedProcesses: workspaceSettings.intelliSenseMaxCachedProcesses, + intellisenseMaxMemory: workspaceSettings.intelliSenseMaxMemory, + referencesMaxConcurrentThreads: workspaceSettings.referencesMaxConcurrentThreads, + referencesMaxCachedProcesses: workspaceSettings.referencesMaxCachedProcesses, + referencesMaxMemory: workspaceSettings.referencesMaxMemory, + codeAnalysisMaxConcurrentThreads: workspaceSettings.codeAnalysisMaxConcurrentThreads, + codeAnalysisMaxMemory: workspaceSettings.codeAnalysisMaxMemory, + codeAnalysisUpdateDelay: workspaceSettings.codeAnalysisUpdateDelay, + workspaceFolderSettings: workspaceFolderSettingsParams + }; + } + + private async createLanguageClient(): Promise { + this.currentCaseSensitiveFileSupport = new PersistentWorkspaceState("CPP.currentCaseSensitiveFileSupport", false); + let resetDatabase: boolean = false; + const serverModule: string = getLanguageServerFileName(); + const exeExists: boolean = fs.existsSync(serverModule); + if (!exeExists) { + telemetry.logLanguageServerEvent("missingLanguageServerBinary"); + throw String('Missing binary at ' + serverModule); + } + const serverName: string = this.getName(this.rootFolder); + const serverOptions: ServerOptions = { + run: { command: serverModule, options: { detached: false, cwd: util.getExtensionFilePath("bin") } }, + debug: { command: serverModule, args: [serverName], options: { detached: true, cwd: util.getExtensionFilePath("bin") } } + }; + + // The IntelliSense process should automatically detect when AutoPCH is + // not supportable (on platforms that don't support disabling ASLR/PIE). + // We've had reports of issues on arm64 macOS that are addressed by + // disabling the IntelliSense cache, suggesting fallback does not + // always work as expected. It's actually more efficient to disable + // the cache on platforms we know do not support it. We do that here. + let intelliSenseCacheDisabled: boolean = false; + if (os.platform() === "darwin") { + // AutoPCH doesn't work for arm64 macOS. + if (os.arch() === "arm64") { + intelliSenseCacheDisabled = true; + } else { + // AutoPCH doesn't work for older x64 macOS's. + const releaseParts: string[] = os.release().split("."); + if (releaseParts.length >= 1) { + intelliSenseCacheDisabled = parseInt(releaseParts[0]) < 17; + } + } + } else { + // AutoPCH doesn't work for arm64 Windows. + intelliSenseCacheDisabled = os.platform() === "win32" && os.arch() === "arm64"; + } + + const localizedStrings: string[] = []; + for (let i: number = 0; i < localizedStringCount; i++) { + localizedStrings.push(lookupString(i)); + } + + const workspaceSettings: CppSettings = new CppSettings(); + if (workspaceSettings.isCaseSensitiveFileSupportEnabled !== this.currentCaseSensitiveFileSupport.Value) { + resetDatabase = true; + this.currentCaseSensitiveFileSupport.Value = workspaceSettings.isCaseSensitiveFileSupportEnabled; + } + + const cacheStoragePath: string = util.getCacheStoragePath(); + const databaseStoragePath: string = (cacheStoragePath.length > 0) && (workspaceHash.length > 0) ? + path.join(cacheStoragePath, workspaceHash) : ""; + + const cppInitializationParams: CppInitializationParams = { + packageVersion: util.packageJson.version, + extensionPath: util.extensionPath, + databaseStoragePath: databaseStoragePath, + workspaceStoragePath: this.workspaceStoragePath, + cacheStoragePath: cacheStoragePath, + vcpkgRoot: util.getVcpkgRoot(), + intelliSenseCacheDisabled: intelliSenseCacheDisabled, + caseSensitiveFileSupport: workspaceSettings.isCaseSensitiveFileSupportEnabled, + resetDatabase: resetDatabase, + edgeMessagesDirectory: path.join(util.getExtensionFilePath("bin"), "messages", getLocaleId()), + localizedStrings: localizedStrings, + settings: this.getAllSettings() + }; + + this.loggingLevel = util.getNumericLoggingLevel(cppInitializationParams.settings.loggingLevel); + const lspInitializationOptions: LspInitializationOptions = { + loggingLevel: this.loggingLevel + }; + + const clientOptions: LanguageClientOptions = { + documentSelector: [ + { scheme: 'file', language: 'c' }, + { scheme: 'file', language: 'cpp' }, + { scheme: 'file', language: 'cuda-cpp' } + ], + initializationOptions: lspInitializationOptions, + middleware: createProtocolFilter(), + errorHandler: { + error: (_error, _message, _count) => ({ action: ErrorAction.Continue }), + closed: () => { + languageClientCrashTimes.push(Date.now()); + languageClientCrashedNeedsRestart = true; + telemetry.logLanguageServerEvent("languageClientCrash"); + let restart: boolean = true; + if (languageClientCrashTimes.length < 5) { + void clients.recreateClients(); + } else { + const elapsed: number = languageClientCrashTimes[languageClientCrashTimes.length - 1] - languageClientCrashTimes[0]; + if (elapsed <= 3 * 60 * 1000) { + void clients.recreateClients(true); + restart = false; + } else { + languageClientCrashTimes.shift(); + void clients.recreateClients(); + } + } + const message: string = restart ? localize('server.crashed.restart', 'The language server crashed. Restarting...') + : localize('server.crashed2', 'The language server crashed 5 times in the last 3 minutes. It will not be restarted.'); + + // We manually restart the language server so tell the LanguageClient not to do it automatically for us. + return { action: CloseAction.DoNotRestart, message }; + } + } + + // TODO: should I set the output channel? Does this sort output between servers? + }; + + // Create the language client + languageClient = new LanguageClient(`cpptools`, serverOptions, clientOptions); + languageClient.onNotification(DebugProtocolNotification, logDebugProtocol); + languageClient.onNotification(DebugLogNotification, logLocalized); + languageClient.registerProposedFeatures(); + await languageClient.start(); + + if (usesCrashHandler()) { + watchForCrashes(await languageClient.sendRequest(PreInitializationRequest, null)); + } + + // Move initialization to a separate message, so we can see log output from it. + // A request is used in order to wait for completion and ensure that no subsequent + // higher priority message may be processed before the Initialization request. + await languageClient.sendRequest(InitializationRequest, cppInitializationParams); + } + + public async sendDidChangeSettings(): Promise { + // Send settings json to native side + await this.ready; + await this.languageClient.sendNotification(DidChangeSettingsNotification, this.getAllSettings()); + } + + public async onDidChangeSettings(_event: vscode.ConfigurationChangeEvent): Promise> { + const defaultClient: Client = clients.getDefaultClient(); + if (this === defaultClient) { + // Only send the updated settings information once, as it includes values for all folders. + void this.sendDidChangeSettings(); + } + const changedSettings: Record = this.settingsTracker.getChangedSettings(); + + await this.ready; + + if (Object.keys(changedSettings).length > 0) { + if (this === defaultClient) { + if (changedSettings.commentContinuationPatterns) { + updateLanguageConfigurations(); + } + if (changedSettings.loggingLevel) { + const oldLoggingLevelLogged: boolean = this.loggingLevel > 1; + this.loggingLevel = util.getNumericLoggingLevel(changedSettings.loggingLevel); + if (oldLoggingLevelLogged || this.loggingLevel > 1) { + getOutputChannelLogger().appendLine(localize({ key: "loggingLevel.changed", comment: ["{0} is the setting name 'loggingLevel', {1} is a string value such as 'Debug'"] }, "{0} has changed to: {1}", "loggingLevel", changedSettings.loggingLevel)); + } + } + const settings: CppSettings = new CppSettings(); + if (changedSettings.enhancedColorization) { + if (settings.isEnhancedColorizationEnabled && semanticTokensLegend) { + this.semanticTokensProvider = new SemanticTokensProvider(); + this.semanticTokensProviderDisposable = vscode.languages.registerDocumentSemanticTokensProvider(util.documentSelector, this.semanticTokensProvider, semanticTokensLegend); + } else if (this.semanticTokensProviderDisposable) { + this.semanticTokensProviderDisposable.dispose(); + this.semanticTokensProviderDisposable = undefined; + this.semanticTokensProvider = undefined; + } + } + + // If an inlay hints setting has changed, force an inlay provider update on the visible documents. + if (["inlayHints.autoDeclarationTypes.enabled", + "inlayHints.autoDeclarationTypes.showOnLeft", + "inlayHints.parameterNames.enabled", + "inlayHints.parameterNames.hideLeadingUnderscores", + "inlayHints.parameterNames.suppressWhenArgumentContainsName", + "inlayHints.referenceOperator.enabled", + "inlayHints.referenceOperator.showSpace"].some(setting => setting in changedSettings)) { + vscode.window.visibleTextEditors.forEach((visibleEditor: vscode.TextEditor) => { + // The exact range doesn't matter. + const visibleRange: vscode.Range | undefined = visibleEditor.visibleRanges.at(0); + if (visibleRange !== undefined) { + void vscode.commands.executeCommand('vscode.executeInlayHintProvider', + visibleEditor.document.uri, visibleRange); + } + }); + } + + const showButtonSender: string = "settingsChanged"; + if (changedSettings["default.configurationProvider"] !== undefined) { + void ui.ShowConfigureIntelliSenseButton(false, this, ConfigurationType.ConfigProvider, showButtonSender); + } else if (changedSettings["default.compileCommands"] !== undefined) { + void ui.ShowConfigureIntelliSenseButton(false, this, ConfigurationType.CompileCommands, showButtonSender); + } if (changedSettings["default.compilerPath"] !== undefined) { + void ui.ShowConfigureIntelliSenseButton(false, this, ConfigurationType.CompilerPath, showButtonSender); + } + } + if (changedSettings.legacyCompilerArgsBehavior) { + this.configuration.handleConfigurationChange(); + } + if (changedSettings["default.compilerPath"] !== undefined || changedSettings["default.compileCommands"] !== undefined || changedSettings["default.configurationProvider"] !== undefined) { + void ui.ShowConfigureIntelliSenseButton(false, this).catch(logAndReturn.undefined); + } + this.configuration.onDidChangeSettings(); + telemetry.logLanguageServerEvent("CppSettingsChange", changedSettings, undefined); + } + + return changedSettings; + } + + private prepareVisibleRanges(editors: readonly vscode.TextEditor[]): { [uri: string]: Range[] } { + const visibleRanges: { [uri: string]: Range[] } = {}; + editors.forEach(editor => { + // Use a map, to account for multiple editors for the same file. + // First, we just concat all ranges for the same file. + const uri: string = editor.document.uri.toString(); + if (!visibleRanges[uri]) { + visibleRanges[uri] = []; + } + visibleRanges[uri] = visibleRanges[uri].concat(editor.visibleRanges.map(makeLspRange)); + }); + + // We may need to merge visible ranges, if there are multiple editors for the same file, + // and some of the ranges overlap. + Object.keys(visibleRanges).forEach(uri => { + visibleRanges[uri] = util.mergeOverlappingRanges(visibleRanges[uri]); + }); + + return visibleRanges; + } + + // Handles changes to visible files/ranges, changes to current selection/position, + // and changes to the active text editor. Should only be called on the primary client. + public async onDidChangeVisibleTextEditors(editors: readonly vscode.TextEditor[]): Promise { + const params: DidChangeVisibleTextEditorsParams = { + visibleRanges: this.prepareVisibleRanges(editors) + }; + if (vscode.window.activeTextEditor) { + if (util.isCpp(vscode.window.activeTextEditor.document)) { + params.activeUri = vscode.window.activeTextEditor.document.uri.toString(); + params.activeSelection = makeLspRange(vscode.window.activeTextEditor.selection); + } + } + + await this.languageClient.sendNotification(DidChangeVisibleTextEditorsNotification, params); + } + + public async onDidChangeTextEditorVisibleRanges(uri: vscode.Uri): Promise { + // VS Code will notify us of a particular editor, but same file may be open in + // multiple editors, so we coalesc those visible ranges. + const editors: vscode.TextEditor[] = vscode.window.visibleTextEditors.filter(editor => editor.document.uri === uri); + + let visibleRanges: Range[] = []; + if (editors.length === 1) { + visibleRanges = editors[0].visibleRanges.map(makeLspRange); + } else { + editors.forEach(editor => { + // Use a map, to account for multiple editors for the same file. + // First, we just concat all ranges for the same file. + visibleRanges = visibleRanges.concat(editor.visibleRanges.map(makeLspRange)); + }); + } + + const params: DidChangeTextEditorVisibleRangesParams = { + uri: uri.toString(), + visibleRanges + }; + + await this.languageClient.sendNotification(DidChangeTextEditorVisibleRangesNotification, params); + } + + public onDidChangeTextDocument(textDocumentChangeEvent: vscode.TextDocumentChangeEvent): void { + if (util.isCpp(textDocumentChangeEvent.document)) { + // If any file has changed, we need to abort the current rename operation + if (workspaceReferences.renamePending) { + workspaceReferences.cancelCurrentReferenceRequest(refs.CancellationSender.User); + } + + const oldVersion: number | undefined = openFileVersions.get(textDocumentChangeEvent.document.uri.toString()); + const newVersion: number = textDocumentChangeEvent.document.version; + if (oldVersion === undefined || newVersion > oldVersion) { + openFileVersions.set(textDocumentChangeEvent.document.uri.toString(), newVersion); + } + } + } + + public onDidOpenTextDocument(document: vscode.TextDocument): void { + if (document.uri.scheme === "file") { + const uri: string = document.uri.toString(); + openFileVersions.set(uri, document.version); + void SessionState.buildAndDebugIsSourceFile.set(util.isCppOrCFile(document.uri)); + void SessionState.buildAndDebugIsFolderOpen.set(util.isFolderOpen(document.uri)); + } else { + void SessionState.buildAndDebugIsSourceFile.set(false); + } + } + + public onDidCloseTextDocument(document: vscode.TextDocument): void { + const uri: string = document.uri.toString(); + if (this.semanticTokensProvider) { + this.semanticTokensProvider.removeFile(uri); + } + if (this.inlayHintsProvider) { + this.inlayHintsProvider.removeFile(uri); + } + this.inactiveRegionsDecorations.delete(uri); + if (diagnosticsCollectionIntelliSense) { + diagnosticsCollectionIntelliSense.delete(document.uri); + } + openFileVersions.delete(uri); + } + + public isProviderRegistered(extensionId: string | undefined): boolean { + if (extensionId === undefined || this.registeredProviders === undefined) { + return false; + } + for (const provider of this.registeredProviders.Value) { + if (provider === extensionId) { + return true; + } + } + return false; + } + + private async onCustomConfigurationProviderRegistered(provider: CustomConfigurationProvider1): Promise { + // version 2 providers control the browse.path. Avoid thrashing the tag parser database by pausing parsing until + // the provider has sent the correct browse.path value. + if (provider.version >= Version.v2) { + return this.pauseParsing(); + } + } + + public async onRegisterCustomConfigurationProvider(provider: CustomConfigurationProvider1): Promise { + await this.ready; + + if (this.registeredProviders === undefined // Shouldn't happen. + // Prevent duplicate processing. + || this.registeredProviders.Value.includes(provider.extensionId)) { + return; + } + this.registeredProviders.Value.push(provider.extensionId); + const rootFolder: vscode.WorkspaceFolder | undefined = this.RootFolder; + if (!rootFolder) { + return; // There is no c_cpp_properties.json to edit because there is no folder open. + } + this.configuration.handleConfigurationChange(); + if (this.configStateReceived.configProviders === undefined) { + this.configStateReceived.configProviders = []; + } + this.configStateReceived.configProviders.push(provider); + const selectedProvider: string | undefined = this.configuration.CurrentConfigurationProvider; + if (!selectedProvider || this.showConfigureIntelliSenseButton) { + void this.handleConfigStatus("configProviders"); + if (!selectedProvider) { + return; + } + } + if (isSameProviderExtensionId(selectedProvider, provider.extensionId)) { + void this.onCustomConfigurationProviderRegistered(provider).catch(logAndReturn.undefined); + telemetry.logLanguageServerEvent("customConfigurationProvider", { "providerId": provider.extensionId }); + } else if (selectedProvider === provider.name) { + void this.onCustomConfigurationProviderRegistered(provider).catch(logAndReturn.undefined); + await this.configuration.updateCustomConfigurationProvider(provider.extensionId); // v0 -> v1 upgrade. Update the configurationProvider in c_cpp_properties.json + } + } + + public async updateCustomConfigurations(requestingProvider?: CustomConfigurationProvider1): Promise { + await this.ready; + + if (!this.configurationProvider) { + return this.clearCustomConfigurations(); + } + const currentProvider: CustomConfigurationProvider1 | undefined = getCustomConfigProviders().get(this.configurationProvider); + if (!currentProvider) { + return this.clearCustomConfigurations(); + } + if (requestingProvider && requestingProvider.extensionId !== currentProvider.extensionId) { + // If we are being called by a configuration provider other than the current one, ignore it. + return; + } + if (!currentProvider.isReady) { + return; + } + + await this.clearCustomConfigurations(); + } + + public async updateCustomBrowseConfiguration(requestingProvider?: CustomConfigurationProvider1): Promise { + await this.ready; + + if (!this.configurationProvider) { + return; + } + + const currentProvider: CustomConfigurationProvider1 | undefined = getCustomConfigProviders().get(this.configurationProvider); + if (!currentProvider || !currentProvider.isReady || (requestingProvider && requestingProvider.extensionId !== currentProvider.extensionId)) { + return; + } + + const tokenSource: vscode.CancellationTokenSource = new vscode.CancellationTokenSource(); + let config: WorkspaceBrowseConfiguration | null = null; + let hasCompleted: boolean = false; + try { + if (this.RootUri && await currentProvider.canProvideBrowseConfigurationsPerFolder(tokenSource.token)) { + config = await currentProvider.provideFolderBrowseConfiguration(this.RootUri, tokenSource.token); + } else if (await currentProvider.canProvideBrowseConfiguration(tokenSource.token)) { + config = await currentProvider.provideBrowseConfiguration(tokenSource.token); + } else if (currentProvider.version >= Version.v2) { + console.warn("failed to provide browse configuration"); + } + } catch { + if (!hasCompleted) { + hasCompleted = true; + if (currentProvider.version >= Version.v2) { + await this.resumeParsing(); + } + } + } + + // Initiate request for custom configuration. + // Resume parsing on either resolve or reject, only if parsing was not resumed due to timeout + + if (config) { + if (currentProvider.version < Version.v3) { + // This is to get around the (fixed) CMake Tools bug: https://github.com/microsoft/vscode-cmake-tools/issues/1073 + for (const c of config.browsePath) { + if (vscode.workspace.getWorkspaceFolder(vscode.Uri.file(c)) === this.RootFolder) { + this.sendCustomBrowseConfiguration(config, currentProvider.extensionId, currentProvider.version); + break; + } + } + } else { + this.sendCustomBrowseConfiguration(config, currentProvider.extensionId, currentProvider.version); + } + if (!hasCompleted) { + hasCompleted = true; + if (currentProvider.version >= Version.v2) { + await this.resumeParsing(); + } + } + } + + // Set up a timeout to use previously received configuration and resume parsing if the provider times out + global.setTimeout(() => { + if (!hasCompleted) { + hasCompleted = true; + this.sendCustomBrowseConfiguration(null, undefined, Version.v0, true); + if (currentProvider.version >= Version.v2) { + console.warn(`Configuration Provider timed out in ${configProviderTimeout}ms.`); + void this.resumeParsing().catch(logAndReturn.undefined); + } + } + }, configProviderTimeout); + + } + + public toggleReferenceResultsView(): void { + workspaceReferences.toggleGroupView(); + } + + public async logDiagnostics(): Promise { + await this.ready; + const response: GetDiagnosticsResult = await this.languageClient.sendRequest(GetDiagnosticsRequest, null); + const diagnosticsChannel: vscode.OutputChannel = getDiagnosticsChannel(); + diagnosticsChannel.clear(); + + const header: string = `-------- Diagnostics - ${new Date().toLocaleString()}\n`; + const version: string = `Version: ${util.packageJson.version}\n`; + let configJson: string = ""; + if (this.configuration.CurrentConfiguration) { + configJson = `Current Configuration:\n${JSON.stringify(this.configuration.CurrentConfiguration, null, 4)}\n`; + } + const userModifiedSettings = Object.entries(this.settingsTracker.getUserModifiedSettings()); + if (userModifiedSettings.length > 0) { + const settings: Record = {}; + for (const [key] of userModifiedSettings) { + // Some settings were renamed during a telemetry change, so we need to undo that here. + const realKey = key.endsWith('2') ? key.slice(0, key.length - 1) : key; + const fullKey = `C_Cpp.${realKey}`; + settings[fullKey] = vscode.workspace.getConfiguration("C_Cpp").get(realKey) ?? ''; + } + configJson += `Modified Settings:\n${JSON.stringify(settings, null, 4)}\n`; + } + + { + const editorSettings = new OtherSettings(this.RootUri); + const settings: Record = {}; + settings.editorTabSize = editorSettings.editorTabSize; + settings.editorInsertSpaces = editorSettings.editorInsertSpaces; + settings.editorAutoClosingBrackets = editorSettings.editorAutoClosingBrackets; + settings.filesEncoding = editorSettings.filesEncoding; + settings.filesAssociations = editorSettings.filesAssociations; + settings.filesExclude = editorSettings.filesExclude; + settings.filesAutoSaveAfterDelay = editorSettings.filesAutoSaveAfterDelay; + settings.editorInlayHintsEnabled = editorSettings.editorInlayHintsEnabled; + settings.editorParameterHintsEnabled = editorSettings.editorParameterHintsEnabled; + settings.searchExclude = editorSettings.searchExclude; + settings.workbenchSettingsEditor = editorSettings.workbenchSettingsEditor; + configJson += `Additional Tracked Settings:\n${JSON.stringify(settings, null, 4)}\n`; + } + + // Get diagnostics for configuration provider info. + let configurationLoggingStr: string = ""; + const tuSearchStart: number = response.diagnostics.indexOf("Translation Unit Mappings:"); + if (tuSearchStart >= 0) { + const tuSearchEnd: number = response.diagnostics.indexOf("Translation Unit Configurations:"); + if (tuSearchEnd >= 0 && tuSearchEnd > tuSearchStart) { + let tuSearchString: string = response.diagnostics.substring(tuSearchStart, tuSearchEnd); + let tuSearchIndex: number = tuSearchString.indexOf("["); + while (tuSearchIndex >= 0) { + const tuMatch: RegExpMatchArray | null = tuSearchString.match(/\[\s(.*)\s\]/); + if (tuMatch && tuMatch.length > 1) { + const tuPath: string = vscode.Uri.file(tuMatch[1]).toString(); + if (this.configurationLogging.has(tuPath)) { + if (configurationLoggingStr.length === 0) { + configurationLoggingStr += "Custom configurations:\n"; + } + configurationLoggingStr += `[ ${tuMatch[1]} ]\n${this.configurationLogging.get(tuPath)}\n`; + } + } + tuSearchString = tuSearchString.substring(tuSearchIndex + 1); + tuSearchIndex = tuSearchString.indexOf("["); + } + } + } + diagnosticsChannel.appendLine(`${header}${version}${configJson}${this.browseConfigurationLogging}${configurationLoggingStr}${response.diagnostics}`); + diagnosticsChannel.show(false); + } + + public async rescanFolder(): Promise { + await this.ready; + return this.languageClient.sendNotification(RescanFolderNotification); + } + + public async provideCustomConfiguration(docUri: vscode.Uri): Promise { + const onFinished: () => void = () => { + void this.languageClient.sendNotification(FinishedRequestCustomConfig, { uri: docUri.toString() }); + }; + try { + const providerId: string | undefined = this.configurationProvider; + if (!providerId) { + return; + } + const provider: CustomConfigurationProvider1 | undefined = getCustomConfigProviders().get(providerId); + if (!provider || !provider.isReady) { + return; + } + const resultCode = await this.provideCustomConfigurationAsync(docUri, provider); + telemetry.logLanguageServerEvent('provideCustomConfiguration', { providerId, resultCode }); + } finally { + onFinished(); + } + } + + private async provideCustomConfigurationAsync(docUri: vscode.Uri, provider: CustomConfigurationProvider1): Promise { + const tokenSource: vscode.CancellationTokenSource = new vscode.CancellationTokenSource(); + + // Need to loop through candidates, to see if we can get a custom configuration from any of them. + // Wrap all lookups in a single task, so we can apply a timeout to the entire duration. + const provideConfigurationAsync: () => Thenable = async () => { + try { + if (!await provider.canProvideConfiguration(docUri, tokenSource.token)) { + return []; + } + } catch (err) { + console.warn("Caught exception from canProvideConfiguration"); + } + let configs: util.Mutable[] = []; + try { + configs = await provider.provideConfigurations([docUri], tokenSource.token); + } catch (err) { + console.warn("Caught exception from provideConfigurations"); + } + + if (configs && configs.length > 0 && configs[0]) { + const fileConfiguration: configs.Configuration | undefined = this.configuration.CurrentConfiguration; + if (fileConfiguration?.mergeConfigurations) { + configs.forEach(config => { + if (fileConfiguration.includePath) { + fileConfiguration.includePath.forEach(p => { + if (!config.configuration.includePath.includes(p)) { + config.configuration.includePath.push(p); + } + }); + } + + if (fileConfiguration.defines) { + fileConfiguration.defines.forEach(d => { + if (!config.configuration.defines.includes(d)) { + config.configuration.defines.push(d); + } + }); + } + + if (!config.configuration.forcedInclude) { + config.configuration.forcedInclude = []; + } + + if (fileConfiguration.forcedInclude) { + fileConfiguration.forcedInclude.forEach(i => { + if (config.configuration.forcedInclude) { + if (!config.configuration.forcedInclude.includes(i)) { + config.configuration.forcedInclude.push(i); + } + } + }); + } + }); + } + return configs as SourceFileConfigurationItem[]; + } + return undefined; + }; + let result: string = "success"; + try { + const configs: SourceFileConfigurationItem[] | undefined = await this.callTaskWithTimeout(provideConfigurationAsync, configProviderTimeout, tokenSource); + if (configs && configs.length > 0) { + this.sendCustomConfigurations(configs, provider.version); + } else { + result = "noConfigurations"; + } + } catch (err) { + result = "timeout"; + const settings: CppSettings = new CppSettings(this.RootUri); + if (settings.isConfigurationWarningsEnabled && !this.isExternalHeader(docUri) && !vscode.debug.activeDebugSession) { + const dismiss: string = localize("dismiss.button", "Dismiss"); + const disable: string = localize("disable.warnings.button", "Disable Warnings"); + const configName: string | undefined = this.configuration.CurrentConfiguration?.name; + if (!configName) { + return "noConfigName"; + } + let message: string = localize("unable.to.provide.configuration", + "{0} is unable to provide IntelliSense configuration information for '{1}'. Settings from the '{2}' configuration will be used instead.", + provider.name, docUri.fsPath, configName); + if (err) { + message += ` (${err})`; + } + + if (await vscode.window.showInformationMessage(message, dismiss, disable) === disable) { + settings.toggleSetting("configurationWarnings", "enabled", "disabled"); + } + } + } + return result; + } + + private handleRequestCustomConfig(file: string): void { + const uri: vscode.Uri = vscode.Uri.file(file); + const client: Client = clients.getClientFor(uri); + if (client instanceof DefaultClient) { + const defaultClient: DefaultClient = client as DefaultClient; + void defaultClient.provideCustomConfiguration(uri).catch(logAndReturn.undefined); + } + } + + private isExternalHeader(uri: vscode.Uri): boolean { + const rootUri: vscode.Uri | undefined = this.RootUri; + return !rootUri || (util.isHeaderFile(uri) && !uri.toString().startsWith(rootUri.toString())); + } + + public async getCurrentConfigName(): Promise { + await this.ready; + return this.configuration.CurrentConfiguration?.name; + } + + public async getCurrentConfigCustomVariable(variableName: string): Promise { + await this.ready; + return this.configuration.CurrentConfiguration?.customConfigurationVariables?.[variableName] ?? ''; + } + + public async setCurrentConfigName(configurationName: string): Promise { + await this.ready; + + const configurations: configs.Configuration[] = this.configuration.Configurations ?? []; + const configurationIndex: number = configurations.findIndex((config) => config.name === configurationName); + + if (configurationIndex === -1) { + throw new Error(localize("config.not.found", "The requested configuration name is not found: {0}", configurationName)); + } + this.configuration.select(configurationIndex); + } + + public async getCurrentCompilerPathAndArgs(): Promise { + const settings: CppSettings = new CppSettings(this.RootUri); + await this.ready; + return util.extractCompilerPathAndArgs(!!settings.legacyCompilerArgsBehavior, + this.configuration.CurrentConfiguration?.compilerPath, + this.configuration.CurrentConfiguration?.compilerArgs); + + } + + public async getVcpkgInstalled(): Promise { + await this.ready; + return this.configuration.VcpkgInstalled; + } + + public getVcpkgEnabled(): Promise { + const cppSettings: CppSettings = new CppSettings(this.RootUri); + return Promise.resolve(cppSettings.vcpkgEnabled); + } + + public async getKnownCompilers(): Promise { + await this.ready; + return this.configuration.KnownCompiler; + } + + /** + * Take ownership of a document that was previously serviced by another client. + * This process involves sending a textDocument/didOpen message to the server so + * that it knows about the file, as well as adding it to this client's set of + * tracked documents. + */ + public takeOwnership(document: vscode.TextDocument): void { + this.trackedDocuments.set(document.uri.toString(), document); + } + + // Only used in crash recovery. Otherwise, VS Code sends didOpen directly to native process (through the protocolFilter). + public async sendDidOpen(document: vscode.TextDocument): Promise { + const params: DidOpenTextDocumentParams = { + textDocument: { + uri: document.uri.toString(), + languageId: document.languageId, + version: document.version, + text: document.getText() + } + }; + await this.ready; + await this.languageClient.sendNotification(DidOpenNotification, params); + } + + public async getIncludes(maxDepth: number, token: vscode.CancellationToken): Promise { + const params: GetIncludesParams = { maxDepth: maxDepth }; + await this.ready; + return DefaultClient.withLspCancellationHandling( + () => this.languageClient.sendRequest(IncludesRequest, params, token), token); + } + + public async getChatContext(token: vscode.CancellationToken): Promise { + await withCancellation(this.ready, token); + return DefaultClient.withLspCancellationHandling( + () => this.languageClient.sendRequest(CppContextRequest, null, token), token); + } + + /** + * a Promise that can be awaited to know when it's ok to proceed. + * + * This is a lighter-weight complement to `enqueue()` + * + * Use `await .ready` when you need to ensure that the client is initialized, and to run in order + * Use `enqueue()` when you want to ensure that subsequent calls are blocked until a critical bit of code is run. + * + * This is lightweight, because if the queue is empty, then the only thing to wait for is the client itself to be initialized + */ + get ready(): Promise { + if (!DefaultClient.dispatching.isCompleted || DefaultClient.queue.length) { + // if the dispatcher has stuff going on, then we need to stick in a promise into the queue so we can + // be notified when it's our turn + const p = new ManualPromise(); + DefaultClient.queue.push([p as ManualPromise]); + return p; + } + + // otherwise, we're only waiting for the client to be in an initialized state, in which case just wait for that. + return DefaultClient.isStarted; + } + + /** + * Enqueue a task to ensure that the order is maintained. The tasks are executed sequentially after the client is ready. + * + * this is a bit more expensive than `.ready` - this ensures the task is absolutely finished executing before allowing + * the dispatcher to move forward. + * + * Use `enqueue()` when you want to ensure that subsequent calls are blocked until a critical bit of code is run. + * Use `await .ready` when you need to ensure that the client is initialized, and still run in order. + */ + enqueue(task: () => Promise) { + ok(this.isSupported, localize("unsupported.client", "Unsupported client")); + + // create a placeholder promise that is resolved when the task is complete. + const result = new ManualPromise(); + + // add the task to the queue + DefaultClient.queue.push([result, task]); + + // if we're not already dispatching, start + if (DefaultClient.dispatching.isSet) { + // start dispatching + void DefaultClient.dispatch(); + } + + // return the placeholder promise to the caller. + return result as Promise; + } + + /** + * The dispatch loop asynchronously processes items in the async queue in order, and ensures that tasks are dispatched in the + * order they were inserted. + */ + private static async dispatch() { + // reset the promise for the dispatcher + DefaultClient.dispatching.reset(); + + do { + // ensure that this is OK to start working + await this.isStarted; + + // pick items up off the queue and run then one at a time until the queue is empty + const [promise, task] = DefaultClient.queue.shift() ?? []; + if (is.promise(promise)) { + try { + promise.resolve(task ? await task() : undefined); + } catch (e) { + console.log(e); + promise.reject(e); + } + } + } while (DefaultClient.queue.length); + + // unblock anything that is waiting for the dispatcher to empty + this.dispatching.resolve(); + } + + private static async withLspCancellationHandling(task: () => Promise, token: vscode.CancellationToken): Promise { + let result: T; + + try { + result = await task(); + } catch (e: any) { + if (e instanceof ResponseError && (e.code === RequestCancelled || e.code === ServerCancelled)) { + throw new vscode.CancellationError(); + } else { + throw e; + } + } + + if (token.isCancellationRequested) { + throw new vscode.CancellationError(); + } + + return result; + } + + private callTaskWithTimeout(task: () => Thenable, ms: number, cancelToken?: vscode.CancellationTokenSource): Promise { + let timer: NodeJS.Timeout; + + // Create a promise that rejects in milliseconds + const timeout: () => Promise = () => new Promise((resolve, reject) => { + timer = global.setTimeout(() => { + clearTimeout(timer); + if (cancelToken) { + cancelToken.cancel(); + } + reject(localize("timed.out", "Timed out in {0}ms.", ms)); + }, ms); + }); + + // Returns a race between our timeout and the passed in promise + return Promise.race([task(), timeout()]).then( + (result: any) => { + clearTimeout(timer); + return result; + }, + (error: any) => { + clearTimeout(timer); + throw error; + }); + } + + /** + * listen for notifications from the language server. + */ + private registerNotifications(): void { + console.assert(this.languageClient !== undefined, "This method must not be called until this.languageClient is set in \"onReady\""); + + this.languageClient.onNotification(ReloadWindowNotification, () => void util.promptForReloadWindowDueToSettingsChange()); + this.languageClient.onNotification(UpdateTrustedCompilersNotification, (e) => void this.addTrustedCompiler(e.compilerPath)); + this.languageClient.onNotification(LogTelemetryNotification, (e) => this.logTelemetry(e)); + this.languageClient.onNotification(ReportStatusNotification, (e) => void this.updateStatus(e)); + this.languageClient.onNotification(ReportTagParseStatusNotification, (e) => this.updateTagParseStatus(e)); + this.languageClient.onNotification(CompileCommandsPathsNotification, (e) => void this.promptCompileCommands(e)); + this.languageClient.onNotification(ReferencesNotification, (e) => this.processReferencesPreview(e)); + this.languageClient.onNotification(ReportReferencesProgressNotification, (e) => this.handleReferencesProgress(e)); + this.languageClient.onNotification(RequestCustomConfig, (e) => this.handleRequestCustomConfig(e)); + this.languageClient.onNotification(IntelliSenseResultNotification, (e) => this.handleIntelliSenseResult(e)); + this.languageClient.onNotification(PublishRefactorDiagnosticsNotification, publishRefactorDiagnostics); + RegisterCodeAnalysisNotifications(this.languageClient); + this.languageClient.onNotification(ShowMessageWindowNotification, showMessageWindow); + this.languageClient.onNotification(ShowWarningNotification, showWarning); + this.languageClient.onNotification(ReportTextDocumentLanguage, (e) => this.setTextDocumentLanguage(e)); + this.languageClient.onNotification(IntelliSenseSetupNotification, (e) => this.logIntelliSenseSetupTime(e)); + this.languageClient.onNotification(SetTemporaryTextDocumentLanguageNotification, (e) => void this.setTemporaryTextDocumentLanguage(e)); + this.languageClient.onNotification(ReportCodeAnalysisProcessedNotification, (e) => this.updateCodeAnalysisProcessed(e)); + this.languageClient.onNotification(ReportCodeAnalysisTotalNotification, (e) => this.updateCodeAnalysisTotal(e)); + this.languageClient.onNotification(DoxygenCommentGeneratedNotification, (e) => void this.insertDoxygenComment(e)); + this.languageClient.onNotification(CanceledReferencesNotification, this.serverCanceledReferences); + } + + private handleIntelliSenseResult(intelliSenseResult: IntelliSenseResult): void { + const fileVersion: number | undefined = openFileVersions.get(intelliSenseResult.uri); + if (fileVersion !== undefined && fileVersion !== intelliSenseResult.fileVersion) { + return; + } + + if (this.semanticTokensProvider) { + this.semanticTokensProvider.deliverTokens(intelliSenseResult.uri, intelliSenseResult.semanticTokens, intelliSenseResult.clearExistingSemanticTokens); + } + if (this.inlayHintsProvider) { + this.inlayHintsProvider.deliverInlayHints(intelliSenseResult.uri, intelliSenseResult.inlayHints, intelliSenseResult.clearExistingInlayHint); + } + + this.updateInactiveRegions(intelliSenseResult.uri, intelliSenseResult.inactiveRegions, intelliSenseResult.clearExistingInactiveRegions, intelliSenseResult.isCompletePass); + if (intelliSenseResult.clearExistingDiagnostics || intelliSenseResult.diagnostics.length > 0) { + this.updateSquiggles(intelliSenseResult.uri, intelliSenseResult.diagnostics, intelliSenseResult.clearExistingDiagnostics); + } + } + + private updateSquiggles(uriString: string, diagnostics: IntelliSenseDiagnostic[], startNewSet: boolean): void { + + if (!diagnosticsCollectionIntelliSense) { + diagnosticsCollectionIntelliSense = vscode.languages.createDiagnosticCollection(configPrefix + "IntelliSense"); + } + + // Convert from our Diagnostic objects to vscode Diagnostic objects + + const diagnosticsIntelliSense: vscode.Diagnostic[] = []; + diagnostics.forEach((d) => { + const message: string = getLocalizedString(d.localizeStringParams); + const diagnostic: vscode.Diagnostic = new vscode.Diagnostic(makeVscodeRange(d.range), message, d.severity); + diagnostic.code = d.code; + diagnostic.source = CppSourceStr; + if (d.relatedInformation) { + diagnostic.relatedInformation = []; + for (const info of d.relatedInformation) { + diagnostic.relatedInformation.push(new vscode.DiagnosticRelatedInformation(makeVscodeLocation(info.location), info.message)); + } + } + + diagnosticsIntelliSense.push(diagnostic); + }); + + const realUri: vscode.Uri = vscode.Uri.parse(uriString); + if (!startNewSet) { + const existingDiagnostics: readonly vscode.Diagnostic[] | undefined = diagnosticsCollectionIntelliSense.get(realUri); + if (existingDiagnostics) { + // Note: The spread operator puts every element on the stack, so it should be avoided for large arrays. + Array.prototype.push.apply(diagnosticsIntelliSense, existingDiagnostics as any[]); + } + } + diagnosticsCollectionIntelliSense.set(realUri, diagnosticsIntelliSense); + + clients.timeTelemetryCollector.setUpdateRangeTime(realUri); + } + + private setTextDocumentLanguage(languageStr: string): void { + const cppSettings: CppSettings = new CppSettings(); + if (cppSettings.autoAddFileAssociations) { + const is_c: boolean = languageStr.startsWith("c;"); + const is_cuda: boolean = languageStr.startsWith("cu;"); + languageStr = languageStr.substring(is_c ? 2 : is_cuda ? 3 : 1); + this.addFileAssociations(languageStr, is_c ? "c" : is_cuda ? "cuda-cpp" : "cpp"); + } + } + + private async setTemporaryTextDocumentLanguage(params: SetTemporaryTextDocumentLanguageParams): Promise { + const languageId: string = params.isC ? "c" : params.isCuda ? "cuda-cpp" : "cpp"; + const uri: vscode.Uri = vscode.Uri.parse(params.uri); + const client: Client = clients.getClientFor(uri); + const document: vscode.TextDocument | undefined = client.TrackedDocuments.get(params.uri); + if (!!document && document.languageId !== languageId) { + if (document.languageId === "cpp" && languageId === "c") { + handleChangedFromCppToC(document); + } + await vscode.languages.setTextDocumentLanguage(document, languageId); + } + } + + private associations_for_did_change?: Set; + + /** + * listen for file created/deleted events under the ${workspaceFolder} folder + */ + private registerFileWatcher(): void { + console.assert(this.languageClient !== undefined, "This method must not be called until this.languageClient is set in \"onReady\""); + + if (this.rootFolder) { + // WARNING: The default limit on Linux is 8k, so for big directories, this can cause file watching to fail. + this.rootPathFileWatcher = vscode.workspace.createFileSystemWatcher( + "**/*", + false /* ignoreCreateEvents */, + false /* ignoreChangeEvents */, + false /* ignoreDeleteEvents */); + + this.rootPathFileWatcher.onDidCreate(async (uri) => { + if (uri.scheme !== 'file') { + return; + } + const fileName: string = path.basename(uri.fsPath).toLowerCase(); + if (fileName === ".editorconfig") { + cachedEditorConfigSettings.clear(); + cachedEditorConfigLookups.clear(); + this.updateActiveDocumentTextOptions(); + } + if (fileName === ".clang-format" || fileName === "_clang-format") { + cachedEditorConfigLookups.clear(); + } + + void this.languageClient.sendNotification(FileCreatedNotification, { uri: uri.toString() }).catch(logAndReturn.undefined); + }); + + // TODO: Handle new associations without a reload. + this.associations_for_did_change = new Set(["cu", "cuh", "c", "i", "cpp", "cc", "cxx", "c++", "cp", "hpp", "hh", "hxx", "h++", "hp", "h", "ii", "ino", "inl", "ipp", "tcc", "idl"]); + const assocs: any = new OtherSettings().filesAssociations; + for (const assoc in assocs) { + const dotIndex: number = assoc.lastIndexOf('.'); + if (dotIndex !== -1) { + const ext: string = assoc.substring(dotIndex + 1); + this.associations_for_did_change.add(ext); + } + } + this.rootPathFileWatcher.onDidChange(async (uri) => { + if (uri.scheme !== 'file') { + return; + } + const dotIndex: number = uri.fsPath.lastIndexOf('.'); + const fileName: string = path.basename(uri.fsPath).toLowerCase(); + if (fileName === ".editorconfig") { + cachedEditorConfigSettings.clear(); + cachedEditorConfigLookups.clear(); + this.updateActiveDocumentTextOptions(); + } + if (dotIndex !== -1) { + const ext: string = uri.fsPath.substring(dotIndex + 1); + if (this.associations_for_did_change?.has(ext)) { + // 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. + // We workaround this via only sending the change message if the modified time is within 10 seconds. + const mtime: Date = fs.statSync(uri.fsPath).mtime; + const duration: number = Date.now() - mtime.getTime(); + if (duration < 10000) { + void this.languageClient.sendNotification(FileChangedNotification, { uri: uri.toString() }).catch(logAndReturn.undefined); + } + } + } + }); + + this.rootPathFileWatcher.onDidDelete((uri) => { + if (uri.scheme !== 'file') { + return; + } + const fileName: string = path.basename(uri.fsPath).toLowerCase(); + if (fileName === ".editorconfig") { + cachedEditorConfigSettings.clear(); + cachedEditorConfigLookups.clear(); + } + if (fileName === ".clang-format" || fileName === "_clang-format") { + cachedEditorConfigLookups.clear(); + } + void this.languageClient.sendNotification(FileDeletedNotification, { uri: uri.toString() }).catch(logAndReturn.undefined); + }); + + this.disposables.push(this.rootPathFileWatcher); + } else { + this.rootPathFileWatcher = undefined; + } + } + + /** + * handle notifications coming from the language server + */ + + public addFileAssociations(fileAssociations: string, languageId: string): void { + const settings: OtherSettings = new OtherSettings(); + const assocs: any = settings.filesAssociations; + + let foundNewAssociation: boolean = false; + const filesAndPaths: string[] = fileAssociations.split(";"); + for (let i: number = 0; i < filesAndPaths.length; ++i) { + const fileAndPath: string[] = filesAndPaths[i].split("@"); + // Skip empty or malformed + if (fileAndPath.length === 2) { + const file: string = fileAndPath[0]; + const filePath: string = fileAndPath[1]; + if ((file in assocs) || (("**/" + file) in assocs)) { + continue; // File already has an association. + } + const j: number = file.lastIndexOf('.'); + if (j !== -1) { + const ext: string = file.substring(j); + if ((("*" + ext) in assocs) || (("**/*" + ext) in assocs)) { + continue; // Extension already has an association. + } + } + let foundGlobMatch: boolean = false; + for (const assoc in assocs) { + const matcher = new minimatch.Minimatch(assoc); + if (matcher.match(filePath)) { + foundGlobMatch = true; + break; // Assoc matched a glob pattern. + } + } + if (foundGlobMatch) { + continue; + } + assocs[file] = languageId; + foundNewAssociation = true; + } + } + if (foundNewAssociation) { + settings.filesAssociations = assocs; + } + } + + private logTelemetry(notificationBody: TelemetryPayload): void { + if (notificationBody.event === "includeSquiggles" && this.configurationProvider && notificationBody.properties) { + notificationBody.properties["providerId"] = this.configurationProvider; + } + telemetry.logLanguageServerEvent(notificationBody.event, notificationBody.properties, notificationBody.metrics); + } + + private async updateStatus(notificationBody: ReportStatusNotificationBody): Promise { + const message: string = notificationBody.status; + util.setProgress(util.getProgressExecutableSuccess()); + const testHook: TestHook = getTestHook(); + if (message.endsWith("Idle")) { + // nothing to do + } else if (message.endsWith("Parsing")) { + this.model.isParsingWorkspace.Value = true; + this.model.isInitializingWorkspace.Value = false; + this.model.isIndexingWorkspace.Value = false; + const status: IntelliSenseStatus = { status: Status.TagParsingBegun }; + testHook.updateStatus(status); + } else if (message.endsWith("Initializing")) { + this.model.isInitializingWorkspace.Value = true; + this.model.isIndexingWorkspace.Value = false; + this.model.isParsingWorkspace.Value = false; + } else if (message.endsWith("Indexing")) { + this.model.isIndexingWorkspace.Value = true; + this.model.isInitializingWorkspace.Value = false; + this.model.isParsingWorkspace.Value = false; + } else if (message.endsWith("files")) { + this.model.isParsingFiles.Value = true; + } else if (message.endsWith("IntelliSense")) { + timeStamp = Date.now(); + this.model.isUpdatingIntelliSense.Value = true; + const status: IntelliSenseStatus = { status: Status.IntelliSenseCompiling }; + testHook.updateStatus(status); + } else if (message.endsWith("IntelliSense done")) { + getOutputChannelLogger().appendLine(6, localize("update.intellisense.time", "Update IntelliSense time (sec): {0}", (Date.now() - timeStamp) / 1000)); + this.model.isUpdatingIntelliSense.Value = false; + const status: IntelliSenseStatus = { status: Status.IntelliSenseReady }; + testHook.updateStatus(status); + } else if (message.endsWith("Parsing done")) { // Tag Parser Ready + this.model.isParsingWorkspace.Value = false; + const status: IntelliSenseStatus = { status: Status.TagParsingDone }; + testHook.updateStatus(status); + util.setProgress(util.getProgressParseRootSuccess()); + } else if (message.endsWith("files done")) { + this.model.isParsingFiles.Value = false; + } else if (message.endsWith("Analysis")) { + this.model.isRunningCodeAnalysis.Value = true; + this.model.codeAnalysisTotal.Value = 1; + this.model.codeAnalysisProcessed.Value = 0; + } else if (message.endsWith("Analysis done")) { + this.model.isRunningCodeAnalysis.Value = false; + } else if (message.includes("Squiggles Finished - File name:")) { + const index: number = message.lastIndexOf(":"); + const name: string = message.substring(index + 2); + const status: IntelliSenseStatus = { status: Status.IntelliSenseReady, filename: name }; + testHook.updateStatus(status); + } else if (message.endsWith("No Squiggles")) { + util.setIntelliSenseProgress(util.getProgressIntelliSenseNoSquiggles()); + } + } + + private updateTagParseStatus(tagParseStatus: TagParseStatus): void { + this.model.parsingWorkspaceStatus.Value = getLocalizedString(tagParseStatus.localizeStringParams); + this.model.isParsingWorkspacePaused.Value = tagParseStatus.isPaused; + } + + private updateInactiveRegions(uriString: string, inactiveRegions: InputRegion[], startNewSet: boolean, updateFoldingRanges: boolean): void { + if (this.codeFoldingProvider && updateFoldingRanges) { + this.codeFoldingProvider.refresh(); + } + + const client: Client = clients.getClientFor(vscode.Uri.parse(uriString)); + if (!(client instanceof DefaultClient) || (!startNewSet && inactiveRegions.length === 0)) { + return; + } + const settings: CppSettings = new CppSettings(client.RootUri); + const dimInactiveRegions: boolean = settings.dimInactiveRegions; + let currentSet: DecorationRangesPair | undefined = this.inactiveRegionsDecorations.get(uriString); + if (startNewSet || !dimInactiveRegions) { + if (currentSet) { + currentSet.decoration.dispose(); + this.inactiveRegionsDecorations.delete(uriString); + } + if (!dimInactiveRegions) { + return; + } + currentSet = undefined; + } + if (currentSet === undefined) { + const opacity: number | undefined = settings.inactiveRegionOpacity; + currentSet = { + decoration: vscode.window.createTextEditorDecorationType({ + opacity: (opacity === undefined) ? "0.55" : opacity.toString(), + backgroundColor: settings.inactiveRegionBackgroundColor, + color: settings.inactiveRegionForegroundColor, + rangeBehavior: vscode.DecorationRangeBehavior.OpenOpen + }), + ranges: [] + }; + this.inactiveRegionsDecorations.set(uriString, currentSet); + } + + Array.prototype.push.apply(currentSet.ranges, inactiveRegions.map(element => new vscode.Range(element.startLine, 0, element.endLine, 0))); + + // Apply the decorations to all *visible* text editors + const editors: vscode.TextEditor[] = vscode.window.visibleTextEditors.filter(e => e.document.uri.toString() === uriString); + for (const e of editors) { + e.setDecorations(currentSet.decoration, currentSet.ranges); + } + } + + public logIntelliSenseSetupTime(notification: IntelliSenseSetup): void { + clients.timeTelemetryCollector.setSetupTime(vscode.Uri.parse(notification.uri)); + } + + private compileCommandsPaths: string[] = []; + private async promptCompileCommands(params: CompileCommandsPaths): Promise { + if (!params.workspaceFolderUri) { + return; + } + const potentialClient: Client = clients.getClientFor(vscode.Uri.file(params.workspaceFolderUri)); + const client: DefaultClient = potentialClient as DefaultClient; + if (!client) { + return; + } + if (client.configStateReceived.compileCommands) { + return; + } + + client.compileCommandsPaths = params.paths; + client.configStateReceived.compileCommands = true; + await client.handleConfigStatus("compileCommands"); + } + + public async handleConfigStatus(sender?: string): Promise { + if (!this.configStateReceived.timeout + && (!this.configStateReceived.compilers || !this.configStateReceived.compileCommands || !this.configStateReceived.configProviders)) { + return; // Wait till the config state is recevied or timed out. + } + + const rootFolder: vscode.WorkspaceFolder | undefined = this.RootFolder; + const settings: CppSettings = new CppSettings(this.RootUri); + const configProviderNotSet: boolean = !settings.defaultConfigurationProvider && !this.configuration.CurrentConfiguration?.configurationProvider && + !this.configuration.CurrentConfiguration?.configurationProviderInCppPropertiesJson; + const configProviderNotSetAndNoCache: boolean = configProviderNotSet && this.lastCustomBrowseConfigurationProviderId?.Value === undefined; + const compileCommandsNotSet: boolean = !settings.defaultCompileCommands && !this.configuration.CurrentConfiguration?.compileCommands && !this.configuration.CurrentConfiguration?.compileCommandsInCppPropertiesJson; + + // Handle config providers + const provider: CustomConfigurationProvider1 | undefined = + !this.configStateReceived.configProviders ? undefined : + this.configStateReceived.configProviders.length === 0 ? undefined : this.configStateReceived.configProviders[0]; + let showConfigStatus: boolean = false; + if (rootFolder && configProviderNotSetAndNoCache && provider && (sender === "configProviders")) { + const ask: PersistentFolderState = new PersistentFolderState("Client.registerProvider", true, rootFolder); + showConfigStatus = ask.Value; + } + + // Handle compile commands + if (rootFolder && configProviderNotSetAndNoCache && !this.configStateReceived.configProviders && + compileCommandsNotSet && this.compileCommandsPaths.length > 0 && (sender === "compileCommands")) { + const ask: PersistentFolderState = new PersistentFolderState("CPP.showCompileCommandsSelection", true, rootFolder); + showConfigStatus = ask.Value; + } + + const compilerPathNotSet: boolean = settings.defaultCompilerPath === null && this.configuration.CurrentConfiguration?.compilerPath === undefined && this.configuration.CurrentConfiguration?.compilerPathInCppPropertiesJson === undefined; + const configurationNotSet: boolean = configProviderNotSetAndNoCache && compileCommandsNotSet && compilerPathNotSet; + + showConfigStatus = showConfigStatus || (configurationNotSet && + !!compilerDefaults && !compilerDefaults.trustedCompilerFound && trustedCompilerPaths && (trustedCompilerPaths.length !== 1 || trustedCompilerPaths[0] !== "")); + + const configProviderType: ConfigurationType = this.configuration.ConfigProviderAutoSelected ? ConfigurationType.AutoConfigProvider : ConfigurationType.ConfigProvider; + const compilerType: ConfigurationType = this.configuration.CurrentConfiguration?.compilerPathIsExplicit ? ConfigurationType.CompilerPath : ConfigurationType.AutoCompilerPath; + const configType: ConfigurationType = + !configProviderNotSet ? configProviderType : + !compileCommandsNotSet ? ConfigurationType.CompileCommands : + !compilerPathNotSet ? compilerType : + ConfigurationType.NotConfigured; + + this.showConfigureIntelliSenseButton = showConfigStatus; + return ui.ShowConfigureIntelliSenseButton(showConfigStatus, this, configType, "handleConfig"); + } + + /** + * requests to the language server + */ + public async requestSwitchHeaderSource(rootUri: vscode.Uri, fileName: string): Promise { + const params: SwitchHeaderSourceParams = { + switchHeaderSourceFileName: fileName, + workspaceFolderUri: rootUri.toString() + }; + return this.enqueue(async () => this.languageClient.sendRequest(SwitchHeaderSourceRequest, params)); + } + + public async requestCompiler(newCompilerPath?: string): Promise { + const params: QueryDefaultCompilerParams = { + newTrustedCompilerPath: newCompilerPath ?? "" + }; + const results: configs.CompilerDefaults = await this.languageClient.sendRequest(QueryCompilerDefaultsRequest, params); + void SessionState.scanForCompilersDone.set(true); + void SessionState.scanForCompilersEmpty.set(results.knownCompilers === undefined || !results.knownCompilers.length); + void SessionState.trustedCompilerFound.set(results.trustedCompilerFound); + return results; + } + + public updateActiveDocumentTextOptions(): void { + const editor: vscode.TextEditor | undefined = vscode.window.activeTextEditor; + if (editor && util.isCpp(editor.document)) { + void SessionState.buildAndDebugIsSourceFile.set(util.isCppOrCFile(editor.document.uri)); + void SessionState.buildAndDebugIsFolderOpen.set(util.isFolderOpen(editor.document.uri)); + // If using vcFormat, check for a ".editorconfig" file, and apply those text options to the active document. + const settings: CppSettings = new CppSettings(this.RootUri); + if (settings.useVcFormat(editor.document)) { + const editorConfigSettings: any = getEditorConfigSettings(editor.document.uri.fsPath); + if (editorConfigSettings.indent_style === "space" || editorConfigSettings.indent_style === "tab") { + editor.options.insertSpaces = editorConfigSettings.indent_style === "space"; + if (editorConfigSettings.indent_size === "tab") { + if (!editorConfigSettings.tab_width !== undefined) { + editor.options.tabSize = editorConfigSettings.tab_width; + } + } else if (editorConfigSettings.indent_size !== undefined) { + editor.options.tabSize = editorConfigSettings.indent_size; + } + } + if (editorConfigSettings.end_of_line !== undefined) { + void editor.edit((edit) => { + edit.setEndOfLine(editorConfigSettings.end_of_line === "lf" ? vscode.EndOfLine.LF : vscode.EndOfLine.CRLF); + }).then(undefined, logAndReturn.undefined); + } + } + } else { + void SessionState.buildAndDebugIsSourceFile.set(false); + } + } + + /** + * notifications to the language server + */ + public async didChangeActiveEditor(editor?: vscode.TextEditor): Promise { + // For now, we ignore deactivation events. + // VS will refresh IntelliSense on activation, as a catch-all for file changes in + // other applications. But VS Code will deactivate the document when focus is moved + // to another control, such as the Output window. So, to avoid costly updates, we + // only trigger that update when focus moves from one C++ document to another. + // Fortunately, VS Code generates file-change notifications for all files + // in the workspace, so we should trigger appropriate updates for most changes + // made in other applications. + if (!editor || !util.isCpp(editor.document)) { + return; + } + + this.updateActiveDocumentTextOptions(); + + const params: DidChangeActiveEditorParams = { + uri: editor?.document?.uri.toString(), + selection: editor ? makeLspRange(editor.selection) : undefined + }; + + return this.languageClient.sendNotification(DidChangeActiveEditorNotification, params).catch(logAndReturn.undefined); + } + + /** + * send notifications to the language server to restart IntelliSense for the selected file. + */ + public async restartIntelliSenseForFile(document: vscode.TextDocument): Promise { + await this.ready; + return this.languageClient.sendNotification(RestartIntelliSenseForFileNotification, this.languageClient.code2ProtocolConverter.asTextDocumentIdentifier(document)).catch(logAndReturn.undefined); + } + + /** + * enable UI updates from this client and resume tag parsing on the server. + */ + public activate(): void { + this.model.activate(); + void this.resumeParsing().catch(logAndReturn.undefined); + } + + public async selectionChanged(selection: Range): Promise { + return this.languageClient.sendNotification(DidChangeTextEditorSelectionNotification, selection); + } + + public async resetDatabase(): Promise { + await this.ready; + return this.languageClient.sendNotification(ResetDatabaseNotification); + } + + /** + * disable UI updates from this client and pause tag parsing on the server. + */ + public deactivate(): void { + this.model.deactivate(); + } + + public async pauseParsing(): Promise { + await this.ready; + return this.languageClient.sendNotification(PauseParsingNotification); + } + + public async resumeParsing(): Promise { + await this.ready; + return this.languageClient.sendNotification(ResumeParsingNotification); + } + + public async PauseCodeAnalysis(): Promise { + await this.ready; + this.model.isCodeAnalysisPaused.Value = true; + return this.languageClient.sendNotification(PauseCodeAnalysisNotification); + } + + public async ResumeCodeAnalysis(): Promise { + await this.ready; + this.model.isCodeAnalysisPaused.Value = false; + return this.languageClient.sendNotification(ResumeCodeAnalysisNotification); + } + + public async CancelCodeAnalysis(): Promise { + await this.ready; + return this.languageClient.sendNotification(CancelCodeAnalysisNotification); + } + + private updateCodeAnalysisProcessed(processed: number): void { + this.model.codeAnalysisProcessed.Value = processed; + } + + private updateCodeAnalysisTotal(total: number): void { + this.model.codeAnalysisTotal.Value = total; + } + + private async insertDoxygenComment(result: GenerateDoxygenCommentResult): Promise { + const editor: vscode.TextEditor | undefined = vscode.window.activeTextEditor; + if (!editor) { + return; + } + const currentFileVersion: number | undefined = openFileVersions.get(editor.document.uri.toString()); + // Insert the comment only if the cursor has not moved + if (result.fileVersion === currentFileVersion && + result.initPosition.line === editor.selection.start.line && + result.initPosition.character === editor.selection.start.character && + result.contents.length > 1) { + const workspaceEdit: vscode.WorkspaceEdit = new vscode.WorkspaceEdit(); + const edits: vscode.TextEdit[] = []; + const maxColumn: number = 99999999; + const newRange: vscode.Range = new vscode.Range(editor.selection.start.line, 0, editor.selection.end.line, maxColumn); + edits.push(new vscode.TextEdit(newRange, result.contents)); + workspaceEdit.set(editor.document.uri, edits); + await vscode.workspace.applyEdit(workspaceEdit); + + // Set the cursor position after @brief + const newPosition: vscode.Position = new vscode.Position(result.finalCursorPosition.line, result.finalCursorPosition.character); + const newSelection: vscode.Selection = new vscode.Selection(newPosition, newPosition); + editor.selection = newSelection; + } + } + + private doneInitialCustomBrowseConfigurationCheck: boolean = false; + + private async onConfigurationsChanged(cppProperties: configs.CppProperties): Promise { + if (!cppProperties.Configurations) { + return; + } + const configurations: configs.Configuration[] = cppProperties.Configurations; + const params: CppPropertiesParams = { + configurations: [], + currentConfiguration: this.configuration.CurrentConfigurationIndex, + workspaceFolderUri: this.RootUri?.toString(), + isReady: true + }; + const settings: CppSettings = new CppSettings(this.RootUri); + // Clone each entry, as we make modifications before sending it, and don't + // want to add those modifications to the original objects. + configurations.forEach((c) => { + const modifiedConfig: configs.Configuration = deepCopy(c); + // Separate compiler path and args before sending to language client + const compilerPathAndArgs: util.CompilerPathAndArgs = + util.extractCompilerPathAndArgs(!!settings.legacyCompilerArgsBehavior, c.compilerPath, c.compilerArgs); + modifiedConfig.compilerPath = compilerPathAndArgs.compilerPath; + if (settings.legacyCompilerArgsBehavior) { + modifiedConfig.compilerArgsLegacy = compilerPathAndArgs.allCompilerArgs; + modifiedConfig.compilerArgs = undefined; + } else { + modifiedConfig.compilerArgs = compilerPathAndArgs.allCompilerArgs; + } + + params.configurations.push(modifiedConfig); + }); + + await this.languageClient.sendRequest(ChangeCppPropertiesRequest, params); + if (!!this.lastCustomBrowseConfigurationProviderId && !!this.lastCustomBrowseConfiguration && !!this.lastCustomBrowseConfigurationProviderVersion) { + if (!this.doneInitialCustomBrowseConfigurationCheck) { + // Send the last custom browse configuration we received from this provider. + // This ensures we don't start tag parsing without it, and undo'ing work we have to re-do when the (likely same) browse config arrives + // Should only execute on launch, for the initial delivery of configurations + if (this.lastCustomBrowseConfiguration.Value) { + this.sendCustomBrowseConfiguration(this.lastCustomBrowseConfiguration.Value, this.lastCustomBrowseConfigurationProviderId.Value, this.lastCustomBrowseConfigurationProviderVersion.Value); + params.isReady = false; + } + this.doneInitialCustomBrowseConfigurationCheck = true; + } + } + const configName: string | undefined = configurations[params.currentConfiguration].name ?? ""; + this.model.activeConfigName.setValueIfActive(configName); + const newProvider: string | undefined = this.configuration.CurrentConfigurationProvider; + if (!isSameProviderExtensionId(newProvider, this.configurationProvider)) { + if (this.configurationProvider) { + void this.clearCustomBrowseConfiguration().catch(logAndReturn.undefined); + } + this.configurationProvider = newProvider; + void this.updateCustomBrowseConfiguration().catch(logAndReturn.undefined); + void this.updateCustomConfigurations().catch(logAndReturn.undefined); + } + } + + private async onSelectedConfigurationChanged(index: number): Promise { + const params: FolderSelectedSettingParams = { + currentConfiguration: index, + workspaceFolderUri: this.RootUri?.toString() + }; + await this.ready; + await this.languageClient.sendNotification(ChangeSelectedSettingNotification, params); + + let configName: string = ""; + if (this.configuration.ConfigurationNames) { + configName = this.configuration.ConfigurationNames[index]; + } + this.model.activeConfigName.Value = configName; + this.configuration.onDidChangeSettings(); + } + + private async onCompileCommandsChanged(path: string): Promise { + const params: FileChangedParams = { + uri: vscode.Uri.file(path).toString(), + workspaceFolderUri: this.RootUri?.toString() + }; + await this.ready; + return this.languageClient.sendNotification(ChangeCompileCommandsNotification, params); + } + + private isSourceFileConfigurationItem(input: any, providerVersion: Version): input is SourceFileConfigurationItem { + // IntelliSenseMode and standard are optional for version 5+. + let areOptionalsValid: boolean = false; + if (providerVersion < Version.v5) { + areOptionalsValid = util.isString(input.configuration.intelliSenseMode) && util.isString(input.configuration.standard); + } else { + areOptionalsValid = util.isOptionalString(input.configuration.intelliSenseMode) && util.isOptionalString(input.configuration.standard); + } + return input && (util.isString(input.uri) || util.isUri(input.uri)) && + input.configuration && + areOptionalsValid && + util.isArrayOfString(input.configuration.includePath) && + util.isArrayOfString(input.configuration.defines) && + util.isOptionalArrayOfString(input.configuration.compilerArgs) && + util.isOptionalArrayOfString(input.configuration.forcedInclude); + } + + private sendCustomConfigurations(configs: any, providerVersion: Version): void { + // configs is marked as 'any' because it is untrusted data coming from a 3rd-party. We need to sanitize it before sending it to the language server. + if (!configs || !(configs instanceof Array)) { + console.warn("discarding invalid SourceFileConfigurationItems[]: " + configs); + return; + } + + const out: Logger = getOutputChannelLogger(); + out.appendLine(6, localize("configurations.received", "Custom configurations received:")); + const sanitized: SourceFileConfigurationItemAdapter[] = []; + configs.forEach(item => { + if (this.isSourceFileConfigurationItem(item, providerVersion)) { + let uri: string; + if (util.isString(item.uri) && !item.uri.startsWith("file://")) { + // If the uri field is a string, it may actually contain an fsPath. + uri = vscode.Uri.file(item.uri).toString(); + } else { + uri = item.uri.toString(); + } + this.configurationLogging.set(uri, JSON.stringify(item.configuration, null, 4)); + out.appendLine(6, ` uri: ${uri}`); + out.appendLine(6, ` config: ${JSON.stringify(item.configuration, null, 2)}`); + if (item.configuration.includePath.some(path => path.endsWith('**'))) { + console.warn("custom include paths should not use recursive includes ('**')"); + } + // Separate compiler path and args before sending to language client + const itemConfig: util.Mutable = deepCopy(item.configuration); + if (util.isString(itemConfig.compilerPath)) { + const compilerPathAndArgs: util.CompilerPathAndArgs = util.extractCompilerPathAndArgs( + providerVersion < Version.v6, + itemConfig.compilerPath, + util.isArrayOfString(itemConfig.compilerArgs) ? itemConfig.compilerArgs : undefined); + itemConfig.compilerPath = compilerPathAndArgs.compilerPath ?? undefined; + if (itemConfig.compilerPath !== undefined) { + void this.addTrustedCompiler(itemConfig.compilerPath).catch(logAndReturn.undefined); + } + if (providerVersion < Version.v6) { + itemConfig.compilerArgsLegacy = compilerPathAndArgs.allCompilerArgs; + itemConfig.compilerArgs = undefined; + } else { + itemConfig.compilerArgs = compilerPathAndArgs.allCompilerArgs; + } + } + sanitized.push({ + uri, + configuration: itemConfig + }); + } else { + console.warn("discarding invalid SourceFileConfigurationItem: " + JSON.stringify(item)); + } + }); + + if (sanitized.length === 0) { + return; + } + + const params: CustomConfigurationParams = { + configurationItems: sanitized, + workspaceFolderUri: this.RootUri?.toString() + }; + + // We send the higher priority notification to ensure we don't deadlock if the request is blocking the queue. + // We send the normal priority notification to avoid a race that could result in a redundant request when racing with + // the reset of custom configurations. + void this.languageClient.sendNotification(CustomConfigurationHighPriorityNotification, params).catch(logAndReturn.undefined); + void this.languageClient.sendNotification(CustomConfigurationNotification, params).catch(logAndReturn.undefined); + } + + private browseConfigurationLogging: string = ""; + private configurationLogging: Map = new Map(); + + private isWorkspaceBrowseConfiguration(input: any): boolean { + return util.isArrayOfString(input.browsePath) && + util.isOptionalString(input.compilerPath) && + util.isOptionalString(input.standard) && + util.isOptionalArrayOfString(input.compilerArgs) && + util.isOptionalString(input.windowsSdkVersion); + } + + private sendCustomBrowseConfiguration(config: any, providerId: string | undefined, providerVersion: Version, timeoutOccured?: boolean): void { + const rootFolder: vscode.WorkspaceFolder | undefined = this.RootFolder; + if (!rootFolder + || !this.lastCustomBrowseConfiguration + || !this.lastCustomBrowseConfigurationProviderId) { + return; + } + + let sanitized: util.Mutable; + + this.browseConfigurationLogging = ""; + + // This while (true) is here just so we can break out early if the config is set on error + // eslint-disable-next-line no-constant-condition + while (true) { + // config is marked as 'any' because it is untrusted data coming from a 3rd-party. We need to sanitize it before sending it to the language server. + if (timeoutOccured || !config || config instanceof Array) { + if (!timeoutOccured) { + console.log("Received an invalid browse configuration from configuration provider."); + } + const configValue: WorkspaceBrowseConfiguration | undefined = this.lastCustomBrowseConfiguration.Value; + if (configValue) { + sanitized = configValue; + if (sanitized.browsePath.length === 0) { + sanitized.browsePath = ["${workspaceFolder}/**"]; + } + break; + } + return; + } + + const browseConfig: InternalWorkspaceBrowseConfiguration = config as InternalWorkspaceBrowseConfiguration; + sanitized = deepCopy(browseConfig); + if (!this.isWorkspaceBrowseConfiguration(sanitized) || sanitized.browsePath.length === 0) { + console.log("Received an invalid browse configuration from configuration provider: " + JSON.stringify(sanitized)); + const configValue: WorkspaceBrowseConfiguration | undefined = this.lastCustomBrowseConfiguration.Value; + if (configValue) { + sanitized = configValue; + if (sanitized.browsePath.length === 0) { + sanitized.browsePath = ["${workspaceFolder}/**"]; + } + break; + } + return; + } + + getOutputChannelLogger().appendLine(6, localize("browse.configuration.received", "Custom browse configuration received: {0}", JSON.stringify(sanitized, null, 2))); + + // Separate compiler path and args before sending to language client + if (util.isString(sanitized.compilerPath)) { + const compilerPathAndArgs: util.CompilerPathAndArgs = util.extractCompilerPathAndArgs( + providerVersion < Version.v6, + sanitized.compilerPath, + util.isArrayOfString(sanitized.compilerArgs) ? sanitized.compilerArgs : undefined); + sanitized.compilerPath = compilerPathAndArgs.compilerPath ?? undefined; + if (sanitized.compilerPath !== undefined) { + void this.addTrustedCompiler(sanitized.compilerPath).catch(logAndReturn.undefined); + } + if (providerVersion < Version.v6) { + sanitized.compilerArgsLegacy = compilerPathAndArgs.allCompilerArgs; + sanitized.compilerArgs = undefined; + } else { + sanitized.compilerArgs = compilerPathAndArgs.allCompilerArgs; + } + } + + this.lastCustomBrowseConfiguration.Value = sanitized; + if (!providerId) { + this.lastCustomBrowseConfigurationProviderId.setDefault(); + } else { + this.lastCustomBrowseConfigurationProviderId.Value = providerId; + } + break; + } + + this.browseConfigurationLogging = `Custom browse configuration: \n${JSON.stringify(sanitized, null, 4)}\n`; + + const params: CustomBrowseConfigurationParams = { + browseConfiguration: sanitized, + workspaceFolderUri: this.RootUri?.toString() + }; + + void this.languageClient.sendNotification(CustomBrowseConfigurationNotification, params).catch(logAndReturn.undefined); + } + + private async clearCustomConfigurations(): Promise { + this.configurationLogging.clear(); + const params: WorkspaceFolderParams = { + workspaceFolderUri: this.RootUri?.toString() + }; + await this.ready; + return this.languageClient.sendNotification(ClearCustomConfigurationsNotification, params); + } + + private async clearCustomBrowseConfiguration(): Promise { + this.browseConfigurationLogging = ""; + const params: WorkspaceFolderParams = { + workspaceFolderUri: this.RootUri?.toString() + }; + await this.ready; + return this.languageClient.sendNotification(ClearCustomBrowseConfigurationNotification, params); + } + + /** + * command handlers + */ + public async handleConfigurationSelectCommand(): Promise { + await this.ready; + const configNames: string[] | undefined = this.configuration.ConfigurationNames; + if (configNames) { + const index: number = await ui.showConfigurations(configNames); + if (index < 0) { + return; + } + this.configuration.select(index); + } + } + + public async handleConfigurationProviderSelectCommand(): Promise { + await this.ready; + const extensionId: string | undefined = await ui.showConfigurationProviders(this.configuration.CurrentConfigurationProvider); + if (extensionId === undefined) { + // operation was canceled. + return; + } + await this.configuration.updateCustomConfigurationProvider(extensionId); + if (extensionId) { + const provider: CustomConfigurationProvider1 | undefined = getCustomConfigProviders().get(extensionId); + void this.updateCustomBrowseConfiguration(provider).catch(logAndReturn.undefined); + void this.updateCustomConfigurations(provider).catch(logAndReturn.undefined); + telemetry.logLanguageServerEvent("customConfigurationProvider", { "providerId": extensionId }); + } else { + void this.clearCustomConfigurations().catch(logAndReturn.undefined); + void this.clearCustomBrowseConfiguration().catch(logAndReturn.undefined); + } + } + + public async handleShowActiveCodeAnalysisCommands(): Promise { + await this.ready; + const index: number = await ui.showActiveCodeAnalysisCommands(); + switch (index) { + case 0: return this.CancelCodeAnalysis(); + case 1: return this.PauseCodeAnalysis(); + case 2: return this.ResumeCodeAnalysis(); + case 3: return this.handleShowIdleCodeAnalysisCommands(); + } + } + + public async handleShowIdleCodeAnalysisCommands(): Promise { + await this.ready; + const index: number = await ui.showIdleCodeAnalysisCommands(); + switch (index) { + case 0: return this.handleRunCodeAnalysisOnActiveFile(); + case 1: return this.handleRunCodeAnalysisOnAllFiles(); + case 2: return this.handleRunCodeAnalysisOnOpenFiles(); + } + } + + public async handleConfigurationEditCommand(viewColumn: vscode.ViewColumn = vscode.ViewColumn.Active): Promise { + await this.ready; + return this.configuration.handleConfigurationEditCommand(undefined, vscode.window.showTextDocument, viewColumn); + } + + public async handleConfigurationEditJSONCommand(viewColumn: vscode.ViewColumn = vscode.ViewColumn.Active): Promise { + await this.ready; + return this.configuration.handleConfigurationEditJSONCommand(undefined, vscode.window.showTextDocument, viewColumn); + } + + public async handleConfigurationEditUICommand(viewColumn: vscode.ViewColumn = vscode.ViewColumn.Active): Promise { + await this.ready; + return this.configuration.handleConfigurationEditUICommand(undefined, vscode.window.showTextDocument, viewColumn); + } + + public async handleAddToIncludePathCommand(path: string): Promise { + await this.ready; + return this.configuration.addToIncludePathCommand(path); + } + + public async handleGoToDirectiveInGroup(next: boolean): Promise { + const editor: vscode.TextEditor | undefined = vscode.window.activeTextEditor; + if (editor) { + const params: GoToDirectiveInGroupParams = { + uri: editor.document.uri.toString(), + position: editor.selection.active, + next: next + }; + await this.ready; + const response: Position | undefined = await this.languageClient.sendRequest(GoToDirectiveInGroupRequest, params); + if (response) { + const p: vscode.Position = new vscode.Position(response.line, response.character); + const r: vscode.Range = new vscode.Range(p, p); + + // Check if still the active document. + const currentEditor: vscode.TextEditor | undefined = vscode.window.activeTextEditor; + if (currentEditor && editor.document.uri === currentEditor.document.uri) { + currentEditor.selection = new vscode.Selection(r.start, r.end); + currentEditor.revealRange(r); + } + } + } + } + + public async handleGenerateDoxygenComment(args: DoxygenCodeActionCommandArguments | vscode.Uri | undefined): Promise { + const editor: vscode.TextEditor | undefined = vscode.window.activeTextEditor; + if (!editor || !util.isCpp(editor.document)) { + return; + } + + let codeActionArguments: DoxygenCodeActionCommandArguments | undefined; + if (args !== undefined && !(args instanceof vscode.Uri)) { + codeActionArguments = args; + } + const initCursorPosition: vscode.Position = (codeActionArguments !== undefined) ? new vscode.Position(codeActionArguments.initialCursor.line, codeActionArguments.initialCursor.character) : editor.selection.start; + const params: GenerateDoxygenCommentParams = { + uri: editor.document.uri.toString(), + position: (codeActionArguments !== undefined) ? new vscode.Position(codeActionArguments.adjustedCursor.line, codeActionArguments.adjustedCursor.character) : editor.selection.start, + isCodeAction: codeActionArguments !== undefined, + isCursorAboveSignatureLine: codeActionArguments?.isCursorAboveSignatureLine + }; + await this.ready; + const currentFileVersion: number | undefined = openFileVersions.get(params.uri); + if (currentFileVersion === undefined) { + return; + } + const result: GenerateDoxygenCommentResult | undefined = await this.languageClient.sendRequest(GenerateDoxygenCommentRequest, params); + // Insert the comment only if the comment has contents and the cursor has not moved + if (result !== undefined && + initCursorPosition.line === editor.selection.start.line && + initCursorPosition.character === editor.selection.start.character && + result.fileVersion !== undefined && + result.fileVersion === currentFileVersion && + result.contents && result.contents.length > 1) { + const workspaceEdit: vscode.WorkspaceEdit = new vscode.WorkspaceEdit(); + const edits: vscode.TextEdit[] = []; + const maxColumn: number = 99999999; + let newRange: vscode.Range; + const cursorOnEmptyLineAboveSignature: boolean = result.isCursorAboveSignatureLine; + // The reason why we need to set different range is because if cursor is immediately above the signature line, we want the comments to be inserted at the line of cursor and to replace everything on the line. + // If the cursor is on the signature line or is inside the boby, the comment will be inserted on the same line of the signature and it shouldn't replace the content of the signature line. + if (cursorOnEmptyLineAboveSignature) { + if (codeActionArguments !== undefined) { + // The reason why we cannot use finalInsertionLine is because the line number sent from the result is not correct. + // In most cases, the finalInsertionLine is the line of the signature line. + newRange = new vscode.Range(initCursorPosition.line, 0, initCursorPosition.line, maxColumn); + } else { + newRange = new vscode.Range(result.finalInsertionLine, 0, result.finalInsertionLine, maxColumn); + } + } else { + newRange = new vscode.Range(result.finalInsertionLine, 0, result.finalInsertionLine, 0); + } + edits.push(new vscode.TextEdit(newRange, result.contents)); + workspaceEdit.set(editor.document.uri, edits); + await vscode.workspace.applyEdit(workspaceEdit); + // Set the cursor position after @brief + let newPosition: vscode.Position; + if (cursorOnEmptyLineAboveSignature && codeActionArguments !== undefined) { + newPosition = new vscode.Position(result.finalCursorPosition.line - 1, result.finalCursorPosition.character); + } else { + newPosition = new vscode.Position(result.finalCursorPosition.line, result.finalCursorPosition.character); + } + const newSelection: vscode.Selection = new vscode.Selection(newPosition, newPosition); + editor.selection = newSelection; + } + } + + public async handleRunCodeAnalysisOnActiveFile(): Promise { + await this.ready; + return this.languageClient.sendNotification(CodeAnalysisNotification, { scope: CodeAnalysisScope.ActiveFile }); + } + + public async handleRunCodeAnalysisOnOpenFiles(): Promise { + await this.ready; + return this.languageClient.sendNotification(CodeAnalysisNotification, { scope: CodeAnalysisScope.OpenFiles }); + } + + public async handleRunCodeAnalysisOnAllFiles(): Promise { + await this.ready; + return this.languageClient.sendNotification(CodeAnalysisNotification, { scope: CodeAnalysisScope.AllFiles }); + } + + public async handleRemoveAllCodeAnalysisProblems(): Promise { + await this.ready; + if (removeAllCodeAnalysisProblems()) { + return this.languageClient.sendNotification(CodeAnalysisNotification, { scope: CodeAnalysisScope.ClearSquiggles }); + } + } + + public async handleFixCodeAnalysisProblems(workspaceEdit: vscode.WorkspaceEdit, refreshSquigglesOnSave: boolean, identifiersAndUris: CodeAnalysisDiagnosticIdentifiersAndUri[]): Promise { + if (await vscode.workspace.applyEdit(workspaceEdit)) { + const settings: CppSettings = new CppSettings(this.RootUri); + if (settings.clangTidyCodeActionFormatFixes) { + const editedFiles: Set = new Set(); + for (const entry of workspaceEdit.entries()) { + editedFiles.add(entry[0]); + } + const formatEdits: vscode.WorkspaceEdit = new vscode.WorkspaceEdit(); + for (const uri of editedFiles) { + const formatTextEdits: vscode.TextEdit[] | undefined = await vscode.commands.executeCommand( + "vscode.executeFormatDocumentProvider", uri, { onChanges: true, preserveFocus: false }); + if (formatTextEdits && formatTextEdits.length > 0) { + formatEdits.set(uri, formatTextEdits); + } + } + if (formatEdits.size > 0) { + await vscode.workspace.applyEdit(formatEdits); + } + } + return this.handleRemoveCodeAnalysisProblems(refreshSquigglesOnSave, identifiersAndUris); + } + } + + public async handleRemoveCodeAnalysisProblems(refreshSquigglesOnSave: boolean, identifiersAndUris: CodeAnalysisDiagnosticIdentifiersAndUri[]): Promise { + await this.ready; + + // A deep copy is needed because the call to identifiers.splice below can + // remove elements in identifiersAndUris[...].identifiers. + const identifiersAndUrisCopy: CodeAnalysisDiagnosticIdentifiersAndUri[] = []; + for (const identifiersAndUri of identifiersAndUris) { + identifiersAndUrisCopy.push({ uri: identifiersAndUri.uri, identifiers: [...identifiersAndUri.identifiers] }); + } + + if (removeCodeAnalysisProblems(identifiersAndUris)) { + // Need to notify the language client of the removed diagnostics so it doesn't re-send them. + return this.languageClient.sendNotification(RemoveCodeAnalysisProblemsNotification, { + identifiersAndUris: identifiersAndUrisCopy, refreshSquigglesOnSave: refreshSquigglesOnSave + }); + } + } + + public async handleDisableAllTypeCodeAnalysisProblems(code: string, + identifiersAndUris: CodeAnalysisDiagnosticIdentifiersAndUri[]): Promise { + const settings: CppSettings = new CppSettings(this.RootUri); + const codes: string[] = code.split(','); + for (const code of codes) { + settings.addClangTidyChecksDisabled(code); + } + return this.handleRemoveCodeAnalysisProblems(false, identifiersAndUris); + } + + public async handleCreateDeclarationOrDefinition(isCopyToClipboard: boolean, codeActionRange?: Range): Promise { + let range: vscode.Range | undefined; + let uri: vscode.Uri | undefined; + const editor: vscode.TextEditor | undefined = vscode.window.activeTextEditor; + + const editorSettings: OtherSettings = new OtherSettings(uri); + const cppSettings: CppSettings = new CppSettings(uri); + + if (editor) { + uri = editor.document.uri; + if (codeActionRange !== undefined) { + // Request is from a code action command which provides range from code actions args. + range = makeVscodeRange(codeActionRange); + } else { + // Request is from context menu or command palette. Use range from cursor position. + if (editor.selection.isEmpty) { + range = new vscode.Range(editor.selection.active, editor.selection.active); + } else if (editor.selection.isReversed) { + range = new vscode.Range(editor.selection.active, editor.selection.anchor); + } else { + range = new vscode.Range(editor.selection.anchor, editor.selection.active); + } + } + } + + if (uri === undefined || range === undefined || editor === undefined) { + return; + } + + let formatParams: FormatParams | undefined; + if (cppSettings.useVcFormat(editor.document)) { + const editorConfigSettings: any = getEditorConfigSettings(uri.fsPath); + formatParams = { + editorConfigSettings: editorConfigSettings, + useVcFormat: true, + insertSpaces: editorConfigSettings.indent_style !== undefined ? editorConfigSettings.indent_style === "space" ? true : false : true, + tabSize: editorConfigSettings.tab_width !== undefined ? editorConfigSettings.tab_width : 4, + character: "", + range: { + start: { + character: 0, + line: 0 + }, + end: { + character: 0, + line: 0 + } + }, + onChanges: false, + uri: '' + }; + } else { + formatParams = { + editorConfigSettings: {}, + useVcFormat: false, + insertSpaces: editorSettings.editorInsertSpaces !== undefined ? editorSettings.editorInsertSpaces : true, + tabSize: editorSettings.editorTabSize !== undefined ? editorSettings.editorTabSize : 4, + character: "", + range: { + start: { + character: 0, + line: 0 + }, + end: { + character: 0, + line: 0 + } + }, + onChanges: false, + uri: '' + }; + } + + const params: CreateDeclarationOrDefinitionParams = { + uri: uri.toString(), + range: { + start: { + character: range.start.character, + line: range.start.line + }, + end: { + character: range.end.character, + line: range.end.line + } + }, + formatParams: formatParams, + copyToClipboard: isCopyToClipboard + }; + + const result: CreateDeclarationOrDefinitionResult = await this.languageClient.sendRequest(CreateDeclarationOrDefinitionRequest, params); + // Create/Copy returned no result. + if (result.workspaceEdits === undefined) { + // The only condition in which result.edit would be undefined is a + // server-initiated cancellation, in which case the object is actually + // a ResponseError. https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#responseMessage + return; + } + + // Handle CDD error messaging + if (result.errorText) { + let copiedToClipboard: boolean = false; + if (result.clipboardText && !params.copyToClipboard) { + await vscode.env.clipboard.writeText(result.clipboardText); + copiedToClipboard = true; + } + void vscode.window.showInformationMessage(result.errorText + (copiedToClipboard ? localize("fallback.clipboard", " Declaration/definition was copied.") : "")); + return; + } + + if (result.clipboardText && params.copyToClipboard) { + return vscode.env.clipboard.writeText(result.clipboardText); + } + + let workspaceEdits: vscode.WorkspaceEdit = new vscode.WorkspaceEdit(); + let modifiedDocument: vscode.Uri | undefined; + let lastEdit: vscode.TextEdit | undefined; + for (const workspaceEdit of result.workspaceEdits) { + const uri: vscode.Uri = vscode.Uri.file(workspaceEdit.file); + // At most, there will only be two text edits: + // 1.) an edit for: #include header file + // 2.) an edit for: definition or declaration + for (const edit of workspaceEdit.edits) { + let range: vscode.Range = makeVscodeRange(edit.range); + // Get new lines from an edit for: #include header file. + if (lastEdit && lastEdit.newText.length < 300 && lastEdit.newText.includes("#include") && lastEdit.range.isEqual(range)) { + // Destination file is empty. + // The edit positions for #include header file and definition or declaration are the same. + const selectionPositionAdjustment = (lastEdit.newText.match(/\n/g) ?? []).length; + range = new vscode.Range(new vscode.Position(range.start.line + selectionPositionAdjustment, range.start.character), + new vscode.Position(range.end.line + selectionPositionAdjustment, range.end.character)); + } + lastEdit = new vscode.TextEdit(range, edit.newText); + workspaceEdits.insert(uri, range.start, edit.newText); + if (edit.newText.length < 300 && edit.newText.includes("#pragma once")) { + // Commit this so that it can be undone separately, to avoid leaving an empty file, + // which causes the next refactor to not add the #pragma once. + await vscode.workspace.applyEdit(workspaceEdits); + workspaceEdits = new vscode.WorkspaceEdit(); + } + } + modifiedDocument = uri; + } + + if (modifiedDocument === undefined || lastEdit === undefined) { + return; + } + + // Apply the create declaration/definition text edits. + await vscode.workspace.applyEdit(workspaceEdits); + + // Move the cursor to the new declaration/definition edit, accounting for \n or \n\n at the start. + let startLine: number = lastEdit.range.start.line; + let numNewlines: number = (lastEdit.newText.match(/\n/g) ?? []).length; + if (lastEdit.newText.startsWith("\r\n\r\n") || lastEdit.newText.startsWith("\n\n")) { + startLine += 2; + numNewlines -= 2; + } else if (lastEdit.newText.startsWith("\r\n") || lastEdit.newText.startsWith("\n")) { + startLine += 1; + numNewlines -= 1; + } + if (!lastEdit.newText.endsWith("\n")) { + numNewlines++; // Increase the format range. + } + + const selectionPosition: vscode.Position = new vscode.Position(startLine, 0); + const selectionRange: vscode.Range = new vscode.Range(selectionPosition, selectionPosition); + await vscode.window.showTextDocument(modifiedDocument, { selection: selectionRange }); + + // Format the new text edits. + const formatEdits: vscode.WorkspaceEdit = new vscode.WorkspaceEdit(); + const formatRange: vscode.Range = new vscode.Range(selectionRange.start, new vscode.Position(selectionRange.start.line + numNewlines, 0)); + const settings: OtherSettings = new OtherSettings(vscode.workspace.getWorkspaceFolder(modifiedDocument)?.uri); + const formatOptions: vscode.FormattingOptions = { + insertSpaces: settings.editorInsertSpaces ?? true, + tabSize: settings.editorTabSize ?? 4 + }; + const versionBeforeFormatting: number | undefined = openFileVersions.get(modifiedDocument.toString()); + if (versionBeforeFormatting === undefined) { + return; + } + const formatTextEdits: vscode.TextEdit[] | undefined = await vscode.commands.executeCommand("vscode.executeFormatRangeProvider", modifiedDocument, formatRange, formatOptions); + if (formatTextEdits && formatTextEdits.length > 0) { + formatEdits.set(modifiedDocument, formatTextEdits); + } + if (formatEdits.size === 0 || versionBeforeFormatting === undefined) { + return; + } + // Only apply formatting if the document version hasn't changed to prevent + // stale formatting results from being applied. + const versionAfterFormatting: number | undefined = openFileVersions.get(modifiedDocument.toString()); + if (versionAfterFormatting === undefined || versionAfterFormatting > versionBeforeFormatting) { + return; + } + await vscode.workspace.applyEdit(formatEdits); + } + + public async handleExtractToFunction(extractAsGlobal: boolean): Promise { + const editor: vscode.TextEditor | undefined = vscode.window.activeTextEditor; + if (!editor || editor.selection.isEmpty) { + return; + } + + let functionName: string | undefined = await vscode.window.showInputBox({ + title: localize('handle.extract.name', 'Name the extracted function'), + placeHolder: localize('handle.extract.new.function', 'NewFunction') + }); + + if (functionName === undefined || functionName === "") { + functionName = "NewFunction"; + } + + const params: ExtractToFunctionParams = { + uri: editor.document.uri.toString(), + range: { + start: { + character: editor.selection.start.character, + line: editor.selection.start.line + }, + end: { + character: editor.selection.end.character, + line: editor.selection.end.line + } + }, + extractAsGlobal, + name: functionName + }; + + const result: WorkspaceEditResult = await this.languageClient.sendRequest(ExtractToFunctionRequest, params); + if (result.workspaceEdits === undefined) { + // The only condition in which result.edit would be undefined is a + // server-initiated cancellation, in which case the object is actually + // a ResponseError. https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#responseMessage + return; + } + + // Handle error messaging + if (result.errorText) { + void vscode.window.showErrorMessage(`${localize("handle.extract.error", + "Extract to function failed: {0}", result.errorText)}`); + return; + } + + let workspaceEdits: vscode.WorkspaceEdit = new vscode.WorkspaceEdit(); + + // NOTE: References to source/header are in reference to the more common case when it's + // invoked on the source file (alternatively named the first file). When invoked on the header file, + // the header file operates as if it were the source file (isSourceFile stays true). + let sourceReplaceEditRange: vscode.Range | undefined; + let headerReplaceEditRange: vscode.Range | undefined; + let hasProcessedReplace: boolean = false; + const sourceFormatUriAndRanges: VsCodeUriAndRange[] = []; + const headerFormatUriAndRanges: VsCodeUriAndRange[] = []; + let lineOffset: number = 0; + let headerFileLineOffset: number = 0; + let isSourceFile: boolean = true; + // There will be 4-5 text edits: + // - A #pragma once added to a new header file (optional) + // - Add #include for header file (optional) + // - Add the new function declaration (in the source or header file) + // - Replace the selected code with the new function call, + // plus possibly extra declarations beforehand, + // plus possibly extra return value handling afterwards. + // - Add the new function definition (below the selection) + for (const workspaceEdit of result.workspaceEdits) { + if (hasProcessedReplace) { + isSourceFile = false; + lineOffset = 0; + } + const uri: vscode.Uri = vscode.Uri.file(workspaceEdit.file); + let nextLineOffset: number = 0; + for (const edit of workspaceEdit.edits) { + let range: vscode.Range = makeVscodeRange(edit.range); + if (!isSourceFile && headerFileLineOffset) { + range = new vscode.Range(new vscode.Position(range.start.line + headerFileLineOffset, range.start.character), + new vscode.Position(range.end.line + headerFileLineOffset, range.end.character)); + } + const isReplace: boolean = !range.isEmpty && isSourceFile; + lineOffset += nextLineOffset; + nextLineOffset = (edit.newText.match(/\n/g) ?? []).length; + + // Find the editType. + if (isReplace) { + hasProcessedReplace = true; + workspaceEdits.replace(uri, range, edit.newText); + } else { + workspaceEdits.insert(uri, range.start, edit.newText); + if (edit.newText.length < 300) { // Avoid searching large code edits + if (isSourceFile && !hasProcessedReplace && edit.newText.includes("#include")) { + continue; + } + if (edit.newText.includes("#pragma once")) { + // Commit this so that it can be undone separately, to avoid leaving an empty file, + // which causes the next refactor to not add the #pragma once. + await vscode.workspace.applyEdit(workspaceEdits, { isRefactoring: true }); + headerFileLineOffset = nextLineOffset; + workspaceEdits = new vscode.WorkspaceEdit(); + continue; + } + } + } + const formatRangeStartLine: number = range.start.line + lineOffset; + let rangeStartLine: number = formatRangeStartLine; + let rangeStartCharacter: number = 0; + let startWithNewLine: boolean = true; + if (edit.newText.startsWith("\r\n\r\n")) { + rangeStartCharacter = 4; + rangeStartLine += 2; + } else if (edit.newText.startsWith("\n\n")) { + rangeStartCharacter = 2; + rangeStartLine += 2; + } else if (edit.newText.startsWith("\r\n")) { + rangeStartCharacter = 2; + rangeStartLine += 1; + } else if (edit.newText.startsWith("\n")) { + rangeStartCharacter = 1; + rangeStartLine += 1; + } else { + startWithNewLine = false; + } + const newFormatRange: vscode.Range = new vscode.Range( + new vscode.Position(formatRangeStartLine + (nextLineOffset < 0 ? nextLineOffset : 0), range.start.character), + new vscode.Position(formatRangeStartLine + (nextLineOffset < 0 ? 0 : nextLineOffset), + isReplace ? range.end.character : + ((startWithNewLine ? 0 : range.end.character) + (edit.newText.endsWith("\n") ? 0 : edit.newText.length - rangeStartCharacter)) + ) + ); + if (isSourceFile) { + sourceFormatUriAndRanges.push({ uri, range: newFormatRange }); + } else { + headerFormatUriAndRanges.push({ uri, range: newFormatRange }); + } + if (isReplace || !isSourceFile) { + // Handle additional declaration lines added before the new function call. + let currentText: string = edit.newText.substring(rangeStartCharacter); + let currentTextNextLineStart: number = currentText.indexOf("\n"); + let currentTextNewFunctionStart: number = currentText.indexOf(functionName); + let currentTextNextLineStartUpdated: boolean = false; + while (currentTextNextLineStart !== -1 && currentTextNextLineStart < currentTextNewFunctionStart) { + ++rangeStartLine; + currentText = currentText.substring(currentTextNextLineStart + 1); + currentTextNextLineStart = currentText.indexOf("\n"); + currentTextNewFunctionStart = currentText.indexOf(functionName); + currentTextNextLineStartUpdated = true; + } + rangeStartCharacter = (rangeStartCharacter === 0 && !currentTextNextLineStartUpdated ? range.start.character : 0) + + currentTextNewFunctionStart; + if (rangeStartCharacter < 0) { + // functionName is missing -- unexpected error. + void vscode.window.showErrorMessage(`${localize("invalid.edit", + "Extract to function failed. An invalid edit was generated: '{0}'", edit.newText)}`); + continue; + } + const replaceEditRange = new vscode.Range( + new vscode.Position(rangeStartLine, rangeStartCharacter), + new vscode.Position(rangeStartLine, rangeStartCharacter + functionName.length)); + if (isSourceFile) { + sourceReplaceEditRange = replaceEditRange; + } else { + headerReplaceEditRange = replaceEditRange; + } + nextLineOffset -= range.end.line - range.start.line; + } + } + } + + if (sourceReplaceEditRange === undefined || sourceFormatUriAndRanges.length === 0) { + return; + } + + // Apply the extract to function text edits. + await vscode.workspace.applyEdit(workspaceEdits, { isRefactoring: true }); + + if (headerFormatUriAndRanges.length > 0 && headerReplaceEditRange !== undefined) { + // The header needs to be open and shown or the formatting will fail + // (due to issues/requirements in the cpptools process). + // It also seems strange and undesirable to have the header modified + // without being opened because otherwise users may not realize that + // the header had changed (unless they view source control differences). + await vscode.window.showTextDocument(headerFormatUriAndRanges[0].uri, { + selection: headerReplaceEditRange, preserveFocus: false + }); + } + + // Format the new text edits. + let formatEdits: vscode.WorkspaceEdit = new vscode.WorkspaceEdit(); + const formatRanges = async (formatUriAndRanges: VsCodeUriAndRange[]) => { + if (formatUriAndRanges.length === 0) { + return; + } + const formatUriAndRange: VsCodeUriAndRange = formatUriAndRanges[0]; + const isMultipleFormatRanges: boolean = formatUriAndRanges.length > 1; + const settings: OtherSettings = new OtherSettings(vscode.workspace.getWorkspaceFolder(formatUriAndRange.uri)?.uri); + const formatOptions: vscode.FormattingOptions = { + insertSpaces: settings.editorInsertSpaces ?? true, + tabSize: settings.editorTabSize ?? 4, + onChanges: isMultipleFormatRanges, + preserveFocus: true + }; + + const tryFormat = async () => { + const versionBeforeFormatting: number | undefined = openFileVersions.get(formatUriAndRange.uri.toString()); + if (versionBeforeFormatting === undefined) { + return true; + } + + // Only use document (onChange) formatting when there are multiple ranges. + const formatTextEdits: vscode.TextEdit[] | undefined = isMultipleFormatRanges ? + await vscode.commands.executeCommand( + "vscode.executeFormatDocumentProvider", formatUriAndRange.uri, formatOptions) : + await vscode.commands.executeCommand( + "vscode.executeFormatRangeProvider", formatUriAndRange.uri, formatUriAndRange.range, formatOptions); + + if (!formatTextEdits || formatTextEdits.length === 0 || versionBeforeFormatting === undefined) { + return true; + } + // Only apply formatting if the document version hasn't changed to prevent + // stale formatting results from being applied. + const versionAfterFormatting: number | undefined = openFileVersions.get(formatUriAndRange.uri.toString()); + if (versionAfterFormatting === undefined || versionAfterFormatting > versionBeforeFormatting) { + return false; + } + formatEdits.set(formatUriAndRange.uri, formatTextEdits); + return true; + }; + if (!await tryFormat()) { + await tryFormat(); // Try again; + } + }; + + if (headerFormatUriAndRanges.length > 0 && headerReplaceEditRange !== undefined) { + await formatRanges(headerFormatUriAndRanges); + if (formatEdits.size > 0) { + // This showTextDocument is required in order to get the selection to be + // correct after the formatting edit is applied. It could be a VS Code bug. + await vscode.window.showTextDocument(headerFormatUriAndRanges[0].uri, { + selection: headerReplaceEditRange, preserveFocus: false + }); + await vscode.workspace.applyEdit(formatEdits, { isRefactoring: true }); + formatEdits = new vscode.WorkspaceEdit(); + } + } + + // Select the replaced code. + await vscode.window.showTextDocument(sourceFormatUriAndRanges[0].uri, { + selection: sourceReplaceEditRange, preserveFocus: false + }); + + await formatRanges(sourceFormatUriAndRanges); + if (formatEdits.size > 0) { + await vscode.workspace.applyEdit(formatEdits, { isRefactoring: true }); + } + } + + public onInterval(): void { + // These events can be discarded until the language client is ready. + // Don't queue them up with this.notifyWhenLanguageClientReady calls. + if (this.innerLanguageClient !== undefined && this.configuration !== undefined) { + void this.languageClient.sendNotification(IntervalTimerNotification).catch(logAndReturn.undefined); + this.configuration.checkCppProperties(); + this.configuration.checkCompileCommands(); + } + } + + public dispose(): void { + this.disposables.forEach((d) => d.dispose()); + this.disposables = []; + if (this.documentFormattingProviderDisposable) { + this.documentFormattingProviderDisposable.dispose(); + this.documentFormattingProviderDisposable = undefined; + } + if (this.formattingRangeProviderDisposable) { + this.formattingRangeProviderDisposable.dispose(); + this.formattingRangeProviderDisposable = undefined; + } + if (this.onTypeFormattingProviderDisposable) { + this.onTypeFormattingProviderDisposable.dispose(); + this.onTypeFormattingProviderDisposable = undefined; + } + if (this.codeFoldingProviderDisposable) { + this.codeFoldingProviderDisposable.dispose(); + this.codeFoldingProviderDisposable = undefined; + } + if (this.semanticTokensProviderDisposable) { + this.semanticTokensProviderDisposable.dispose(); + this.semanticTokensProviderDisposable = undefined; + } + this.model.dispose(); + } + + public static stopLanguageClient(): Thenable { + return languageClient ? languageClient.stop() : Promise.resolve(); + } + + public async handleReferencesIcon(): Promise { + await this.ready; + + workspaceReferences.UpdateProgressUICounter(this.model.referencesCommandMode.Value); + + // If the search is find all references, preview partial results. + // This will cause the language server to send partial results to display + // in the "Other References" view or channel. Doing a preview should not complete + // an in-progress request until it is finished or canceled. + if (this.ReferencesCommandMode === refs.ReferencesCommandMode.Find) { + void this.languageClient.sendNotification(PreviewReferencesNotification); + } + + } + + private serverCanceledReferences(): void { + workspaceReferences.cancelCurrentReferenceRequest(refs.CancellationSender.LanguageServer); + } + + private handleReferencesProgress(notificationBody: refs.ReportReferencesProgressNotification): void { + workspaceReferences.handleProgress(notificationBody); + } + + private processReferencesPreview(referencesResult: refs.ReferencesResult): void { + workspaceReferences.showResultsInPanelView(referencesResult); + } + + public setReferencesCommandMode(mode: refs.ReferencesCommandMode): void { + this.model.referencesCommandMode.Value = mode; + } + + public async addTrustedCompiler(path: string): Promise { + if (path === null || path === undefined) { + return; + } + if (trustedCompilerPaths.includes(path)) { + DebugConfigurationProvider.ClearDetectedBuildTasks(); + return; + } + trustedCompilerPaths.push(path); + compilerDefaults = await this.requestCompiler(path); + DebugConfigurationProvider.ClearDetectedBuildTasks(); + } +} + +function getLanguageServerFileName(): string { + let extensionProcessName: string = 'cpptools'; + const plat: NodeJS.Platform = process.platform; + if (plat === 'win32') { + extensionProcessName += '.exe'; + } else if (plat !== 'linux' && plat !== 'darwin') { + throw "Invalid Platform"; + } + return path.resolve(util.getExtensionFilePath("bin"), extensionProcessName); +} + +/* eslint-disable @typescript-eslint/no-unused-vars */ +class NullClient implements Client { + private booleanEvent = new vscode.EventEmitter(); + private numberEvent = new vscode.EventEmitter(); + private stringEvent = new vscode.EventEmitter(); + private referencesCommandModeEvent = new vscode.EventEmitter(); + + readonly ready: Promise = Promise.resolve(); + + async enqueue(task: () => Promise) { + return task(); + } + public get InitializingWorkspaceChanged(): vscode.Event { return this.booleanEvent.event; } + public get IndexingWorkspaceChanged(): vscode.Event { return this.booleanEvent.event; } + public get ParsingWorkspaceChanged(): vscode.Event { return this.booleanEvent.event; } + public get ParsingWorkspacePausedChanged(): vscode.Event { return this.booleanEvent.event; } + public get ParsingFilesChanged(): vscode.Event { return this.booleanEvent.event; } + public get IntelliSenseParsingChanged(): vscode.Event { return this.booleanEvent.event; } + public get RunningCodeAnalysisChanged(): vscode.Event { return this.booleanEvent.event; } + public get CodeAnalysisPausedChanged(): vscode.Event { return this.booleanEvent.event; } + public get CodeAnalysisProcessedChanged(): vscode.Event { return this.numberEvent.event; } + public get CodeAnalysisTotalChanged(): vscode.Event { return this.numberEvent.event; } + public get ReferencesCommandModeChanged(): vscode.Event { return this.referencesCommandModeEvent.event; } + public get TagParserStatusChanged(): vscode.Event { return this.stringEvent.event; } + public get ActiveConfigChanged(): vscode.Event { return this.stringEvent.event; } + RootPath: string = "/"; + RootRealPath: string = "/"; + RootUri?: vscode.Uri = vscode.Uri.file("/"); + Name: string = "(empty)"; + TrackedDocuments = new Map(); + async onDidChangeSettings(event: vscode.ConfigurationChangeEvent): Promise> { return {}; } + onDidOpenTextDocument(document: vscode.TextDocument): void { } + onDidCloseTextDocument(document: vscode.TextDocument): void { } + onDidChangeVisibleTextEditors(editors: readonly vscode.TextEditor[]): Promise { return Promise.resolve(); } + onDidChangeTextEditorVisibleRanges(uri: vscode.Uri): Promise { return Promise.resolve(); } + onDidChangeTextDocument(textDocumentChangeEvent: vscode.TextDocumentChangeEvent): void { } + onRegisterCustomConfigurationProvider(provider: CustomConfigurationProvider1): Thenable { return Promise.resolve(); } + updateCustomConfigurations(requestingProvider?: CustomConfigurationProvider1): Thenable { return Promise.resolve(); } + updateCustomBrowseConfiguration(requestingProvider?: CustomConfigurationProvider1): Thenable { return Promise.resolve(); } + provideCustomConfiguration(docUri: vscode.Uri): Promise { return Promise.resolve(); } + logDiagnostics(): Promise { return Promise.resolve(); } + rescanFolder(): Promise { return Promise.resolve(); } + toggleReferenceResultsView(): void { } + setCurrentConfigName(configurationName: string): Thenable { return Promise.resolve(); } + getCurrentConfigName(): Thenable { return Promise.resolve(""); } + getCurrentConfigCustomVariable(variableName: string): Thenable { return Promise.resolve(""); } + getVcpkgInstalled(): Thenable { return Promise.resolve(false); } + getVcpkgEnabled(): Thenable { return Promise.resolve(false); } + getCurrentCompilerPathAndArgs(): Thenable { return Promise.resolve(undefined); } + 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(""); } + updateActiveDocumentTextOptions(): void { } + didChangeActiveEditor(editor?: vscode.TextEditor): Promise { return Promise.resolve(); } + restartIntelliSenseForFile(document: vscode.TextDocument): Promise { return Promise.resolve(); } + activate(): void { } + selectionChanged(selection: Range): void { } + resetDatabase(): void { } + promptSelectIntelliSenseConfiguration(sender?: any): Promise { return Promise.resolve(); } + rescanCompilers(sender?: any): Promise { return Promise.resolve(); } + deactivate(): void { } + pauseParsing(): void { } + resumeParsing(): void { } + PauseCodeAnalysis(): void { } + ResumeCodeAnalysis(): void { } + CancelCodeAnalysis(): void { } + handleConfigurationSelectCommand(): Promise { return Promise.resolve(); } + handleConfigurationProviderSelectCommand(): Promise { return Promise.resolve(); } + handleShowActiveCodeAnalysisCommands(): Promise { return Promise.resolve(); } + handleShowIdleCodeAnalysisCommands(): Promise { return Promise.resolve(); } + handleReferencesIcon(): void { } + handleConfigurationEditCommand(viewColumn?: vscode.ViewColumn): void { } + handleConfigurationEditJSONCommand(viewColumn?: vscode.ViewColumn): void { } + handleConfigurationEditUICommand(viewColumn?: vscode.ViewColumn): void { } + handleAddToIncludePathCommand(path: string): void { } + handleGoToDirectiveInGroup(next: boolean): Promise { return Promise.resolve(); } + handleGenerateDoxygenComment(args: DoxygenCodeActionCommandArguments | vscode.Uri | undefined): Promise { return Promise.resolve(); } + handleRunCodeAnalysisOnActiveFile(): Promise { return Promise.resolve(); } + handleRunCodeAnalysisOnOpenFiles(): Promise { return Promise.resolve(); } + handleRunCodeAnalysisOnAllFiles(): Promise { return Promise.resolve(); } + handleRemoveAllCodeAnalysisProblems(): Promise { return Promise.resolve(); } + handleRemoveCodeAnalysisProblems(refreshSquigglesOnSave: boolean, identifiersAndUris: CodeAnalysisDiagnosticIdentifiersAndUri[]): Promise { return Promise.resolve(); } + handleFixCodeAnalysisProblems(workspaceEdit: vscode.WorkspaceEdit, refreshSquigglesOnSave: boolean, identifiersAndUris: CodeAnalysisDiagnosticIdentifiersAndUri[]): Promise { return Promise.resolve(); } + handleDisableAllTypeCodeAnalysisProblems(code: string, identifiersAndUris: CodeAnalysisDiagnosticIdentifiersAndUri[]): Promise { return Promise.resolve(); } + handleCreateDeclarationOrDefinition(isCopyToClipboard: boolean, codeActionRange?: Range): Promise { return Promise.resolve(); } + handleExtractToFunction(extractAsGlobal: boolean): Promise { return Promise.resolve(); } + onInterval(): void { } + dispose(): void { + this.booleanEvent.dispose(); + this.stringEvent.dispose(); + } + addFileAssociations(fileAssociations: string, languageId: string): void { } + sendDidChangeSettings(): void { } + isInitialized(): boolean { return true; } + getShowConfigureIntelliSenseButton(): boolean { return false; } + setShowConfigureIntelliSenseButton(show: boolean): void { } + addTrustedCompiler(path: string): Promise { return Promise.resolve(); } + getIncludes(maxDepth: number, token: vscode.CancellationToken): Promise { return Promise.resolve({} as GetIncludesResult); } + getChatContext(token: vscode.CancellationToken): Promise { return Promise.resolve({} as ChatContextResult); } +} diff --git a/Extension/src/LanguageServer/extension.ts b/Extension/src/LanguageServer/extension.ts index 02dd3e8861..993db97875 100644 --- a/Extension/src/LanguageServer/extension.ts +++ b/Extension/src/LanguageServer/extension.ts @@ -1,1389 +1,1387 @@ -/* -------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All Rights Reserved. - * See 'LICENSE' in the project root for license information. - * ------------------------------------------------------------------------------------------ */ -'use strict'; - -import * as fs from 'fs'; -// Node.js 18 fetch isn't available until VS 1.82. -import fetch from 'node-fetch'; -import * as StreamZip from 'node-stream-zip'; -import * as os from 'os'; -import * as path from 'path'; -import * as vscode from 'vscode'; -import { Range } from 'vscode-languageclient'; -import * as nls from 'vscode-nls'; -import { TargetPopulation } from 'vscode-tas-client'; -import * as which from 'which'; -import { logAndReturn } from '../Utility/Async/returns'; -import * as util from '../common'; -import { getCrashCallStacksChannel } from '../logger'; -import { PlatformInformation } from '../platform'; -import * as telemetry from '../telemetry'; -import { Client, DefaultClient, DoxygenCodeActionCommandArguments, openFileVersions } from './client'; -import { ClientCollection } from './clientCollection'; -import { CodeActionDiagnosticInfo, CodeAnalysisDiagnosticIdentifiersAndUri, codeAnalysisAllFixes, codeAnalysisCodeToFixes, codeAnalysisFileToCodeActions } from './codeAnalysis'; -import { registerRelatedFilesProvider } from './copilotProviders'; -import { CppBuildTaskProvider } from './cppBuildTaskProvider'; -import { getCustomConfigProviders } from './customProviders'; -import { getLanguageConfig } from './languageConfig'; -import { CppConfigurationLanguageModelTool } from './lmTool'; -import { PersistentState } from './persistentState'; -import { NodeType, TreeNode } from './referencesModel'; -import { CppSettings } from './settings'; -import { LanguageStatusUI, getUI } from './ui'; -import { makeLspRange, rangeEquals, showInstallCompilerWalkthrough } from './utils'; - -nls.config({ messageFormat: nls.MessageFormat.bundle, bundleFormat: nls.BundleFormat.standalone })(); -const localize: nls.LocalizeFunc = nls.loadMessageBundle(); -export const CppSourceStr: string = "C/C++"; -export const configPrefix: string = "C/C++: "; - -let prevMacCrashFile: string; -let prevCppCrashFile: string; -let prevCppCrashCallStackData: string = ""; -export let clients: ClientCollection; -let activeDocument: vscode.TextDocument | undefined; -let ui: LanguageStatusUI; -const disposables: vscode.Disposable[] = []; -const commandDisposables: vscode.Disposable[] = []; -let languageConfigurations: vscode.Disposable[] = []; -let intervalTimer: NodeJS.Timeout; -let codeActionProvider: vscode.Disposable; -export const intelliSenseDisabledError: string = "Do not activate the extension when IntelliSense is disabled."; - -type VcpkgDatabase = Record; // Stored as
-> [] -let vcpkgDbPromise: Promise; -async function initVcpkgDatabase(): Promise { - const database: VcpkgDatabase = {}; - try { - const zip = new StreamZip.async({ file: util.getExtensionFilePath('VCPkgHeadersDatabase.zip') }); - try { - const data = await zip.entryData('VCPkgHeadersDatabase.txt'); - const lines = data.toString().split('\n'); - lines.forEach(line => { - const portFilePair: string[] = line.split(':'); - if (portFilePair.length !== 2) { - return; - } - - const portName: string = portFilePair[0]; - const relativeHeader: string = portFilePair[1].trimEnd(); - - if (!database[relativeHeader]) { - database[relativeHeader] = []; - } - - database[relativeHeader].push(portName); - }); - } catch { - console.log("Unable to parse vcpkg database file."); - } - await zip.close(); - } catch { - console.log("Unable to open vcpkg database file."); - } - return database; -} - -function getVcpkgHelpAction(): vscode.CodeAction { - const dummy: any[] = [{}]; // To distinguish between entry from CodeActions and the command palette - return { - command: { title: 'vcpkgOnlineHelpSuggested', command: 'C_Cpp.VcpkgOnlineHelpSuggested', arguments: dummy }, - title: localize("learn.how.to.install.a.library", "Learn how to install a library for this header with vcpkg"), - kind: vscode.CodeActionKind.QuickFix - }; -} - -function getVcpkgClipboardInstallAction(port: string): vscode.CodeAction { - return { - command: { title: 'vcpkgClipboardInstallSuggested', command: 'C_Cpp.VcpkgClipboardInstallSuggested', arguments: [[port]] }, - title: localize("copy.vcpkg.command", "Copy vcpkg command to install '{0}' to the clipboard", port), - kind: vscode.CodeActionKind.QuickFix - }; -} - -async function lookupIncludeInVcpkg(document: vscode.TextDocument, line: number): Promise { - const matches: RegExpMatchArray | null = document.lineAt(line).text.match(/#include\s*[<"](?[^>"]*)[>"]/); - if (!matches || !matches.length || !matches.groups) { - return []; - } - const missingHeader: string = matches.groups.includeFile.replace(/\//g, '\\'); - - let portsWithHeader: string[] | undefined; - const vcpkgDb: VcpkgDatabase = await vcpkgDbPromise; - if (vcpkgDb) { - portsWithHeader = vcpkgDb[missingHeader]; - } - return portsWithHeader ? portsWithHeader : []; -} - -function isMissingIncludeDiagnostic(diagnostic: vscode.Diagnostic): boolean { - const missingIncludeCode: number = 1696; - if (diagnostic.code === null || diagnostic.code === undefined || !diagnostic.source) { - return false; - } - return diagnostic.code === missingIncludeCode && diagnostic.source === 'C/C++'; -} - -function sendActivationTelemetry(): void { - const activateEvent: Record = {}; - // Don't log telemetry for machineId if it's a special value used by the dev host: someValue.machineid - if (vscode.env.machineId !== "someValue.machineId") { - const machineIdPersistentState: PersistentState = new PersistentState("CPP.machineId", undefined); - if (!machineIdPersistentState.Value) { - activateEvent.newMachineId = vscode.env.machineId; - } else if (machineIdPersistentState.Value !== vscode.env.machineId) { - activateEvent.newMachineId = vscode.env.machineId; - activateEvent.oldMachineId = machineIdPersistentState.Value; - } - machineIdPersistentState.Value = vscode.env.machineId; - } - if (vscode.env.uiKind === vscode.UIKind.Web) { - activateEvent.WebUI = "1"; - } - telemetry.logLanguageServerEvent("Activate", activateEvent); -} - -/** - * activate: set up the extension for language services - */ -export async function activate(): Promise { - sendActivationTelemetry(); - const checkForConflictingExtensions: PersistentState = new PersistentState("CPP." + util.packageJson.version + ".checkForConflictingExtensions", true); - if (checkForConflictingExtensions.Value) { - checkForConflictingExtensions.Value = false; - const clangCommandAdapterActive: boolean = vscode.extensions.all.some((extension: vscode.Extension): boolean => - extension.isActive && extension.id === "mitaki28.vscode-clang"); - if (clangCommandAdapterActive) { - telemetry.logLanguageServerEvent("conflictingExtension"); - } - } - - clients = new ClientCollection(); - ui = getUI(); - - // There may have already been registered CustomConfigurationProviders. - // Request for configurations from those providers. - clients.forEach(client => { - getCustomConfigProviders().forEach(provider => void client.onRegisterCustomConfigurationProvider(provider)); - }); - - disposables.push(vscode.workspace.onDidChangeConfiguration(onDidChangeSettings)); - disposables.push(vscode.workspace.onDidChangeTextDocument(onDidChangeTextDocument)); - disposables.push(vscode.window.onDidChangeTextEditorVisibleRanges((e) => clients.ActiveClient.enqueue(async () => onDidChangeTextEditorVisibleRanges(e)))); - disposables.push(vscode.window.onDidChangeActiveTextEditor((e) => clients.ActiveClient.enqueue(async () => onDidChangeActiveTextEditor(e)))); - ui.didChangeActiveEditor(); // Handle already active documents (for non-cpp files that we don't register didOpen). - disposables.push(vscode.window.onDidChangeTextEditorSelection((e) => clients.ActiveClient.enqueue(async () => onDidChangeTextEditorSelection(e)))); - disposables.push(vscode.window.onDidChangeVisibleTextEditors((e) => clients.ActiveClient.enqueue(async () => onDidChangeVisibleTextEditors(e)))); - - updateLanguageConfigurations(); - - reportMacCrashes(); - - vcpkgDbPromise = initVcpkgDatabase(); - - void clients.ActiveClient.ready.then(() => intervalTimer = global.setInterval(onInterval, 2500)); - - await registerCommands(true); - - vscode.tasks.onDidStartTask(() => getActiveClient().PauseCodeAnalysis()); - - vscode.tasks.onDidEndTask(event => { - getActiveClient().ResumeCodeAnalysis(); - if (event.execution.task.definition.type === CppBuildTaskProvider.CppBuildScriptType - || event.execution.task.name.startsWith(configPrefix)) { - if (event.execution.task.scope !== vscode.TaskScope.Global && event.execution.task.scope !== vscode.TaskScope.Workspace) { - const folder: vscode.WorkspaceFolder | undefined = event.execution.task.scope; - if (folder) { - const settings: CppSettings = new CppSettings(folder.uri); - if (settings.codeAnalysisRunOnBuild && settings.clangTidyEnabled) { - void clients.getClientFor(folder.uri).handleRunCodeAnalysisOnAllFiles().catch(logAndReturn.undefined); - } - return; - } - } - const settings: CppSettings = new CppSettings(); - if (settings.codeAnalysisRunOnBuild && settings.clangTidyEnabled) { - void clients.ActiveClient.handleRunCodeAnalysisOnAllFiles().catch(logAndReturn.undefined); - } - } - }); - - const selector: vscode.DocumentSelector = [ - { scheme: 'file', language: 'c' }, - { scheme: 'file', language: 'cpp' }, - { scheme: 'file', language: 'cuda-cpp' } - ]; - codeActionProvider = vscode.languages.registerCodeActionsProvider(selector, { - provideCodeActions: async (document: vscode.TextDocument, range: vscode.Range, context: vscode.CodeActionContext): Promise => { - - if (!await clients.ActiveClient.getVcpkgEnabled()) { - return []; - } - - // Generate vcpkg install/help commands if the incoming doc/range is a missing include error - if (!context.diagnostics.some(isMissingIncludeDiagnostic)) { - return []; - } - - const ports: string[] = await lookupIncludeInVcpkg(document, range.start.line); - if (ports.length <= 0) { - return []; - } - - telemetry.logLanguageServerEvent('codeActionsProvided', { "source": "vcpkg" }); - - if (!await clients.ActiveClient.getVcpkgInstalled()) { - return [getVcpkgHelpAction()]; - } - - const actions: vscode.CodeAction[] = ports.map(getVcpkgClipboardInstallAction); - return actions; - } - }); - - await vscode.commands.executeCommand('setContext', 'cpptools.msvcEnvironmentFound', util.hasMsvcEnvironment()); - - // Log cold start. - const activeEditor: vscode.TextEditor | undefined = vscode.window.activeTextEditor; - if (activeEditor) { - clients.timeTelemetryCollector.setFirstFile(activeEditor.document.uri); - activeDocument = activeEditor.document; - } - - if (util.extensionContext) { - // lmTools wasn't stabilized until 1.95, but (as of October 2024) - // cpptools can be installed on older versions of VS Code. See - // https://github.com/microsoft/vscode-cpptools/blob/main/Extension/package.json#L14 - const version = util.getVsCodeVersion(); - if (version[0] > 1 || (version[0] === 1 && version[1] >= 95)) { - const tool = vscode.lm.registerTool('cpptools-lmtool-configuration', new CppConfigurationLanguageModelTool()); - disposables.push(tool); - } - } - - await registerRelatedFilesProvider(); -} - -export function updateLanguageConfigurations(): void { - languageConfigurations.forEach(d => d.dispose()); - languageConfigurations = []; - - languageConfigurations.push(vscode.languages.setLanguageConfiguration('c', getLanguageConfig('c'))); - languageConfigurations.push(vscode.languages.setLanguageConfiguration('cpp', getLanguageConfig('cpp'))); - languageConfigurations.push(vscode.languages.setLanguageConfiguration('cuda-cpp', getLanguageConfig('cuda-cpp'))); -} - -/** - * workspace events - */ -async function onDidChangeSettings(event: vscode.ConfigurationChangeEvent): Promise { - const client: Client = clients.getDefaultClient(); - if (client instanceof DefaultClient) { - const defaultClient: DefaultClient = client as DefaultClient; - const changedDefaultClientSettings: Record = await defaultClient.onDidChangeSettings(event); - clients.forEach(client => { - if (client !== defaultClient) { - void client.onDidChangeSettings(event).catch(logAndReturn.undefined); - } - }); - const newUpdateChannel: string = changedDefaultClientSettings.updateChannel; - if (newUpdateChannel || event.affectsConfiguration("extensions.autoUpdate")) { - UpdateInsidersAccess(); - } - } -} - -async function onDidChangeTextDocument(event: vscode.TextDocumentChangeEvent): Promise { - const me: Client = clients.getClientFor(event.document.uri); - me.onDidChangeTextDocument(event); -} - -let noActiveEditorTimeout: NodeJS.Timeout | undefined; - -async function onDidChangeTextEditorVisibleRanges(event: vscode.TextEditorVisibleRangesChangeEvent): Promise { - if (util.isCpp(event.textEditor.document)) { - await clients.getDefaultClient().onDidChangeTextEditorVisibleRanges(event.textEditor.document.uri); - } -} - -function onDidChangeActiveTextEditor(editor?: vscode.TextEditor): void { - /* need to notify the affected client(s) */ - console.assert(clients !== undefined, "client should be available before active editor is changed"); - if (clients === undefined) { - return; - } - - if (noActiveEditorTimeout) { - clearTimeout(noActiveEditorTimeout); - noActiveEditorTimeout = undefined; - } - if (!editor) { - // When switching between documents, VS Code is setting the active editor to undefined - // temporarily, so this prevents the C++-related status bar items from flickering off/on. - noActiveEditorTimeout = setTimeout(() => { - activeDocument = undefined; - ui.didChangeActiveEditor(); - noActiveEditorTimeout = undefined; - }, 100); - void clients.didChangeActiveEditor(undefined).catch(logAndReturn.undefined); - } else { - ui.didChangeActiveEditor(); - if (util.isCppOrRelated(editor.document)) { - if (util.isCpp(editor.document)) { - activeDocument = editor.document; - void clients.didChangeActiveEditor(editor).catch(logAndReturn.undefined); - } else { - activeDocument = undefined; - void clients.didChangeActiveEditor(undefined).catch(logAndReturn.undefined); - } - //clients.ActiveClient.selectionChanged(makeLspRange(editor.selection)); - } else { - activeDocument = undefined; - } - } -} - -function onDidChangeTextEditorSelection(event: vscode.TextEditorSelectionChangeEvent): void { - if (!util.isCpp(event.textEditor.document)) { - return; - } - clients.ActiveClient.selectionChanged(makeLspRange(event.selections[0])); -} - -async function onDidChangeVisibleTextEditors(editors: readonly vscode.TextEditor[]): Promise { - const cppEditors: vscode.TextEditor[] = editors.filter(e => util.isCpp(e.document)); - await clients.getDefaultClient().onDidChangeVisibleTextEditors(cppEditors); -} - -function onInterval(): void { - // TODO: do we need to pump messages to all clients? depends on what we do with the icons, I suppose. - clients.ActiveClient.onInterval(); -} - -/** - * registered commands - */ -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.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)); - commandDisposables.push(vscode.commands.registerCommand('C_Cpp.ConfigurationSelect', enabled ? onSelectConfiguration : onDisabledCommand)); - commandDisposables.push(vscode.commands.registerCommand('C_Cpp.ConfigurationProviderSelect', enabled ? onSelectConfigurationProvider : onDisabledCommand)); - commandDisposables.push(vscode.commands.registerCommand('C_Cpp.ConfigurationEditJSON', enabled ? onEditConfigurationJSON : onDisabledCommand)); - commandDisposables.push(vscode.commands.registerCommand('C_Cpp.ConfigurationEditUI', enabled ? onEditConfigurationUI : onDisabledCommand)); - commandDisposables.push(vscode.commands.registerCommand('C_Cpp.ConfigurationEdit', enabled ? onEditConfiguration : onDisabledCommand)); - commandDisposables.push(vscode.commands.registerCommand('C_Cpp.AddToIncludePath', enabled ? onAddToIncludePath : onDisabledCommand)); - commandDisposables.push(vscode.commands.registerCommand('C_Cpp.EnableErrorSquiggles', enabled ? onEnableSquiggles : onDisabledCommand)); - commandDisposables.push(vscode.commands.registerCommand('C_Cpp.DisableErrorSquiggles', enabled ? onDisableSquiggles : onDisabledCommand)); - commandDisposables.push(vscode.commands.registerCommand('C_Cpp.ToggleDimInactiveRegions', enabled ? onToggleDimInactiveRegions : onDisabledCommand)); - commandDisposables.push(vscode.commands.registerCommand('C_Cpp.PauseParsing', enabled ? onPauseParsing : onDisabledCommand)); - commandDisposables.push(vscode.commands.registerCommand('C_Cpp.ResumeParsing', enabled ? onResumeParsing : onDisabledCommand)); - commandDisposables.push(vscode.commands.registerCommand('C_Cpp.PauseCodeAnalysis', enabled ? onPauseCodeAnalysis : onDisabledCommand)); - commandDisposables.push(vscode.commands.registerCommand('C_Cpp.ResumeCodeAnalysis', enabled ? onResumeCodeAnalysis : onDisabledCommand)); - commandDisposables.push(vscode.commands.registerCommand('C_Cpp.CancelCodeAnalysis', enabled ? onCancelCodeAnalysis : onDisabledCommand)); - commandDisposables.push(vscode.commands.registerCommand('C_Cpp.ShowActiveCodeAnalysisCommands', enabled ? onShowActiveCodeAnalysisCommands : onDisabledCommand)); - commandDisposables.push(vscode.commands.registerCommand('C_Cpp.ShowIdleCodeAnalysisCommands', enabled ? onShowIdleCodeAnalysisCommands : onDisabledCommand)); - commandDisposables.push(vscode.commands.registerCommand('C_Cpp.ShowReferencesProgress', enabled ? onShowReferencesProgress : onDisabledCommand)); - commandDisposables.push(vscode.commands.registerCommand('C_Cpp.TakeSurvey', enabled ? onTakeSurvey : onDisabledCommand)); - commandDisposables.push(vscode.commands.registerCommand('C_Cpp.LogDiagnostics', enabled ? onLogDiagnostics : onDisabledCommand)); - commandDisposables.push(vscode.commands.registerCommand('C_Cpp.RescanWorkspace', enabled ? onRescanWorkspace : onDisabledCommand)); - commandDisposables.push(vscode.commands.registerCommand('C_Cpp.ShowReferenceItem', enabled ? onShowRefCommand : onDisabledCommand)); - commandDisposables.push(vscode.commands.registerCommand('C_Cpp.referencesViewGroupByType', enabled ? onToggleRefGroupView : onDisabledCommand)); - commandDisposables.push(vscode.commands.registerCommand('C_Cpp.referencesViewUngroupByType', enabled ? onToggleRefGroupView : onDisabledCommand)); - commandDisposables.push(vscode.commands.registerCommand('C_Cpp.VcpkgClipboardInstallSuggested', enabled ? onVcpkgClipboardInstallSuggested : onDisabledCommand)); - commandDisposables.push(vscode.commands.registerCommand('C_Cpp.VcpkgOnlineHelpSuggested', enabled ? onVcpkgOnlineHelpSuggested : onDisabledCommand)); - commandDisposables.push(vscode.commands.registerCommand('C_Cpp.GenerateEditorConfig', enabled ? onGenerateEditorConfig : onDisabledCommand)); - commandDisposables.push(vscode.commands.registerCommand('C_Cpp.GoToNextDirectiveInGroup', enabled ? onGoToNextDirectiveInGroup : onDisabledCommand)); - commandDisposables.push(vscode.commands.registerCommand('C_Cpp.GoToPrevDirectiveInGroup', enabled ? onGoToPrevDirectiveInGroup : onDisabledCommand)); - commandDisposables.push(vscode.commands.registerCommand('C_Cpp.RunCodeAnalysisOnActiveFile', enabled ? onRunCodeAnalysisOnActiveFile : onDisabledCommand)); - commandDisposables.push(vscode.commands.registerCommand('C_Cpp.RunCodeAnalysisOnOpenFiles', enabled ? onRunCodeAnalysisOnOpenFiles : onDisabledCommand)); - commandDisposables.push(vscode.commands.registerCommand('C_Cpp.RunCodeAnalysisOnAllFiles', enabled ? onRunCodeAnalysisOnAllFiles : onDisabledCommand)); - commandDisposables.push(vscode.commands.registerCommand('C_Cpp.RemoveCodeAnalysisProblems', enabled ? onRemoveCodeAnalysisProblems : onDisabledCommand)); - commandDisposables.push(vscode.commands.registerCommand('C_Cpp.RemoveAllCodeAnalysisProblems', enabled ? onRemoveAllCodeAnalysisProblems : onDisabledCommand)); - commandDisposables.push(vscode.commands.registerCommand('C_Cpp.FixThisCodeAnalysisProblem', enabled ? onFixThisCodeAnalysisProblem : onDisabledCommand)); - commandDisposables.push(vscode.commands.registerCommand('C_Cpp.FixAllTypeCodeAnalysisProblems', enabled ? onFixAllTypeCodeAnalysisProblems : onDisabledCommand)); - commandDisposables.push(vscode.commands.registerCommand('C_Cpp.FixAllCodeAnalysisProblems', enabled ? onFixAllCodeAnalysisProblems : onDisabledCommand)); - commandDisposables.push(vscode.commands.registerCommand('C_Cpp.DisableAllTypeCodeAnalysisProblems', enabled ? onDisableAllTypeCodeAnalysisProblems : onDisabledCommand)); - commandDisposables.push(vscode.commands.registerCommand('C_Cpp.ShowCodeAnalysisDocumentation', enabled ? (uri) => vscode.env.openExternal(uri) : onDisabledCommand)); - 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.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)); - commandDisposables.push(vscode.commands.registerCommand('C_Cpp.CopyDeclarationOrDefinition', enabled ? onCopyDeclarationOrDefinition : onDisabledCommand)); - commandDisposables.push(vscode.commands.registerCommand('C_Cpp.RescanCompilers', enabled ? onRescanCompilers : onDisabledCommand)); - commandDisposables.push(vscode.commands.registerCommand('C_Cpp.AddMissingInclude', enabled ? onAddMissingInclude : onDisabledCommand)); - commandDisposables.push(vscode.commands.registerCommand('C_Cpp.ExtractToFunction', enabled ? () => onExtractToFunction(false, false) : onDisabledCommand)); - commandDisposables.push(vscode.commands.registerCommand('C_Cpp.ExtractToFreeFunction', enabled ? () => onExtractToFunction(true, false) : onDisabledCommand)); - commandDisposables.push(vscode.commands.registerCommand('C_Cpp.ExtractToMemberFunction', enabled ? () => onExtractToFunction(false, true) : onDisabledCommand)); - commandDisposables.push(vscode.commands.registerCommand('C_Cpp.ExpandSelection', enabled ? (r: Range) => onExpandSelection(r) : onDisabledCommand)); -} - -function onDisabledCommand() { - const message: string = localize( - { - key: "on.disabled.command", - comment: [ - "Markdown text between `` should not be translated or localized (they represent literal text) and the capitalization, spacing, and punctuation (including the ``) should not be altered." - ] - }, - "IntelliSense-related commands cannot be executed when `C_Cpp.intelliSenseEngine` is set to `disabled`."); - return vscode.window.showWarningMessage(message); -} - -async function onRestartIntelliSenseForFile() { - const activeEditor: vscode.TextEditor | undefined = vscode.window.activeTextEditor; - if (!activeEditor || !util.isCpp(activeEditor.document)) { - return; - } - return clients.ActiveClient.restartIntelliSenseForFile(activeEditor.document); -} - -async function onSwitchHeaderSource(): Promise { - const activeEditor: vscode.TextEditor | undefined = vscode.window.activeTextEditor; - if (!activeEditor || !util.isCpp(activeEditor.document)) { - return; - } - - let rootUri: vscode.Uri | undefined = clients.ActiveClient.RootUri; - const fileName: string = activeEditor.document.fileName; - - if (!rootUri) { - 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 document: vscode.TextDocument = await vscode.workspace.openTextDocument(targetFileName); - const workbenchConfig: vscode.WorkspaceConfiguration = vscode.workspace.getConfiguration("workbench"); - let foundEditor: boolean = false; - if (workbenchConfig.get("editor.revealIfOpen")) { - // If the document is already visible in another column, open it there. - vscode.window.visibleTextEditors.forEach(editor => { - if (editor.document === document && !foundEditor) { - foundEditor = true; - void vscode.window.showTextDocument(document, editor.viewColumn).then(undefined, logAndReturn.undefined); - } - }); - } - - if (!foundEditor) { - void vscode.window.showTextDocument(document).then(undefined, logAndReturn.undefined); - } -} - -/** - * Allow the user to select a workspace when multiple workspaces exist and get the corresponding Client back. - * The resulting client is used to handle some command that was previously invoked. - */ -async function selectClient(): Promise { - if (clients.Count === 1) { - return clients.ActiveClient; - } else { - const key: string | undefined = await ui.showWorkspaces(clients.Names); - if (key !== undefined && key !== "") { - const client: Client | undefined = clients.get(key); - if (client) { - return client; - } else { - console.assert("client not found"); - } - } - throw new Error(localize("client.not.found", "client not found")); - } -} - -async function onResetDatabase(): Promise { - await clients.ActiveClient.ready; - clients.ActiveClient.resetDatabase(); -} - -async function onRescanCompilers(sender?: any): Promise { - await clients.ActiveClient.ready; - return clients.ActiveClient.rescanCompilers(sender); -} - -async function onAddMissingInclude(): Promise { - telemetry.logLanguageServerEvent('AddMissingInclude'); -} - -async function selectIntelliSenseConfiguration(sender?: any): Promise { - await clients.ActiveClient.ready; - return clients.ActiveClient.promptSelectIntelliSenseConfiguration(sender); -} - -async function installCompiler(sender?: any): Promise { - const telemetryProperties = { sender: util.getSenderType(sender), platform: os.platform(), ranCommand: 'false' }; - const ok = localize('ok', 'OK'); - switch (os.platform()) { - case "win32": - showInstallCompilerWalkthrough(); - break; - case "darwin": { - const title = localize('install.compiler.mac.title', 'The clang compiler will now be installed'); - const detail = localize('install.compiler.mac.detail', 'You may be prompted to type your password in the VS Code terminal window to authorize the installation.'); - const response = await vscode.window.showInformationMessage(title, { modal: true, detail }, ok); - if (response === ok) { - const terminal = vscode.window.createTerminal('Install C++ Compiler'); - terminal.sendText('sudo xcode-select --install'); - terminal.show(); - telemetryProperties.ranCommand = 'true'; - } - break; - } - default: { - const info = await PlatformInformation.GetPlatformInformation(); - const installCommand = (() => { - switch (info.distribution?.name) { - case 'ubuntu': - case 'linuxmint': - case 'debian': { - return 'sudo sh -c \'apt update ; apt install -y build-essential\''; - } - case 'centos': - case 'fedora': - case 'rhel': { - return 'sudo sh -c \'yum install -y gcc-c++ gdb\''; - } - case 'opensuse': - case 'opensuse-leap': - case 'opensuse-tumbleweed': { - return 'sudo sh -c \'zypper refresh ; zypper install gcc-c++ gdb\''; - } - } - return undefined; - })(); - if (installCommand) { - const title = localize('install.compiler.linux.title', 'The gcc compiler will now be installed'); - const detail = localize('install.compiler.linux.detail', 'You may be prompted to type your password in the VS Code terminal window to authorize the installation.'); - const response = await vscode.window.showInformationMessage(title, { modal: true, detail }, ok); - if (response === ok) { - const terminal = vscode.window.createTerminal('Install C++ Compiler'); - terminal.sendText(installCommand); - terminal.show(true); - telemetryProperties.ranCommand = 'true'; - } - } - } - } - telemetry.logLanguageServerEvent('installCompiler', telemetryProperties); -} - -async function onSelectConfiguration(): Promise { - if (!isFolderOpen()) { - void vscode.window.showInformationMessage(localize("configuration.select.first", 'Open a folder first to select a configuration.')); - } else { - // This only applies to the active client. You cannot change the configuration for - // a client that is not active since that client's UI will not be visible. - return clients.ActiveClient.handleConfigurationSelectCommand(); - } -} - -function onSelectConfigurationProvider(): void { - if (!isFolderOpen()) { - void vscode.window.showInformationMessage(localize("configuration.provider.select.first", 'Open a folder first to select a configuration provider.')); - } else { - void selectClient().then(client => client.handleConfigurationProviderSelectCommand(), logAndReturn.undefined); - } -} - -function onEditConfigurationJSON(viewColumn: vscode.ViewColumn = vscode.ViewColumn.Active): void { - telemetry.logLanguageServerEvent("SettingsCommand", { "palette": "json" }, undefined); - if (!isFolderOpen()) { - void vscode.window.showInformationMessage(localize('edit.configurations.open.first', 'Open a folder first to edit configurations')); - } else { - void selectClient().then(client => client.handleConfigurationEditJSONCommand(viewColumn), logAndReturn.undefined); - } -} - -function onEditConfigurationUI(viewColumn: vscode.ViewColumn = vscode.ViewColumn.Active): void { - telemetry.logLanguageServerEvent("SettingsCommand", { "palette": "ui" }, undefined); - if (!isFolderOpen()) { - void vscode.window.showInformationMessage(localize('edit.configurations.open.first', 'Open a folder first to edit configurations')); - } else { - void selectClient().then(client => client.handleConfigurationEditUICommand(viewColumn), logAndReturn.undefined); - } -} - -function onEditConfiguration(viewColumn: vscode.ViewColumn = vscode.ViewColumn.Active): void { - if (!isFolderOpen()) { - void vscode.window.showInformationMessage(localize('edit.configurations.open.first', 'Open a folder first to edit configurations')); - } else { - void selectClient().then(client => client.handleConfigurationEditCommand(viewColumn), logAndReturn.undefined); - } -} - -function onGenerateEditorConfig(): void { - if (!isFolderOpen()) { - const settings: CppSettings = new CppSettings(); - void settings.generateEditorConfig(); - } else { - void selectClient().then(client => { - const settings: CppSettings = new CppSettings(client.RootUri); - void settings.generateEditorConfig(); - }).catch(logAndReturn.undefined); - } -} - -async function onGoToNextDirectiveInGroup(): Promise { - return getActiveClient().handleGoToDirectiveInGroup(true); -} - -async function onGoToPrevDirectiveInGroup(): Promise { - return getActiveClient().handleGoToDirectiveInGroup(false); -} - -async function onRunCodeAnalysisOnActiveFile(): Promise { - if (activeDocument) { - await vscode.commands.executeCommand("workbench.action.files.saveAll"); - return getActiveClient().handleRunCodeAnalysisOnActiveFile(); - } -} - -async function onRunCodeAnalysisOnOpenFiles(): Promise { - if (openFileVersions.size > 0) { - await vscode.commands.executeCommand("workbench.action.files.saveAll"); - return getActiveClient().handleRunCodeAnalysisOnOpenFiles(); - } -} - -async function onRunCodeAnalysisOnAllFiles(): Promise { - await vscode.commands.executeCommand("workbench.action.files.saveAll"); - return getActiveClient().handleRunCodeAnalysisOnAllFiles(); -} - -async function onRemoveAllCodeAnalysisProblems(): Promise { - return getActiveClient().handleRemoveAllCodeAnalysisProblems(); -} - -async function onRemoveCodeAnalysisProblems(refreshSquigglesOnSave: boolean, identifiersAndUris: CodeAnalysisDiagnosticIdentifiersAndUri[]): Promise { - return getActiveClient().handleRemoveCodeAnalysisProblems(refreshSquigglesOnSave, identifiersAndUris); -} - -// Needed due to https://github.com/microsoft/vscode/issues/148723 . -const codeActionAbortedString: string = localize('code.action.aborted', "The code analysis fix could not be applied because the document has changed."); - -async function onFixThisCodeAnalysisProblem(version: number, workspaceEdit: vscode.WorkspaceEdit, refreshSquigglesOnSave: boolean, identifiersAndUris: CodeAnalysisDiagnosticIdentifiersAndUri[]): Promise { - if (identifiersAndUris.length < 1) { - return; - } - const codeActions: CodeActionDiagnosticInfo[] | undefined = codeAnalysisFileToCodeActions.get(identifiersAndUris[0].uri); - if (codeActions === undefined) { - return; - } - for (const codeAction of codeActions) { - if (codeAction.code === identifiersAndUris[0].identifiers[0].code && rangeEquals(codeAction.range, identifiersAndUris[0].identifiers[0].range)) { - if (version !== codeAction.version) { - void vscode.window.showErrorMessage(codeActionAbortedString); - return; - } - break; - } - } - return getActiveClient().handleFixCodeAnalysisProblems(workspaceEdit, refreshSquigglesOnSave, identifiersAndUris); -} - -async function onFixAllTypeCodeAnalysisProblems(type: string, version: number, workspaceEdit: vscode.WorkspaceEdit, refreshSquigglesOnSave: boolean, identifiersAndUris: CodeAnalysisDiagnosticIdentifiersAndUri[]): Promise { - if (version === codeAnalysisCodeToFixes.get(type)?.version) { - return getActiveClient().handleFixCodeAnalysisProblems(workspaceEdit, refreshSquigglesOnSave, identifiersAndUris); - } - void vscode.window.showErrorMessage(codeActionAbortedString); -} - -async function onFixAllCodeAnalysisProblems(version: number, workspaceEdit: vscode.WorkspaceEdit, refreshSquigglesOnSave: boolean, identifiersAndUris: CodeAnalysisDiagnosticIdentifiersAndUri[]): Promise { - if (version === codeAnalysisAllFixes.version) { - return getActiveClient().handleFixCodeAnalysisProblems(workspaceEdit, refreshSquigglesOnSave, identifiersAndUris); - } - void vscode.window.showErrorMessage(codeActionAbortedString); -} - -async function onDisableAllTypeCodeAnalysisProblems(code: string, identifiersAndUris: CodeAnalysisDiagnosticIdentifiersAndUri[]): Promise { - return getActiveClient().handleDisableAllTypeCodeAnalysisProblems(code, identifiersAndUris); -} - -async function onCopyDeclarationOrDefinition(args?: any): Promise { - const sender: any | undefined = util.isString(args?.sender) ? args.sender : args; - const properties: Record = { - sender: util.getSenderType(sender) - }; - telemetry.logLanguageServerEvent('CopyDeclDefn', properties); - return getActiveClient().handleCreateDeclarationOrDefinition(true, args?.range); -} - -async function onCreateDeclarationOrDefinition(args?: any): Promise { - const sender: any | undefined = util.isString(args?.sender) ? args.sender : args; - const properties: Record = { - sender: util.getSenderType(sender) - }; - telemetry.logLanguageServerEvent('CreateDeclDefn', properties); - return getActiveClient().handleCreateDeclarationOrDefinition(false, args?.range); -} - -async function onExtractToFunction(extractAsGlobal: boolean, extractAsMemberFunction: boolean): Promise { - if (extractAsGlobal) { - telemetry.logLanguageServerEvent('ExtractToFreeFunction'); - } else if (extractAsMemberFunction) { - telemetry.logLanguageServerEvent('ExtractToMemberFunction'); - } else { - telemetry.logLanguageServerEvent('ExtractToFunction'); - } - return getActiveClient().handleExtractToFunction(extractAsGlobal); -} - -function onExpandSelection(r: Range) { - const activeTextEditor: vscode.TextEditor | undefined = vscode.window.activeTextEditor; - if (activeTextEditor) { - activeTextEditor.selection = new vscode.Selection(new vscode.Position(r.start.line, r.start.character), new vscode.Position(r.end.line, r.end.character)); - telemetry.logLanguageServerEvent('ExpandSelection'); - } -} - -function onAddToIncludePath(path: string): void { - if (isFolderOpen()) { - // This only applies to the active client. It would not make sense to add the include path - // suggestion to a different workspace. - return clients.ActiveClient.handleAddToIncludePathCommand(path); - } -} - -function onEnableSquiggles(): void { - // This only applies to the active client. - const settings: CppSettings = new CppSettings(clients.ActiveClient.RootUri); - settings.update("errorSquiggles", "enabled"); -} - -function onDisableSquiggles(): void { - // This only applies to the active client. - const settings: CppSettings = new CppSettings(clients.ActiveClient.RootUri); - settings.update("errorSquiggles", "disabled"); -} - -function onToggleDimInactiveRegions(): void { - // This only applies to the active client. - const settings: CppSettings = new CppSettings(clients.ActiveClient.RootUri); - settings.update("dimInactiveRegions", !settings.dimInactiveRegions); -} - -function onPauseParsing(): void { - clients.ActiveClient.pauseParsing(); -} - -function onResumeParsing(): void { - clients.ActiveClient.resumeParsing(); -} - -function onPauseCodeAnalysis(): void { - clients.ActiveClient.PauseCodeAnalysis(); -} - -function onResumeCodeAnalysis(): void { - clients.ActiveClient.ResumeCodeAnalysis(); -} - -function onCancelCodeAnalysis(): void { - clients.ActiveClient.CancelCodeAnalysis(); -} - -function onShowActiveCodeAnalysisCommands(): Promise { - return clients.ActiveClient.handleShowActiveCodeAnalysisCommands(); -} - -function onShowIdleCodeAnalysisCommands(): Promise { - return clients.ActiveClient.handleShowIdleCodeAnalysisCommands(); -} - -function onShowReferencesProgress(): void { - clients.ActiveClient.handleReferencesIcon(); -} - -function onToggleRefGroupView(): void { - // Set context to switch icons - const client: Client = getActiveClient(); - client.toggleReferenceResultsView(); -} - -function onTakeSurvey(): void { - telemetry.logLanguageServerEvent("onTakeSurvey"); - const uri: vscode.Uri = vscode.Uri.parse(`https://www.research.net/r/VBVV6C6?o=${os.platform()}&m=${vscode.env.machineId}`); - void vscode.commands.executeCommand('vscode.open', uri); -} - -function onVcpkgOnlineHelpSuggested(dummy?: any): void { - telemetry.logLanguageServerEvent('vcpkgAction', { 'source': dummy ? 'CodeAction' : 'CommandPalette', 'action': 'vcpkgOnlineHelpSuggested' }); - const uri: vscode.Uri = vscode.Uri.parse(`https://aka.ms/vcpkg`); - void vscode.commands.executeCommand('vscode.open', uri); -} - -async function onVcpkgClipboardInstallSuggested(ports?: string[]): Promise { - let source: string; - if (ports && ports.length) { - source = 'CodeAction'; - } else { - source = 'CommandPalette'; - // Glob up all existing diagnostics for missing includes and look them up in the vcpkg database - const missingIncludeLocations: [vscode.TextDocument, number[]][] = []; - vscode.languages.getDiagnostics().forEach(uriAndDiagnostics => { - // Extract textDocument - const textDocument: vscode.TextDocument | undefined = vscode.workspace.textDocuments.find(doc => doc.uri.fsPath === uriAndDiagnostics[0].fsPath); - if (!textDocument) { - return; - } - - // Extract lines numbers for missing include diagnostics - let lines: number[] = uriAndDiagnostics[1].filter(isMissingIncludeDiagnostic).map(d => d.range.start.line); - if (!lines.length) { - return; - } - - // Filter duplicate lines - lines = lines.filter((line: number, index: number) => { - const foundIndex: number = lines.indexOf(line); - return foundIndex === index; - }); - - missingIncludeLocations.push([textDocument, lines]); - }); - if (!missingIncludeLocations.length) { - return; - } - - // Queue look ups in the vcpkg database for missing ports; filter out duplicate results - const portsPromises: Promise[] = []; - missingIncludeLocations.forEach(docAndLineNumbers => { - docAndLineNumbers[1].forEach(line => { - portsPromises.push(lookupIncludeInVcpkg(docAndLineNumbers[0], line)); - }); - }); - ports = ([] as string[]).concat(...await Promise.all(portsPromises)); - if (!ports.length) { - return; - } - const ports2: string[] = ports; - ports = ports2.filter((port: string, index: number) => ports2.indexOf(port) === index); - } - - let installCommand: string = 'vcpkg install'; - ports.forEach(port => installCommand += ` ${port}`); - telemetry.logLanguageServerEvent('vcpkgAction', { 'source': source, 'action': 'vcpkgClipboardInstallSuggested', 'ports': ports.toString() }); - - await vscode.env.clipboard.writeText(installCommand); -} - -function onGenerateDoxygenComment(arg: DoxygenCodeActionCommandArguments): Promise { - return getActiveClient().handleGenerateDoxygenComment(arg); -} - -function onSetActiveConfigName(configurationName: string): Thenable { - return clients.ActiveClient.setCurrentConfigName(configurationName); -} - -function onGetActiveConfigName(): Thenable { - return clients.ActiveClient.getCurrentConfigName(); -} - -function onGetActiveConfigCustomVariable(variableName: string): Thenable { - return clients.ActiveClient.getCurrentConfigCustomVariable(variableName); -} - -function onLogDiagnostics(): Promise { - return clients.ActiveClient.logDiagnostics(); -} - -function onRescanWorkspace(): Promise { - return clients.ActiveClient.rescanFolder(); -} - -function onShowRefCommand(arg?: TreeNode): void { - if (!arg) { - return; - } - const { node } = arg; - if (node === NodeType.reference) { - const { referenceLocation } = arg; - if (referenceLocation) { - void vscode.window.showTextDocument(referenceLocation.uri, { - selection: referenceLocation.range.with({ start: referenceLocation.range.start, end: referenceLocation.range.end }) - }).then(undefined, logAndReturn.undefined); - } - } else if (node === NodeType.fileWithPendingRef) { - const { fileUri } = arg; - if (fileUri) { - void vscode.window.showTextDocument(fileUri).then(undefined, logAndReturn.undefined); - } - } -} - -function reportMacCrashes(): void { - if (process.platform === "darwin") { - prevMacCrashFile = ""; - const home: string = os.homedir(); - const crashFolder: string = path.resolve(home, "Library/Logs/DiagnosticReports"); - fs.stat(crashFolder, (err) => { - const crashObject: Record = {}; - if (err?.code) { - // If the directory isn't there, we have a problem... - crashObject["errCode"] = err.code; - telemetry.logLanguageServerEvent("MacCrash", crashObject); - return; - } - - // vscode.workspace.createFileSystemWatcher only works in workspace folders. - try { - fs.watch(crashFolder, (event, filename) => { - if (event !== "rename") { - return; - } - if (!filename || filename === prevMacCrashFile) { - return; - } - prevMacCrashFile = filename; - if (!filename.startsWith("cpptools")) { - return; - } - // Wait 5 seconds to allow time for the crash log to finish being written. - setTimeout(() => { - fs.readFile(path.resolve(crashFolder, filename), 'utf8', (err, data) => { - if (err) { - // Try again? - fs.readFile(path.resolve(crashFolder, filename), 'utf8', handleMacCrashFileRead); - return; - } - handleMacCrashFileRead(err, data); - }); - }, 5000); - }); - } catch (e) { - // The file watcher limit is hit (may not be possible on Mac, but just in case). - } - }); - } -} - -export function usesCrashHandler(): boolean { - if (os.platform() === "darwin") { - if (os.arch() === "arm64") { - return true; - } else { - const releaseParts: string[] = os.release().split("."); - if (releaseParts.length >= 1) { - // Avoid potentially intereferring with the older macOS crash handler. - return parseInt(releaseParts[0]) >= 19; - } - return true; - } - } - return os.platform() !== "win32" && os.arch() === "x64"; -} - -export function watchForCrashes(crashDirectory: string): void { - if (crashDirectory !== "") { - prevCppCrashFile = ""; - fs.stat(crashDirectory, (err) => { - const crashObject: Record = {}; - if (err?.code) { - // If the directory isn't there, we have a problem... - crashObject["errCode"] = err.code; - telemetry.logLanguageServerEvent("CppCrash", crashObject); - return; - } - - // vscode.workspace.createFileSystemWatcher only works in workspace folders. - try { - fs.watch(crashDirectory, (event, filename) => { - if (event !== "rename") { - return; - } - if (!filename || filename === prevCppCrashFile) { - return; - } - prevCppCrashFile = filename; - if (!filename.startsWith("cpptools")) { - return; - } - const crashDate: Date = new Date(); - - // Wait 5 seconds to allow time for the crash log to finish being written. - setTimeout(() => { - fs.readFile(path.resolve(crashDirectory, filename), 'utf8', (err, data) => { - void handleCrashFileRead(crashDirectory, filename, crashDate, err, data); - }); - }, 5000); - }); - } catch (e) { - // The file watcher limit is hit (may not be possible on Mac, but just in case). - } - }); - } -} - -let previousCrashData: string; -let previousCrashCount: number = 0; - -function logCrashTelemetry(data: string, type: string, offsetData?: string): void { - const crashObject: Record = {}; - const crashCountObject: Record = {}; - crashObject.CrashingThreadCallStack = data; - if (offsetData !== undefined) { - crashObject.CrashingThreadCallStackOffsets = offsetData; - } - previousCrashCount = data === previousCrashData ? previousCrashCount + 1 : 0; - previousCrashData = data; - crashCountObject.CrashCount = previousCrashCount + 1; - telemetry.logLanguageServerEvent(type, crashObject, crashCountObject); -} - -function logMacCrashTelemetry(data: string): void { - logCrashTelemetry(data, "MacCrash"); -} - -function logCppCrashTelemetry(data: string, offsetData?: string): void { - logCrashTelemetry(data, "CppCrash", offsetData); -} - -function handleMacCrashFileRead(err: NodeJS.ErrnoException | undefined | null, data: string): void { - if (err) { - return logMacCrashTelemetry("readFile: " + err.code); - } - - // Extract the crashing process version, because the version might not match - // if multiple VS Codes are running with different extension versions. - let binaryVersion: string = ""; - const startVersion: number = data.indexOf("Version:"); - if (startVersion >= 0) { - data = data.substring(startVersion); - const binaryVersionMatches: string[] | null = data.match(/^Version:\s*(\d*\.\d*\.\d*\.\d*|\d)/); - binaryVersion = binaryVersionMatches && binaryVersionMatches.length > 1 ? binaryVersionMatches[1] : ""; - } - - // Extract any message indicating missing dynamically loaded symbols. - let dynamicLoadError: string = ""; - const dynamicLoadErrorStart: string = "Dyld Error Message:"; - const startDynamicLoadError: number = data.indexOf(dynamicLoadErrorStart); - if (startDynamicLoadError >= 0) { - // Scan until the next blank line. - const dynamicLoadErrorEnd: string = "\n\n"; - const endDynamicLoadError: number = data.indexOf(dynamicLoadErrorEnd, startDynamicLoadError); - if (endDynamicLoadError >= 0) { - dynamicLoadError = data.substring(startDynamicLoadError, endDynamicLoadError); - if (dynamicLoadError.includes("/")) { - dynamicLoadError = ""; - } - dynamicLoadError += "\n\n"; - } - } - - // Extract the crashing thread's call stack. - const crashStart: string = " Crashed:"; - let startCrash: number = data.indexOf(crashStart); - if (startCrash < 0) { - return logMacCrashTelemetry(dynamicLoadError + "No crash start"); - } - startCrash += crashStart.length + 1; // Skip past crashStart. - let endCrash: number = data.indexOf("Thread ", startCrash); - if (endCrash < 0) { - endCrash = data.length - 1; // Not expected, but just in case. - } - if (endCrash <= startCrash) { - return logMacCrashTelemetry(dynamicLoadError + "No crash end"); - } - data = data.substring(startCrash, endCrash); - - // Get rid of the memory addresses (which breaks being able get a hit count for each crash call stack). - data = data.replace(/0x................ /g, ""); - data = data.replace(/0x1........ \+ 0/g, ""); - - // Get rid of the process names on each line and just add it to the start. - const processNames: string[] = ["cpptools-srv", "cpptools-wordexp", "cpptools", - // Since only crash logs that start with "cpptools" are reported, the cases below would only occur - // if the crash were to happen before the new process had fully started and renamed itself. - "clang-tidy", "clang-format", "clang", "gcc"]; - let processNameFound: boolean = false; - for (const processName of processNames) { - if (data.includes(processName)) { - data = data.replace(new RegExp(processName + "\\s+", "g"), ""); - data = `${processName}\t${binaryVersion}\n${data}`; - processNameFound = true; - break; - } - } - if (!processNameFound) { - // Not expected, but just in case a new binary gets added. - // Warning: Don't use ??? because that is checked below. - data = `cpptools??\t${binaryVersion}\n${data}`; - } - - // Remove runtime lines because they can be different on different machines. - const lines: string[] = data.split("\n"); - data = ""; - lines.forEach((line: string) => { - if (!line.includes(".dylib") && !line.includes("???")) { - line = line.replace(/^\d+\s+/, ""); // Remove from the start of the line. - line = line.replace(/std::__1::/g, "std::"); // __1:: is not helpful. - if (line.includes("/")) { - data += "\n"; - } else { - data += line + "\n"; - } - } - }); - data = data.trimRight(); - - // Prepend the dynamic load error. - data = dynamicLoadError + data; - - if (data.length > 8192) { // The API has an 8k limit. - data = data.substring(0, 8189) + "..."; - } - - logMacCrashTelemetry(data); -} - -async function handleCrashFileRead(crashDirectory: string, crashFile: string, crashDate: Date, err: NodeJS.ErrnoException | undefined | null, data: string): Promise { - if (err) { - if (err.code === "ENOENT") { - return; // ignore known issue - } - return logCppCrashTelemetry("readFile: " + err.code); - } - - const lines: string[] = data.split("\n"); - let addressData: string = ".\n."; - const isCppToolsSrv: boolean = crashFile.startsWith("cpptools-srv"); - const telemetryHeader: string = (isCppToolsSrv ? "cpptools-srv.txt" : crashFile) + "\n"; - const filtPath: string | null = which.sync("c++filt", { nothrow: true }); - const isMac: boolean = process.platform === "darwin"; - const startStr: string = isMac ? " _" : "<"; - const offsetStr: string = isMac ? " + " : "+"; - const endOffsetStr: string = isMac ? " " : " <"; - const dotStr: string = "\n…"; - let signalType: string; - if (lines[0].startsWith("SIG")) { - signalType = lines[0]; - } else { - // The signal type may fail to be written. - signalType = "SIG-??\n"; // Intentionally different from SIG-? from cpptools. - } - let crashCallStack: string = ""; - let validFrameFound: boolean = false; - for (let lineNum: number = 0; lineNum < lines.length - 3; ++lineNum) { // skip last lines - const line: string = lines[lineNum]; - const startPos: number = line.indexOf(startStr); - if (startPos === -1 || line[startPos + (isMac ? 1 : 4)] === "+") { - if (!validFrameFound) { - continue; // Skip extra … at the start. - } - crashCallStack += dotStr; - const startAddressPos: number = line.indexOf("0x"); - const endAddressPos: number = line.indexOf(endOffsetStr, startAddressPos + 2); - addressData += "\n"; - if (startAddressPos === -1 || endAddressPos === -1 || startAddressPos >= endAddressPos) { - addressData += "Unexpected offset"; - } else { - addressData += line.substring(startAddressPos, endAddressPos); - } - continue; - } - const offsetPos: number = line.indexOf(offsetStr, startPos + startStr.length); - if (offsetPos === -1) { - crashCallStack += "\nMissing offsetStr"; - addressData += "\n"; - continue; // unexpected - } - const startPos2: number = startPos + 1; - let funcStr: string = line.substring(startPos2, offsetPos); - if (filtPath && filtPath.length !== 0) { - let ret: util.ProcessReturnType | undefined = await util.spawnChildProcess(filtPath, ["--no-strip-underscore", funcStr], undefined, true).catch(logAndReturn.undefined); - if (ret?.output === funcStr) { - ret = await util.spawnChildProcess(filtPath, [funcStr], undefined, true).catch(logAndReturn.undefined); - } - if (ret !== undefined && ret.succeeded) { - funcStr = ret.output; - funcStr = funcStr.replace(/std::(?:__1|__cxx11)/g, "std"); // simplify std namespaces. - funcStr = funcStr.replace(/std::basic_/g, "std::"); - funcStr = funcStr.replace(/ >/g, ">"); - funcStr = funcStr.replace(/, std::(?:allocator|char_traits)/g, ""); - funcStr = funcStr.replace(//g, ""); - funcStr = funcStr.replace(/, std::allocator/g, ""); - } - } - if (funcStr.includes("/")) { - funcStr = ""; - } else if (!validFrameFound && (funcStr.startsWith("crash_handler(") || funcStr.startsWith("_sigtramp"))) { - continue; // Skip these on early frames. - } - validFrameFound = true; - crashCallStack += "\n"; - addressData += "\n"; - crashCallStack += funcStr + offsetStr; - const offsetPos2: number = offsetPos + offsetStr.length; - if (isMac) { - const pendingOffset: string = line.substring(offsetPos2); - if (!pendingOffset.includes("/")) { - crashCallStack += pendingOffset; - } - const startAddressPos: number = line.indexOf("0x"); - if (startAddressPos === -1 || startAddressPos >= startPos) { - // unexpected - crashCallStack += ""; - continue; - } - addressData += `${line.substring(startAddressPos, startPos)}`; - } else { - const endPos: number = line.indexOf(">", offsetPos2); - if (endPos === -1) { - crashCallStack += " >"; - continue; // unexpected - } - const pendingOffset: string = line.substring(offsetPos2, endPos); - if (!pendingOffset.includes("/")) { - crashCallStack += pendingOffset; - } - } - } - - if (crashCallStack !== prevCppCrashCallStackData) { - prevCppCrashCallStackData = crashCallStack; - - const settings: vscode.WorkspaceConfiguration = vscode.workspace.getConfiguration("C_Cpp", null); - if (lines.length >= 6 && util.getNumericLoggingLevel(settings.get("loggingLevel")) >= 1) { - const out: vscode.OutputChannel = getCrashCallStacksChannel(); - out.appendLine(`\n${isCppToolsSrv ? "cpptools-srv" : "cpptools"}\n${crashDate.toLocaleString()}\n${signalType}${crashCallStack}`); - } - } - - data = telemetryHeader + signalType + crashCallStack; - - if (data.length > 8192) { // The API has an 8k limit. - data = data.substring(0, 8191) + "…"; - } - - logCppCrashTelemetry(data, addressData); - - await util.deleteFile(path.resolve(crashDirectory, crashFile)).catch(logAndReturn.undefined); - if (crashFile === "cpptools.txt") { - void util.deleteDirectory(crashDirectory).catch(logAndReturn.undefined); - } -} - -export function deactivate(): Thenable { - clients.timeTelemetryCollector.clear(); - telemetry.logLanguageServerEvent("LanguageServerShutdown"); - clearInterval(intervalTimer); - commandDisposables.forEach(d => d.dispose()); - disposables.forEach(d => d.dispose()); - languageConfigurations.forEach(d => d.dispose()); - ui.dispose(); - if (codeActionProvider) { - codeActionProvider.dispose(); - } - return clients.dispose(); -} - -export function isFolderOpen(): boolean { - return vscode.workspace.workspaceFolders !== undefined && vscode.workspace.workspaceFolders.length > 0; -} - -export function getClients(): ClientCollection { - return clients; -} - -export function getActiveClient(): Client { - return clients.ActiveClient; -} - -export function UpdateInsidersAccess(): void { - let installPrerelease: boolean = false; - - // Only move them to the new prerelease mechanism if using updateChannel of Insiders. - const settings: CppSettings = new CppSettings(); - const migratedInsiders: PersistentState = new PersistentState("CPP.migratedInsiders", false); - if (settings.updateChannel === "Insiders") { - // Don't do anything while the user has autoUpdate disabled, so we do not cause the extension to be updated. - if (!migratedInsiders.Value && vscode.workspace.getConfiguration("extensions", null).get("autoUpdate")) { - installPrerelease = true; - migratedInsiders.Value = true; - } - } else { - // Reset persistent value, so we register again if they switch to "Insiders" again. - if (migratedInsiders.Value) { - migratedInsiders.Value = false; - } - } - - // Mitigate an issue with VS Code not recognizing a programmatically installed VSIX as Prerelease. - // If using VS Code Insiders, and updateChannel is not explicitly set, default to Prerelease. - // Only do this once. If the user manually switches to Release, we don't want to switch them back to Prerelease again. - if (util.isVsCodeInsiders()) { - const insidersMitigationDone: PersistentState = new PersistentState("CPP.insidersMitigationDone", false); - if (!insidersMitigationDone.Value) { - if (vscode.workspace.getConfiguration("extensions", null).get("autoUpdate")) { - if (settings.getStringWithUndefinedDefault("updateChannel") === undefined) { - installPrerelease = true; - } - } - insidersMitigationDone.Value = true; - } - } - - if (installPrerelease) { - void vscode.commands.executeCommand("workbench.extensions.installExtension", "ms-vscode.cpptools", { installPreReleaseVersion: true }).then(undefined, logAndReturn.undefined); - } -} - -export async function preReleaseCheck(): Promise { - const displayedPreReleasePrompt: PersistentState = new PersistentState("CPP.displayedPreReleasePrompt", false); - const isOnPreRelease: PersistentState = new PersistentState("CPP.isOnPreRelease", false); - - if (util.getCppToolsTargetPopulation() === TargetPopulation.Insiders) { - isOnPreRelease.Value = true; - return; - } - - // First we need to make sure the user isn't already on a pre-release version and hasn't dismissed this prompt before. - if (!isOnPreRelease.Value && !displayedPreReleasePrompt.Value && util.getCppToolsTargetPopulation() === TargetPopulation.Public) { - // Get the info on the latest version from the marketplace to check if there is a pre-release version available. - const response = await fetch('https://marketplace.visualstudio.com/_apis/public/gallery/extensionquery', { - method: 'POST', - headers: { - Accept: 'application/json; api-version=3.0-preview', - 'Content-Type': 'application/json', - 'User-Agent': 'vscode-cpptools' - }, - body: '{"filters": [{"criteria": [{"filterType": 7, "value": "ms-vscode.cpptools"}]}], "flags": 529}' - }).catch(logAndReturn.undefined); - - telemetry.logLanguageServerEvent("marketplaceFetch", undefined, { status: response?.status ?? 0 }); - - const data: any = await response?.json().catch(logAndReturn.undefined); - - const preReleaseAvailable = data?.results[0].extensions[0].versions[0].properties.some((e: object) => Object.values(e).includes("Microsoft.VisualStudio.Code.PreRelease")); - - // If the user isn't on the pre-release version, but one is available, prompt them to install it. - if (preReleaseAvailable) { - displayedPreReleasePrompt.Value = true; - const message: string = localize("prerelease.message", "A pre-release version of the C/C++ extension is available. Would you like to switch to it?"); - const yes: string = localize("yes.button", "Yes"); - const no: string = localize("no.button", "No"); - void vscode.window.showInformationMessage(message, yes, no).then((selection) => { - if (selection === yes) { - void vscode.commands.executeCommand("workbench.extensions.installExtension", "ms-vscode.cpptools", { installPreReleaseVersion: true }).then(undefined, logAndReturn.undefined); - } - }); - } - } -} +/* -------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All Rights Reserved. + * See 'LICENSE' in the project root for license information. + * ------------------------------------------------------------------------------------------ */ +'use strict'; + +import * as fs from 'fs'; +// Node.js 18 fetch isn't available until VS 1.82. +import fetch from 'node-fetch'; +import * as StreamZip from 'node-stream-zip'; +import * as os from 'os'; +import * as path from 'path'; +import * as vscode from 'vscode'; +import { Range } from 'vscode-languageclient'; +import * as nls from 'vscode-nls'; +import { TargetPopulation } from 'vscode-tas-client'; +import * as which from 'which'; +import { logAndReturn } from '../Utility/Async/returns'; +import * as util from '../common'; +import { getCrashCallStacksChannel } from '../logger'; +import { PlatformInformation } from '../platform'; +import * as telemetry from '../telemetry'; +import { Client, DefaultClient, DoxygenCodeActionCommandArguments, openFileVersions } from './client'; +import { ClientCollection } from './clientCollection'; +import { CodeActionDiagnosticInfo, CodeAnalysisDiagnosticIdentifiersAndUri, codeAnalysisAllFixes, codeAnalysisCodeToFixes, codeAnalysisFileToCodeActions } from './codeAnalysis'; +import { registerRelatedFilesProvider } from './copilotProviders'; +import { CppBuildTaskProvider } from './cppBuildTaskProvider'; +import { getCustomConfigProviders } from './customProviders'; +import { getLanguageConfig } from './languageConfig'; +import { CppConfigurationLanguageModelTool } from './lmTool'; +import { PersistentState } from './persistentState'; +import { NodeType, TreeNode } from './referencesModel'; +import { CppSettings } from './settings'; +import { LanguageStatusUI, getUI } from './ui'; +import { makeLspRange, rangeEquals, showInstallCompilerWalkthrough } from './utils'; + +nls.config({ messageFormat: nls.MessageFormat.bundle, bundleFormat: nls.BundleFormat.standalone })(); +const localize: nls.LocalizeFunc = nls.loadMessageBundle(); +export const CppSourceStr: string = "C/C++"; +export const configPrefix: string = "C/C++: "; + +let prevMacCrashFile: string; +let prevCppCrashFile: string; +let prevCppCrashCallStackData: string = ""; +export let clients: ClientCollection; +let activeDocument: vscode.TextDocument | undefined; +let ui: LanguageStatusUI; +const disposables: vscode.Disposable[] = []; +const commandDisposables: vscode.Disposable[] = []; +let languageConfigurations: vscode.Disposable[] = []; +let intervalTimer: NodeJS.Timeout; +let codeActionProvider: vscode.Disposable; +export const intelliSenseDisabledError: string = "Do not activate the extension when IntelliSense is disabled."; + +type VcpkgDatabase = Record; // Stored as
-> [] +let vcpkgDbPromise: Promise; +async function initVcpkgDatabase(): Promise { + const database: VcpkgDatabase = {}; + try { + const zip = new StreamZip.async({ file: util.getExtensionFilePath('VCPkgHeadersDatabase.zip') }); + try { + const data = await zip.entryData('VCPkgHeadersDatabase.txt'); + const lines = data.toString().split('\n'); + lines.forEach(line => { + const portFilePair: string[] = line.split(':'); + if (portFilePair.length !== 2) { + return; + } + + const portName: string = portFilePair[0]; + const relativeHeader: string = portFilePair[1].trimEnd(); + + if (!database[relativeHeader]) { + database[relativeHeader] = []; + } + + database[relativeHeader].push(portName); + }); + } catch { + console.log("Unable to parse vcpkg database file."); + } + await zip.close(); + } catch { + console.log("Unable to open vcpkg database file."); + } + return database; +} + +function getVcpkgHelpAction(): vscode.CodeAction { + const dummy: any[] = [{}]; // To distinguish between entry from CodeActions and the command palette + return { + command: { title: 'vcpkgOnlineHelpSuggested', command: 'C_Cpp.VcpkgOnlineHelpSuggested', arguments: dummy }, + title: localize("learn.how.to.install.a.library", "Learn how to install a library for this header with vcpkg"), + kind: vscode.CodeActionKind.QuickFix + }; +} + +function getVcpkgClipboardInstallAction(port: string): vscode.CodeAction { + return { + command: { title: 'vcpkgClipboardInstallSuggested', command: 'C_Cpp.VcpkgClipboardInstallSuggested', arguments: [[port]] }, + title: localize("copy.vcpkg.command", "Copy vcpkg command to install '{0}' to the clipboard", port), + kind: vscode.CodeActionKind.QuickFix + }; +} + +async function lookupIncludeInVcpkg(document: vscode.TextDocument, line: number): Promise { + const matches: RegExpMatchArray | null = document.lineAt(line).text.match(/#include\s*[<"](?[^>"]*)[>"]/); + if (!matches || !matches.length || !matches.groups) { + return []; + } + const missingHeader: string = matches.groups.includeFile.replace(/\//g, '\\'); + + let portsWithHeader: string[] | undefined; + const vcpkgDb: VcpkgDatabase = await vcpkgDbPromise; + if (vcpkgDb) { + portsWithHeader = vcpkgDb[missingHeader]; + } + return portsWithHeader ? portsWithHeader : []; +} + +function isMissingIncludeDiagnostic(diagnostic: vscode.Diagnostic): boolean { + const missingIncludeCode: number = 1696; + if (diagnostic.code === null || diagnostic.code === undefined || !diagnostic.source) { + return false; + } + return diagnostic.code === missingIncludeCode && diagnostic.source === 'C/C++'; +} + +function sendActivationTelemetry(): void { + const activateEvent: Record = {}; + // Don't log telemetry for machineId if it's a special value used by the dev host: someValue.machineid + if (vscode.env.machineId !== "someValue.machineId") { + const machineIdPersistentState: PersistentState = new PersistentState("CPP.machineId", undefined); + if (!machineIdPersistentState.Value) { + activateEvent.newMachineId = vscode.env.machineId; + } else if (machineIdPersistentState.Value !== vscode.env.machineId) { + activateEvent.newMachineId = vscode.env.machineId; + activateEvent.oldMachineId = machineIdPersistentState.Value; + } + machineIdPersistentState.Value = vscode.env.machineId; + } + if (vscode.env.uiKind === vscode.UIKind.Web) { + activateEvent.WebUI = "1"; + } + telemetry.logLanguageServerEvent("Activate", activateEvent); +} + +/** + * activate: set up the extension for language services + */ +export async function activate(): Promise { + sendActivationTelemetry(); + const checkForConflictingExtensions: PersistentState = new PersistentState("CPP." + util.packageJson.version + ".checkForConflictingExtensions", true); + if (checkForConflictingExtensions.Value) { + checkForConflictingExtensions.Value = false; + const clangCommandAdapterActive: boolean = vscode.extensions.all.some((extension: vscode.Extension): boolean => + extension.isActive && extension.id === "mitaki28.vscode-clang"); + if (clangCommandAdapterActive) { + telemetry.logLanguageServerEvent("conflictingExtension"); + } + } + + clients = new ClientCollection(); + ui = getUI(); + + // There may have already been registered CustomConfigurationProviders. + // Request for configurations from those providers. + clients.forEach(client => { + getCustomConfigProviders().forEach(provider => void client.onRegisterCustomConfigurationProvider(provider)); + }); + + disposables.push(vscode.workspace.onDidChangeConfiguration(onDidChangeSettings)); + disposables.push(vscode.workspace.onDidChangeTextDocument(onDidChangeTextDocument)); + disposables.push(vscode.window.onDidChangeTextEditorVisibleRanges((e) => clients.ActiveClient.enqueue(async () => onDidChangeTextEditorVisibleRanges(e)))); + disposables.push(vscode.window.onDidChangeActiveTextEditor((e) => clients.ActiveClient.enqueue(async () => onDidChangeActiveTextEditor(e)))); + ui.didChangeActiveEditor(); // Handle already active documents (for non-cpp files that we don't register didOpen). + disposables.push(vscode.window.onDidChangeTextEditorSelection((e) => clients.ActiveClient.enqueue(async () => onDidChangeTextEditorSelection(e)))); + disposables.push(vscode.window.onDidChangeVisibleTextEditors((e) => clients.ActiveClient.enqueue(async () => onDidChangeVisibleTextEditors(e)))); + + updateLanguageConfigurations(); + + reportMacCrashes(); + + vcpkgDbPromise = initVcpkgDatabase(); + + void clients.ActiveClient.ready.then(() => intervalTimer = global.setInterval(onInterval, 2500)); + + await registerCommands(true); + + vscode.tasks.onDidStartTask(() => getActiveClient().PauseCodeAnalysis()); + + vscode.tasks.onDidEndTask(event => { + getActiveClient().ResumeCodeAnalysis(); + if (event.execution.task.definition.type === CppBuildTaskProvider.CppBuildScriptType + || event.execution.task.name.startsWith(configPrefix)) { + if (event.execution.task.scope !== vscode.TaskScope.Global && event.execution.task.scope !== vscode.TaskScope.Workspace) { + const folder: vscode.WorkspaceFolder | undefined = event.execution.task.scope; + if (folder) { + const settings: CppSettings = new CppSettings(folder.uri); + if (settings.codeAnalysisRunOnBuild && settings.clangTidyEnabled) { + void clients.getClientFor(folder.uri).handleRunCodeAnalysisOnAllFiles().catch(logAndReturn.undefined); + } + return; + } + } + const settings: CppSettings = new CppSettings(); + if (settings.codeAnalysisRunOnBuild && settings.clangTidyEnabled) { + void clients.ActiveClient.handleRunCodeAnalysisOnAllFiles().catch(logAndReturn.undefined); + } + } + }); + + const selector: vscode.DocumentSelector = [ + { scheme: 'file', language: 'c' }, + { scheme: 'file', language: 'cpp' }, + { scheme: 'file', language: 'cuda-cpp' } + ]; + codeActionProvider = vscode.languages.registerCodeActionsProvider(selector, { + provideCodeActions: async (document: vscode.TextDocument, range: vscode.Range, context: vscode.CodeActionContext): Promise => { + + if (!await clients.ActiveClient.getVcpkgEnabled()) { + return []; + } + + // Generate vcpkg install/help commands if the incoming doc/range is a missing include error + if (!context.diagnostics.some(isMissingIncludeDiagnostic)) { + return []; + } + + const ports: string[] = await lookupIncludeInVcpkg(document, range.start.line); + if (ports.length <= 0) { + return []; + } + + telemetry.logLanguageServerEvent('codeActionsProvided', { "source": "vcpkg" }); + + if (!await clients.ActiveClient.getVcpkgInstalled()) { + return [getVcpkgHelpAction()]; + } + + const actions: vscode.CodeAction[] = ports.map(getVcpkgClipboardInstallAction); + return actions; + } + }); + + await vscode.commands.executeCommand('setContext', 'cpptools.msvcEnvironmentFound', util.hasMsvcEnvironment()); + + // Log cold start. + const activeEditor: vscode.TextEditor | undefined = vscode.window.activeTextEditor; + if (activeEditor) { + clients.timeTelemetryCollector.setFirstFile(activeEditor.document.uri); + activeDocument = activeEditor.document; + } + + if (util.extensionContext) { + // lmTools wasn't stabilized until 1.95, but (as of October 2024) + // cpptools can be installed on older versions of VS Code. See + // https://github.com/microsoft/vscode-cpptools/blob/main/Extension/package.json#L14 + const version = util.getVsCodeVersion(); + if (version[0] > 1 || (version[0] === 1 && version[1] >= 95)) { + const tool = vscode.lm.registerTool('cpptools-lmtool-configuration', new CppConfigurationLanguageModelTool()); + disposables.push(tool); + } + } + + await registerRelatedFilesProvider(); +} + +export function updateLanguageConfigurations(): void { + languageConfigurations.forEach(d => d.dispose()); + languageConfigurations = []; + + languageConfigurations.push(vscode.languages.setLanguageConfiguration('c', getLanguageConfig('c'))); + languageConfigurations.push(vscode.languages.setLanguageConfiguration('cpp', getLanguageConfig('cpp'))); + languageConfigurations.push(vscode.languages.setLanguageConfiguration('cuda-cpp', getLanguageConfig('cuda-cpp'))); +} + +/** + * workspace events + */ +async function onDidChangeSettings(event: vscode.ConfigurationChangeEvent): Promise { + const client: Client = clients.getDefaultClient(); + if (client instanceof DefaultClient) { + const defaultClient: DefaultClient = client as DefaultClient; + const changedDefaultClientSettings: Record = await defaultClient.onDidChangeSettings(event); + clients.forEach(client => { + if (client !== defaultClient) { + void client.onDidChangeSettings(event).catch(logAndReturn.undefined); + } + }); + const newUpdateChannel: string = changedDefaultClientSettings.updateChannel; + if (newUpdateChannel || event.affectsConfiguration("extensions.autoUpdate")) { + UpdateInsidersAccess(); + } + } +} + +async function onDidChangeTextDocument(event: vscode.TextDocumentChangeEvent): Promise { + const me: Client = clients.getClientFor(event.document.uri); + me.onDidChangeTextDocument(event); +} + +let noActiveEditorTimeout: NodeJS.Timeout | undefined; + +async function onDidChangeTextEditorVisibleRanges(event: vscode.TextEditorVisibleRangesChangeEvent): Promise { + if (util.isCpp(event.textEditor.document)) { + await clients.getDefaultClient().onDidChangeTextEditorVisibleRanges(event.textEditor.document.uri); + } +} + +function onDidChangeActiveTextEditor(editor?: vscode.TextEditor): void { + /* need to notify the affected client(s) */ + console.assert(clients !== undefined, "client should be available before active editor is changed"); + if (clients === undefined) { + return; + } + + if (noActiveEditorTimeout) { + clearTimeout(noActiveEditorTimeout); + noActiveEditorTimeout = undefined; + } + if (!editor) { + // When switching between documents, VS Code is setting the active editor to undefined + // temporarily, so this prevents the C++-related status bar items from flickering off/on. + noActiveEditorTimeout = setTimeout(() => { + activeDocument = undefined; + ui.didChangeActiveEditor(); + noActiveEditorTimeout = undefined; + }, 100); + void clients.didChangeActiveEditor(undefined).catch(logAndReturn.undefined); + } else { + ui.didChangeActiveEditor(); + if (util.isCppOrRelated(editor.document)) { + if (util.isCpp(editor.document)) { + activeDocument = editor.document; + void clients.didChangeActiveEditor(editor).catch(logAndReturn.undefined); + } else { + activeDocument = undefined; + void clients.didChangeActiveEditor(undefined).catch(logAndReturn.undefined); + } + //clients.ActiveClient.selectionChanged(makeLspRange(editor.selection)); + } else { + activeDocument = undefined; + } + } +} + +function onDidChangeTextEditorSelection(event: vscode.TextEditorSelectionChangeEvent): void { + if (!util.isCpp(event.textEditor.document)) { + return; + } + clients.ActiveClient.selectionChanged(makeLspRange(event.selections[0])); +} + +async function onDidChangeVisibleTextEditors(editors: readonly vscode.TextEditor[]): Promise { + const cppEditors: vscode.TextEditor[] = editors.filter(e => util.isCpp(e.document)); + await clients.getDefaultClient().onDidChangeVisibleTextEditors(cppEditors); +} + +function onInterval(): void { + // TODO: do we need to pump messages to all clients? depends on what we do with the icons, I suppose. + clients.ActiveClient.onInterval(); +} + +/** + * registered commands + */ +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.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)); + commandDisposables.push(vscode.commands.registerCommand('C_Cpp.ConfigurationSelect', enabled ? onSelectConfiguration : onDisabledCommand)); + commandDisposables.push(vscode.commands.registerCommand('C_Cpp.ConfigurationProviderSelect', enabled ? onSelectConfigurationProvider : onDisabledCommand)); + commandDisposables.push(vscode.commands.registerCommand('C_Cpp.ConfigurationEditJSON', enabled ? onEditConfigurationJSON : onDisabledCommand)); + commandDisposables.push(vscode.commands.registerCommand('C_Cpp.ConfigurationEditUI', enabled ? onEditConfigurationUI : onDisabledCommand)); + commandDisposables.push(vscode.commands.registerCommand('C_Cpp.ConfigurationEdit', enabled ? onEditConfiguration : onDisabledCommand)); + commandDisposables.push(vscode.commands.registerCommand('C_Cpp.AddToIncludePath', enabled ? onAddToIncludePath : onDisabledCommand)); + commandDisposables.push(vscode.commands.registerCommand('C_Cpp.EnableErrorSquiggles', enabled ? onEnableSquiggles : onDisabledCommand)); + commandDisposables.push(vscode.commands.registerCommand('C_Cpp.DisableErrorSquiggles', enabled ? onDisableSquiggles : onDisabledCommand)); + commandDisposables.push(vscode.commands.registerCommand('C_Cpp.ToggleDimInactiveRegions', enabled ? onToggleDimInactiveRegions : onDisabledCommand)); + commandDisposables.push(vscode.commands.registerCommand('C_Cpp.PauseParsing', enabled ? onPauseParsing : onDisabledCommand)); + commandDisposables.push(vscode.commands.registerCommand('C_Cpp.ResumeParsing', enabled ? onResumeParsing : onDisabledCommand)); + commandDisposables.push(vscode.commands.registerCommand('C_Cpp.PauseCodeAnalysis', enabled ? onPauseCodeAnalysis : onDisabledCommand)); + commandDisposables.push(vscode.commands.registerCommand('C_Cpp.ResumeCodeAnalysis', enabled ? onResumeCodeAnalysis : onDisabledCommand)); + commandDisposables.push(vscode.commands.registerCommand('C_Cpp.CancelCodeAnalysis', enabled ? onCancelCodeAnalysis : onDisabledCommand)); + commandDisposables.push(vscode.commands.registerCommand('C_Cpp.ShowActiveCodeAnalysisCommands', enabled ? onShowActiveCodeAnalysisCommands : onDisabledCommand)); + commandDisposables.push(vscode.commands.registerCommand('C_Cpp.ShowIdleCodeAnalysisCommands', enabled ? onShowIdleCodeAnalysisCommands : onDisabledCommand)); + commandDisposables.push(vscode.commands.registerCommand('C_Cpp.ShowReferencesProgress', enabled ? onShowReferencesProgress : onDisabledCommand)); + commandDisposables.push(vscode.commands.registerCommand('C_Cpp.TakeSurvey', enabled ? onTakeSurvey : onDisabledCommand)); + commandDisposables.push(vscode.commands.registerCommand('C_Cpp.LogDiagnostics', enabled ? onLogDiagnostics : onDisabledCommand)); + commandDisposables.push(vscode.commands.registerCommand('C_Cpp.RescanWorkspace', enabled ? onRescanWorkspace : onDisabledCommand)); + commandDisposables.push(vscode.commands.registerCommand('C_Cpp.ShowReferenceItem', enabled ? onShowRefCommand : onDisabledCommand)); + commandDisposables.push(vscode.commands.registerCommand('C_Cpp.referencesViewGroupByType', enabled ? onToggleRefGroupView : onDisabledCommand)); + commandDisposables.push(vscode.commands.registerCommand('C_Cpp.referencesViewUngroupByType', enabled ? onToggleRefGroupView : onDisabledCommand)); + commandDisposables.push(vscode.commands.registerCommand('C_Cpp.VcpkgClipboardInstallSuggested', enabled ? onVcpkgClipboardInstallSuggested : onDisabledCommand)); + commandDisposables.push(vscode.commands.registerCommand('C_Cpp.VcpkgOnlineHelpSuggested', enabled ? onVcpkgOnlineHelpSuggested : onDisabledCommand)); + commandDisposables.push(vscode.commands.registerCommand('C_Cpp.GenerateEditorConfig', enabled ? onGenerateEditorConfig : onDisabledCommand)); + commandDisposables.push(vscode.commands.registerCommand('C_Cpp.GoToNextDirectiveInGroup', enabled ? onGoToNextDirectiveInGroup : onDisabledCommand)); + commandDisposables.push(vscode.commands.registerCommand('C_Cpp.GoToPrevDirectiveInGroup', enabled ? onGoToPrevDirectiveInGroup : onDisabledCommand)); + commandDisposables.push(vscode.commands.registerCommand('C_Cpp.RunCodeAnalysisOnActiveFile', enabled ? onRunCodeAnalysisOnActiveFile : onDisabledCommand)); + commandDisposables.push(vscode.commands.registerCommand('C_Cpp.RunCodeAnalysisOnOpenFiles', enabled ? onRunCodeAnalysisOnOpenFiles : onDisabledCommand)); + commandDisposables.push(vscode.commands.registerCommand('C_Cpp.RunCodeAnalysisOnAllFiles', enabled ? onRunCodeAnalysisOnAllFiles : onDisabledCommand)); + commandDisposables.push(vscode.commands.registerCommand('C_Cpp.RemoveCodeAnalysisProblems', enabled ? onRemoveCodeAnalysisProblems : onDisabledCommand)); + commandDisposables.push(vscode.commands.registerCommand('C_Cpp.RemoveAllCodeAnalysisProblems', enabled ? onRemoveAllCodeAnalysisProblems : onDisabledCommand)); + commandDisposables.push(vscode.commands.registerCommand('C_Cpp.FixThisCodeAnalysisProblem', enabled ? onFixThisCodeAnalysisProblem : onDisabledCommand)); + commandDisposables.push(vscode.commands.registerCommand('C_Cpp.FixAllTypeCodeAnalysisProblems', enabled ? onFixAllTypeCodeAnalysisProblems : onDisabledCommand)); + commandDisposables.push(vscode.commands.registerCommand('C_Cpp.FixAllCodeAnalysisProblems', enabled ? onFixAllCodeAnalysisProblems : onDisabledCommand)); + commandDisposables.push(vscode.commands.registerCommand('C_Cpp.DisableAllTypeCodeAnalysisProblems', enabled ? onDisableAllTypeCodeAnalysisProblems : onDisabledCommand)); + commandDisposables.push(vscode.commands.registerCommand('C_Cpp.ShowCodeAnalysisDocumentation', enabled ? (uri) => vscode.env.openExternal(uri) : onDisabledCommand)); + 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.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)); + commandDisposables.push(vscode.commands.registerCommand('C_Cpp.CopyDeclarationOrDefinition', enabled ? onCopyDeclarationOrDefinition : onDisabledCommand)); + commandDisposables.push(vscode.commands.registerCommand('C_Cpp.RescanCompilers', enabled ? onRescanCompilers : onDisabledCommand)); + commandDisposables.push(vscode.commands.registerCommand('C_Cpp.AddMissingInclude', enabled ? onAddMissingInclude : onDisabledCommand)); + commandDisposables.push(vscode.commands.registerCommand('C_Cpp.ExtractToFunction', enabled ? () => onExtractToFunction(false, false) : onDisabledCommand)); + commandDisposables.push(vscode.commands.registerCommand('C_Cpp.ExtractToFreeFunction', enabled ? () => onExtractToFunction(true, false) : onDisabledCommand)); + commandDisposables.push(vscode.commands.registerCommand('C_Cpp.ExtractToMemberFunction', enabled ? () => onExtractToFunction(false, true) : onDisabledCommand)); + commandDisposables.push(vscode.commands.registerCommand('C_Cpp.ExpandSelection', enabled ? (r: Range) => onExpandSelection(r) : onDisabledCommand)); +} + +function onDisabledCommand() { + const message: string = localize( + { + key: "on.disabled.command", + comment: [ + "Markdown text between `` should not be translated or localized (they represent literal text) and the capitalization, spacing, and punctuation (including the ``) should not be altered." + ] + }, + "IntelliSense-related commands cannot be executed when `C_Cpp.intelliSenseEngine` is set to `disabled`."); + return vscode.window.showWarningMessage(message); +} + +async function onRestartIntelliSenseForFile() { + const activeEditor: vscode.TextEditor | undefined = vscode.window.activeTextEditor; + if (!activeEditor || !util.isCpp(activeEditor.document)) { + return; + } + return clients.ActiveClient.restartIntelliSenseForFile(activeEditor.document); +} + +async function onSwitchHeaderSource(): Promise { + const activeEditor: vscode.TextEditor | undefined = vscode.window.activeTextEditor; + if (!activeEditor || !util.isCpp(activeEditor.document)) { + return; + } + + let rootUri: vscode.Uri | undefined = clients.ActiveClient.RootUri; + const fileName: string = activeEditor.document.fileName; + + if (!rootUri) { + 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 document: vscode.TextDocument = await vscode.workspace.openTextDocument(targetFileName); + const workbenchConfig: vscode.WorkspaceConfiguration = vscode.workspace.getConfiguration("workbench"); + let foundEditor: boolean = false; + if (workbenchConfig.get("editor.revealIfOpen")) { + // If the document is already visible in another column, open it there. + vscode.window.visibleTextEditors.forEach(editor => { + if (editor.document === document && !foundEditor) { + foundEditor = true; + void vscode.window.showTextDocument(document, editor.viewColumn).then(undefined, logAndReturn.undefined); + } + }); + } + + if (!foundEditor) { + void vscode.window.showTextDocument(document).then(undefined, logAndReturn.undefined); + } +} + +/** + * Allow the user to select a workspace when multiple workspaces exist and get the corresponding Client back. + * The resulting client is used to handle some command that was previously invoked. + */ +async function selectClient(): Promise { + if (clients.Count === 1) { + return clients.ActiveClient; + } else { + const key: string | undefined = await ui.showWorkspaces(clients.Names); + if (key !== undefined && key !== "") { + const client: Client | undefined = clients.get(key); + if (client) { + return client; + } else { + console.assert("client not found"); + } + } + throw new Error(localize("client.not.found", "client not found")); + } +} + +async function onResetDatabase(): Promise { + await clients.ActiveClient.ready; + clients.ActiveClient.resetDatabase(); +} + +async function onRescanCompilers(sender?: any): Promise { + await clients.ActiveClient.ready; + return clients.ActiveClient.rescanCompilers(sender); +} + +async function onAddMissingInclude(): Promise { + telemetry.logLanguageServerEvent('AddMissingInclude'); +} + +async function selectIntelliSenseConfiguration(sender?: any): Promise { + await clients.ActiveClient.ready; + return clients.ActiveClient.promptSelectIntelliSenseConfiguration(sender); +} + +async function installCompiler(sender?: any): Promise { + const telemetryProperties = { sender: util.getSenderType(sender), platform: os.platform(), ranCommand: 'false' }; + const ok = localize('ok', 'OK'); + switch (os.platform()) { + case "win32": + showInstallCompilerWalkthrough(); + break; + case "darwin": { + const title = localize('install.compiler.mac.title', 'The clang compiler will now be installed'); + const detail = localize('install.compiler.mac.detail', 'You may be prompted to type your password in the VS Code terminal window to authorize the installation.'); + const response = await vscode.window.showInformationMessage(title, { modal: true, detail }, ok); + if (response === ok) { + const terminal = vscode.window.createTerminal('Install C++ Compiler'); + terminal.sendText('sudo xcode-select --install'); + terminal.show(); + telemetryProperties.ranCommand = 'true'; + } + break; + } + default: { + const info = await PlatformInformation.GetPlatformInformation(); + const installCommand = (() => { + switch (info.distribution?.name) { + case 'ubuntu': + case 'linuxmint': + case 'debian': { + return 'sudo sh -c \'apt update ; apt install -y build-essential\''; + } + case 'centos': + case 'fedora': + case 'rhel': { + return 'sudo sh -c \'yum install -y gcc-c++ gdb\''; + } + case 'opensuse': + case 'opensuse-leap': + case 'opensuse-tumbleweed': { + return 'sudo sh -c \'zypper refresh ; zypper install gcc-c++ gdb\''; + } + } + return undefined; + })(); + if (installCommand) { + const title = localize('install.compiler.linux.title', 'The gcc compiler will now be installed'); + const detail = localize('install.compiler.linux.detail', 'You may be prompted to type your password in the VS Code terminal window to authorize the installation.'); + const response = await vscode.window.showInformationMessage(title, { modal: true, detail }, ok); + if (response === ok) { + const terminal = vscode.window.createTerminal('Install C++ Compiler'); + terminal.sendText(installCommand); + terminal.show(true); + telemetryProperties.ranCommand = 'true'; + } + } + } + } + telemetry.logLanguageServerEvent('installCompiler', telemetryProperties); +} + +async function onSelectConfiguration(): Promise { + if (!isFolderOpen()) { + void vscode.window.showInformationMessage(localize("configuration.select.first", 'Open a folder first to select a configuration.')); + } else { + // This only applies to the active client. You cannot change the configuration for + // a client that is not active since that client's UI will not be visible. + return clients.ActiveClient.handleConfigurationSelectCommand(); + } +} + +function onSelectConfigurationProvider(): void { + if (!isFolderOpen()) { + void vscode.window.showInformationMessage(localize("configuration.provider.select.first", 'Open a folder first to select a configuration provider.')); + } else { + void selectClient().then(client => client.handleConfigurationProviderSelectCommand(), logAndReturn.undefined); + } +} + +function onEditConfigurationJSON(viewColumn: vscode.ViewColumn = vscode.ViewColumn.Active): void { + telemetry.logLanguageServerEvent("SettingsCommand", { "palette": "json" }, undefined); + if (!isFolderOpen()) { + void vscode.window.showInformationMessage(localize('edit.configurations.open.first', 'Open a folder first to edit configurations')); + } else { + void selectClient().then(client => client.handleConfigurationEditJSONCommand(viewColumn), logAndReturn.undefined); + } +} + +function onEditConfigurationUI(viewColumn: vscode.ViewColumn = vscode.ViewColumn.Active): void { + telemetry.logLanguageServerEvent("SettingsCommand", { "palette": "ui" }, undefined); + if (!isFolderOpen()) { + void vscode.window.showInformationMessage(localize('edit.configurations.open.first', 'Open a folder first to edit configurations')); + } else { + void selectClient().then(client => client.handleConfigurationEditUICommand(viewColumn), logAndReturn.undefined); + } +} + +function onEditConfiguration(viewColumn: vscode.ViewColumn = vscode.ViewColumn.Active): void { + if (!isFolderOpen()) { + void vscode.window.showInformationMessage(localize('edit.configurations.open.first', 'Open a folder first to edit configurations')); + } else { + void selectClient().then(client => client.handleConfigurationEditCommand(viewColumn), logAndReturn.undefined); + } +} + +function onGenerateEditorConfig(): void { + if (!isFolderOpen()) { + const settings: CppSettings = new CppSettings(); + void settings.generateEditorConfig(); + } else { + void selectClient().then(client => { + const settings: CppSettings = new CppSettings(client.RootUri); + void settings.generateEditorConfig(); + }).catch(logAndReturn.undefined); + } +} + +async function onGoToNextDirectiveInGroup(): Promise { + return getActiveClient().handleGoToDirectiveInGroup(true); +} + +async function onGoToPrevDirectiveInGroup(): Promise { + return getActiveClient().handleGoToDirectiveInGroup(false); +} + +async function onRunCodeAnalysisOnActiveFile(): Promise { + if (activeDocument) { + await vscode.commands.executeCommand("workbench.action.files.saveAll"); + return getActiveClient().handleRunCodeAnalysisOnActiveFile(); + } +} + +async function onRunCodeAnalysisOnOpenFiles(): Promise { + if (openFileVersions.size > 0) { + await vscode.commands.executeCommand("workbench.action.files.saveAll"); + return getActiveClient().handleRunCodeAnalysisOnOpenFiles(); + } +} + +async function onRunCodeAnalysisOnAllFiles(): Promise { + await vscode.commands.executeCommand("workbench.action.files.saveAll"); + return getActiveClient().handleRunCodeAnalysisOnAllFiles(); +} + +async function onRemoveAllCodeAnalysisProblems(): Promise { + return getActiveClient().handleRemoveAllCodeAnalysisProblems(); +} + +async function onRemoveCodeAnalysisProblems(refreshSquigglesOnSave: boolean, identifiersAndUris: CodeAnalysisDiagnosticIdentifiersAndUri[]): Promise { + return getActiveClient().handleRemoveCodeAnalysisProblems(refreshSquigglesOnSave, identifiersAndUris); +} + +// Needed due to https://github.com/microsoft/vscode/issues/148723 . +const codeActionAbortedString: string = localize('code.action.aborted', "The code analysis fix could not be applied because the document has changed."); + +async function onFixThisCodeAnalysisProblem(version: number, workspaceEdit: vscode.WorkspaceEdit, refreshSquigglesOnSave: boolean, identifiersAndUris: CodeAnalysisDiagnosticIdentifiersAndUri[]): Promise { + if (identifiersAndUris.length < 1) { + return; + } + const codeActions: CodeActionDiagnosticInfo[] | undefined = codeAnalysisFileToCodeActions.get(identifiersAndUris[0].uri); + if (codeActions === undefined) { + return; + } + for (const codeAction of codeActions) { + if (codeAction.code === identifiersAndUris[0].identifiers[0].code && rangeEquals(codeAction.range, identifiersAndUris[0].identifiers[0].range)) { + if (version !== codeAction.version) { + void vscode.window.showErrorMessage(codeActionAbortedString); + return; + } + break; + } + } + return getActiveClient().handleFixCodeAnalysisProblems(workspaceEdit, refreshSquigglesOnSave, identifiersAndUris); +} + +async function onFixAllTypeCodeAnalysisProblems(type: string, version: number, workspaceEdit: vscode.WorkspaceEdit, refreshSquigglesOnSave: boolean, identifiersAndUris: CodeAnalysisDiagnosticIdentifiersAndUri[]): Promise { + if (version === codeAnalysisCodeToFixes.get(type)?.version) { + return getActiveClient().handleFixCodeAnalysisProblems(workspaceEdit, refreshSquigglesOnSave, identifiersAndUris); + } + void vscode.window.showErrorMessage(codeActionAbortedString); +} + +async function onFixAllCodeAnalysisProblems(version: number, workspaceEdit: vscode.WorkspaceEdit, refreshSquigglesOnSave: boolean, identifiersAndUris: CodeAnalysisDiagnosticIdentifiersAndUri[]): Promise { + if (version === codeAnalysisAllFixes.version) { + return getActiveClient().handleFixCodeAnalysisProblems(workspaceEdit, refreshSquigglesOnSave, identifiersAndUris); + } + void vscode.window.showErrorMessage(codeActionAbortedString); +} + +async function onDisableAllTypeCodeAnalysisProblems(code: string, identifiersAndUris: CodeAnalysisDiagnosticIdentifiersAndUri[]): Promise { + return getActiveClient().handleDisableAllTypeCodeAnalysisProblems(code, identifiersAndUris); +} + +async function onCopyDeclarationOrDefinition(args?: any): Promise { + const sender: any | undefined = util.isString(args?.sender) ? args.sender : args; + const properties: Record = { + sender: util.getSenderType(sender) + }; + telemetry.logLanguageServerEvent('CopyDeclDefn', properties); + return getActiveClient().handleCreateDeclarationOrDefinition(true, args?.range); +} + +async function onCreateDeclarationOrDefinition(args?: any): Promise { + const sender: any | undefined = util.isString(args?.sender) ? args.sender : args; + const properties: Record = { + sender: util.getSenderType(sender) + }; + telemetry.logLanguageServerEvent('CreateDeclDefn', properties); + return getActiveClient().handleCreateDeclarationOrDefinition(false, args?.range); +} + +async function onExtractToFunction(extractAsGlobal: boolean, extractAsMemberFunction: boolean): Promise { + if (extractAsGlobal) { + telemetry.logLanguageServerEvent('ExtractToFreeFunction'); + } else if (extractAsMemberFunction) { + telemetry.logLanguageServerEvent('ExtractToMemberFunction'); + } else { + telemetry.logLanguageServerEvent('ExtractToFunction'); + } + return getActiveClient().handleExtractToFunction(extractAsGlobal); +} + +function onExpandSelection(r: Range) { + const activeTextEditor: vscode.TextEditor | undefined = vscode.window.activeTextEditor; + if (activeTextEditor) { + activeTextEditor.selection = new vscode.Selection(new vscode.Position(r.start.line, r.start.character), new vscode.Position(r.end.line, r.end.character)); + telemetry.logLanguageServerEvent('ExpandSelection'); + } +} + +function onAddToIncludePath(path: string): void { + if (isFolderOpen()) { + // This only applies to the active client. It would not make sense to add the include path + // suggestion to a different workspace. + return clients.ActiveClient.handleAddToIncludePathCommand(path); + } +} + +function onEnableSquiggles(): void { + // This only applies to the active client. + const settings: CppSettings = new CppSettings(clients.ActiveClient.RootUri); + settings.update("errorSquiggles", "enabled"); +} + +function onDisableSquiggles(): void { + // This only applies to the active client. + const settings: CppSettings = new CppSettings(clients.ActiveClient.RootUri); + settings.update("errorSquiggles", "disabled"); +} + +function onToggleDimInactiveRegions(): void { + // This only applies to the active client. + const settings: CppSettings = new CppSettings(clients.ActiveClient.RootUri); + settings.update("dimInactiveRegions", !settings.dimInactiveRegions); +} + +function onPauseParsing(): void { + clients.ActiveClient.pauseParsing(); +} + +function onResumeParsing(): void { + clients.ActiveClient.resumeParsing(); +} + +function onPauseCodeAnalysis(): void { + clients.ActiveClient.PauseCodeAnalysis(); +} + +function onResumeCodeAnalysis(): void { + clients.ActiveClient.ResumeCodeAnalysis(); +} + +function onCancelCodeAnalysis(): void { + clients.ActiveClient.CancelCodeAnalysis(); +} + +function onShowActiveCodeAnalysisCommands(): Promise { + return clients.ActiveClient.handleShowActiveCodeAnalysisCommands(); +} + +function onShowIdleCodeAnalysisCommands(): Promise { + return clients.ActiveClient.handleShowIdleCodeAnalysisCommands(); +} + +function onShowReferencesProgress(): void { + clients.ActiveClient.handleReferencesIcon(); +} + +function onToggleRefGroupView(): void { + // Set context to switch icons + const client: Client = getActiveClient(); + client.toggleReferenceResultsView(); +} + +function onTakeSurvey(): void { + telemetry.logLanguageServerEvent("onTakeSurvey"); + const uri: vscode.Uri = vscode.Uri.parse(`https://www.research.net/r/VBVV6C6?o=${os.platform()}&m=${vscode.env.machineId}`); + void vscode.commands.executeCommand('vscode.open', uri); +} + +function onVcpkgOnlineHelpSuggested(dummy?: any): void { + telemetry.logLanguageServerEvent('vcpkgAction', { 'source': dummy ? 'CodeAction' : 'CommandPalette', 'action': 'vcpkgOnlineHelpSuggested' }); + const uri: vscode.Uri = vscode.Uri.parse(`https://aka.ms/vcpkg`); + void vscode.commands.executeCommand('vscode.open', uri); +} + +async function onVcpkgClipboardInstallSuggested(ports?: string[]): Promise { + let source: string; + if (ports && ports.length) { + source = 'CodeAction'; + } else { + source = 'CommandPalette'; + // Glob up all existing diagnostics for missing includes and look them up in the vcpkg database + const missingIncludeLocations: [vscode.TextDocument, number[]][] = []; + vscode.languages.getDiagnostics().forEach(uriAndDiagnostics => { + // Extract textDocument + const textDocument: vscode.TextDocument | undefined = vscode.workspace.textDocuments.find(doc => doc.uri.fsPath === uriAndDiagnostics[0].fsPath); + if (!textDocument) { + return; + } + + // Extract lines numbers for missing include diagnostics + let lines: number[] = uriAndDiagnostics[1].filter(isMissingIncludeDiagnostic).map(d => d.range.start.line); + if (!lines.length) { + return; + } + + // Filter duplicate lines + lines = lines.filter((line: number, index: number) => { + const foundIndex: number = lines.indexOf(line); + return foundIndex === index; + }); + + missingIncludeLocations.push([textDocument, lines]); + }); + if (!missingIncludeLocations.length) { + return; + } + + // Queue look ups in the vcpkg database for missing ports; filter out duplicate results + const portsPromises: Promise[] = []; + missingIncludeLocations.forEach(docAndLineNumbers => { + docAndLineNumbers[1].forEach(line => { + portsPromises.push(lookupIncludeInVcpkg(docAndLineNumbers[0], line)); + }); + }); + ports = ([] as string[]).concat(...await Promise.all(portsPromises)); + if (!ports.length) { + return; + } + const ports2: string[] = ports; + ports = ports2.filter((port: string, index: number) => ports2.indexOf(port) === index); + } + + let installCommand: string = 'vcpkg install'; + ports.forEach(port => installCommand += ` ${port}`); + telemetry.logLanguageServerEvent('vcpkgAction', { 'source': source, 'action': 'vcpkgClipboardInstallSuggested', 'ports': ports.toString() }); + + await vscode.env.clipboard.writeText(installCommand); +} + +function onGenerateDoxygenComment(arg: DoxygenCodeActionCommandArguments): Promise { + return getActiveClient().handleGenerateDoxygenComment(arg); +} + +function onSetActiveConfigName(configurationName: string): Thenable { + return clients.ActiveClient.setCurrentConfigName(configurationName); +} + +function onGetActiveConfigName(): Thenable { + return clients.ActiveClient.getCurrentConfigName(); +} + +function onGetActiveConfigCustomVariable(variableName: string): Thenable { + return clients.ActiveClient.getCurrentConfigCustomVariable(variableName); +} + +function onLogDiagnostics(): Promise { + return clients.ActiveClient.logDiagnostics(); +} + +function onRescanWorkspace(): Promise { + return clients.ActiveClient.rescanFolder(); +} + +function onShowRefCommand(arg?: TreeNode): void { + if (!arg) { + return; + } + const { node } = arg; + if (node === NodeType.reference) { + const { referenceLocation } = arg; + if (referenceLocation) { + void vscode.window.showTextDocument(referenceLocation.uri, { + selection: referenceLocation.range.with({ start: referenceLocation.range.start, end: referenceLocation.range.end }) + }).then(undefined, logAndReturn.undefined); + } + } else if (node === NodeType.fileWithPendingRef) { + const { fileUri } = arg; + if (fileUri) { + void vscode.window.showTextDocument(fileUri).then(undefined, logAndReturn.undefined); + } + } +} + +function reportMacCrashes(): void { + if (process.platform === "darwin") { + prevMacCrashFile = ""; + const home: string = os.homedir(); + const crashFolder: string = path.resolve(home, "Library/Logs/DiagnosticReports"); + fs.stat(crashFolder, (err) => { + const crashObject: Record = {}; + if (err?.code) { + // If the directory isn't there, we have a problem... + crashObject["errCode"] = err.code; + telemetry.logLanguageServerEvent("MacCrash", crashObject); + return; + } + + // vscode.workspace.createFileSystemWatcher only works in workspace folders. + try { + fs.watch(crashFolder, (event, filename) => { + if (event !== "rename") { + return; + } + if (!filename || filename === prevMacCrashFile) { + return; + } + prevMacCrashFile = filename; + if (!filename.startsWith("cpptools")) { + return; + } + // Wait 5 seconds to allow time for the crash log to finish being written. + setTimeout(() => { + fs.readFile(path.resolve(crashFolder, filename), 'utf8', (err, data) => { + if (err) { + // Try again? + fs.readFile(path.resolve(crashFolder, filename), 'utf8', handleMacCrashFileRead); + return; + } + handleMacCrashFileRead(err, data); + }); + }, 5000); + }); + } catch (e) { + // The file watcher limit is hit (may not be possible on Mac, but just in case). + } + }); + } +} + +export function usesCrashHandler(): boolean { + if (os.platform() === "darwin") { + if (os.arch() === "arm64") { + return true; + } else { + const releaseParts: string[] = os.release().split("."); + if (releaseParts.length >= 1) { + // Avoid potentially intereferring with the older macOS crash handler. + return parseInt(releaseParts[0]) >= 19; + } + return true; + } + } + return os.platform() !== "win32" && os.arch() === "x64"; +} + +export function watchForCrashes(crashDirectory: string): void { + if (crashDirectory !== "") { + prevCppCrashFile = ""; + fs.stat(crashDirectory, (err) => { + const crashObject: Record = {}; + if (err?.code) { + // If the directory isn't there, we have a problem... + crashObject["errCode"] = err.code; + telemetry.logLanguageServerEvent("CppCrash", crashObject); + return; + } + + // vscode.workspace.createFileSystemWatcher only works in workspace folders. + try { + fs.watch(crashDirectory, (event, filename) => { + if (event !== "rename") { + return; + } + if (!filename || filename === prevCppCrashFile) { + return; + } + prevCppCrashFile = filename; + if (!filename.startsWith("cpptools")) { + return; + } + const crashDate: Date = new Date(); + + // Wait 5 seconds to allow time for the crash log to finish being written. + setTimeout(() => { + fs.readFile(path.resolve(crashDirectory, filename), 'utf8', (err, data) => { + void handleCrashFileRead(crashDirectory, filename, crashDate, err, data); + }); + }, 5000); + }); + } catch (e) { + // The file watcher limit is hit (may not be possible on Mac, but just in case). + } + }); + } +} + +let previousCrashData: string; +let previousCrashCount: number = 0; + +function logCrashTelemetry(data: string, type: string, offsetData?: string): void { + const crashObject: Record = {}; + const crashCountObject: Record = {}; + crashObject.CrashingThreadCallStack = data; + if (offsetData !== undefined) { + crashObject.CrashingThreadCallStackOffsets = offsetData; + } + previousCrashCount = data === previousCrashData ? previousCrashCount + 1 : 0; + previousCrashData = data; + crashCountObject.CrashCount = previousCrashCount + 1; + telemetry.logLanguageServerEvent(type, crashObject, crashCountObject); +} + +function logMacCrashTelemetry(data: string): void { + logCrashTelemetry(data, "MacCrash"); +} + +function logCppCrashTelemetry(data: string, offsetData?: string): void { + logCrashTelemetry(data, "CppCrash", offsetData); +} + +function handleMacCrashFileRead(err: NodeJS.ErrnoException | undefined | null, data: string): void { + if (err) { + return logMacCrashTelemetry("readFile: " + err.code); + } + + // Extract the crashing process version, because the version might not match + // if multiple VS Codes are running with different extension versions. + let binaryVersion: string = ""; + const startVersion: number = data.indexOf("Version:"); + if (startVersion >= 0) { + data = data.substring(startVersion); + const binaryVersionMatches: string[] | null = data.match(/^Version:\s*(\d*\.\d*\.\d*\.\d*|\d)/); + binaryVersion = binaryVersionMatches && binaryVersionMatches.length > 1 ? binaryVersionMatches[1] : ""; + } + + // Extract any message indicating missing dynamically loaded symbols. + let dynamicLoadError: string = ""; + const dynamicLoadErrorStart: string = "Dyld Error Message:"; + const startDynamicLoadError: number = data.indexOf(dynamicLoadErrorStart); + if (startDynamicLoadError >= 0) { + // Scan until the next blank line. + const dynamicLoadErrorEnd: string = "\n\n"; + const endDynamicLoadError: number = data.indexOf(dynamicLoadErrorEnd, startDynamicLoadError); + if (endDynamicLoadError >= 0) { + dynamicLoadError = data.substring(startDynamicLoadError, endDynamicLoadError); + if (dynamicLoadError.includes("/")) { + dynamicLoadError = ""; + } + dynamicLoadError += "\n\n"; + } + } + + // Extract the crashing thread's call stack. + const crashStart: string = " Crashed:"; + let startCrash: number = data.indexOf(crashStart); + if (startCrash < 0) { + return logMacCrashTelemetry(dynamicLoadError + "No crash start"); + } + startCrash += crashStart.length + 1; // Skip past crashStart. + let endCrash: number = data.indexOf("Thread ", startCrash); + if (endCrash < 0) { + endCrash = data.length - 1; // Not expected, but just in case. + } + if (endCrash <= startCrash) { + return logMacCrashTelemetry(dynamicLoadError + "No crash end"); + } + data = data.substring(startCrash, endCrash); + + // Get rid of the memory addresses (which breaks being able get a hit count for each crash call stack). + data = data.replace(/0x................ /g, ""); + data = data.replace(/0x1........ \+ 0/g, ""); + + // Get rid of the process names on each line and just add it to the start. + const processNames: string[] = ["cpptools-srv", "cpptools-wordexp", "cpptools", + // Since only crash logs that start with "cpptools" are reported, the cases below would only occur + // if the crash were to happen before the new process had fully started and renamed itself. + "clang-tidy", "clang-format", "clang", "gcc"]; + let processNameFound: boolean = false; + for (const processName of processNames) { + if (data.includes(processName)) { + data = data.replace(new RegExp(processName + "\\s+", "g"), ""); + data = `${processName}\t${binaryVersion}\n${data}`; + processNameFound = true; + break; + } + } + if (!processNameFound) { + // Not expected, but just in case a new binary gets added. + // Warning: Don't use ??? because that is checked below. + data = `cpptools??\t${binaryVersion}\n${data}`; + } + + // Remove runtime lines because they can be different on different machines. + const lines: string[] = data.split("\n"); + data = ""; + lines.forEach((line: string) => { + if (!line.includes(".dylib") && !line.includes("???")) { + line = line.replace(/^\d+\s+/, ""); // Remove from the start of the line. + line = line.replace(/std::__1::/g, "std::"); // __1:: is not helpful. + if (line.includes("/")) { + data += "\n"; + } else { + data += line + "\n"; + } + } + }); + data = data.trimRight(); + + // Prepend the dynamic load error. + data = dynamicLoadError + data; + + if (data.length > 8192) { // The API has an 8k limit. + data = data.substring(0, 8189) + "..."; + } + + logMacCrashTelemetry(data); +} + +async function handleCrashFileRead(crashDirectory: string, crashFile: string, crashDate: Date, err: NodeJS.ErrnoException | undefined | null, data: string): Promise { + if (err) { + if (err.code === "ENOENT") { + return; // ignore known issue + } + return logCppCrashTelemetry("readFile: " + err.code); + } + + const lines: string[] = data.split("\n"); + let addressData: string = ".\n."; + const isCppToolsSrv: boolean = crashFile.startsWith("cpptools-srv"); + const telemetryHeader: string = (isCppToolsSrv ? "cpptools-srv.txt" : crashFile) + "\n"; + const filtPath: string | null = which.sync("c++filt", { nothrow: true }); + const isMac: boolean = process.platform === "darwin"; + const startStr: string = isMac ? " _" : "<"; + const offsetStr: string = isMac ? " + " : "+"; + const endOffsetStr: string = isMac ? " " : " <"; + const dotStr: string = "\n…"; + let signalType: string; + if (lines[0].startsWith("SIG")) { + signalType = lines[0]; + } else { + // The signal type may fail to be written. + signalType = "SIG-??\n"; // Intentionally different from SIG-? from cpptools. + } + let crashCallStack: string = ""; + let validFrameFound: boolean = false; + for (let lineNum: number = 0; lineNum < lines.length - 3; ++lineNum) { // skip last lines + const line: string = lines[lineNum]; + const startPos: number = line.indexOf(startStr); + if (startPos === -1 || line[startPos + (isMac ? 1 : 4)] === "+") { + if (!validFrameFound) { + continue; // Skip extra … at the start. + } + crashCallStack += dotStr; + const startAddressPos: number = line.indexOf("0x"); + const endAddressPos: number = line.indexOf(endOffsetStr, startAddressPos + 2); + addressData += "\n"; + if (startAddressPos === -1 || endAddressPos === -1 || startAddressPos >= endAddressPos) { + addressData += "Unexpected offset"; + } else { + addressData += line.substring(startAddressPos, endAddressPos); + } + continue; + } + const offsetPos: number = line.indexOf(offsetStr, startPos + startStr.length); + if (offsetPos === -1) { + crashCallStack += "\nMissing offsetStr"; + addressData += "\n"; + continue; // unexpected + } + const startPos2: number = startPos + 1; + let funcStr: string = line.substring(startPos2, offsetPos); + if (filtPath && filtPath.length !== 0) { + let ret: util.ProcessReturnType | undefined = await util.spawnChildProcess(filtPath, ["--no-strip-underscore", funcStr], undefined, true).catch(logAndReturn.undefined); + if (ret?.output === funcStr) { + ret = await util.spawnChildProcess(filtPath, [funcStr], undefined, true).catch(logAndReturn.undefined); + } + if (ret !== undefined && ret.succeeded) { + funcStr = ret.output; + funcStr = funcStr.replace(/std::(?:__1|__cxx11)/g, "std"); // simplify std namespaces. + funcStr = funcStr.replace(/std::basic_/g, "std::"); + funcStr = funcStr.replace(/ >/g, ">"); + funcStr = funcStr.replace(/, std::(?:allocator|char_traits)/g, ""); + funcStr = funcStr.replace(//g, ""); + funcStr = funcStr.replace(/, std::allocator/g, ""); + } + } + if (funcStr.includes("/")) { + funcStr = ""; + } else if (!validFrameFound && (funcStr.startsWith("crash_handler(") || funcStr.startsWith("_sigtramp"))) { + continue; // Skip these on early frames. + } + validFrameFound = true; + crashCallStack += "\n"; + addressData += "\n"; + crashCallStack += funcStr + offsetStr; + const offsetPos2: number = offsetPos + offsetStr.length; + if (isMac) { + const pendingOffset: string = line.substring(offsetPos2); + if (!pendingOffset.includes("/")) { + crashCallStack += pendingOffset; + } + const startAddressPos: number = line.indexOf("0x"); + if (startAddressPos === -1 || startAddressPos >= startPos) { + // unexpected + crashCallStack += ""; + continue; + } + addressData += `${line.substring(startAddressPos, startPos)}`; + } else { + const endPos: number = line.indexOf(">", offsetPos2); + if (endPos === -1) { + crashCallStack += " >"; + continue; // unexpected + } + const pendingOffset: string = line.substring(offsetPos2, endPos); + if (!pendingOffset.includes("/")) { + crashCallStack += pendingOffset; + } + } + } + + if (crashCallStack !== prevCppCrashCallStackData) { + prevCppCrashCallStackData = crashCallStack; + + if (lines.length >= 6 && util.getLoggingLevel() >= 1) { + getCrashCallStacksChannel().appendLine(`\n${isCppToolsSrv ? "cpptools-srv" : "cpptools"}\n${crashDate.toLocaleString()}\n${signalType}${crashCallStack}`); + } + } + + data = telemetryHeader + signalType + crashCallStack; + + if (data.length > 8192) { // The API has an 8k limit. + data = data.substring(0, 8191) + "…"; + } + + logCppCrashTelemetry(data, addressData); + + await util.deleteFile(path.resolve(crashDirectory, crashFile)).catch(logAndReturn.undefined); + if (crashFile === "cpptools.txt") { + void util.deleteDirectory(crashDirectory).catch(logAndReturn.undefined); + } +} + +export function deactivate(): Thenable { + clients.timeTelemetryCollector.clear(); + telemetry.logLanguageServerEvent("LanguageServerShutdown"); + clearInterval(intervalTimer); + commandDisposables.forEach(d => d.dispose()); + disposables.forEach(d => d.dispose()); + languageConfigurations.forEach(d => d.dispose()); + ui.dispose(); + if (codeActionProvider) { + codeActionProvider.dispose(); + } + return clients.dispose(); +} + +export function isFolderOpen(): boolean { + return vscode.workspace.workspaceFolders !== undefined && vscode.workspace.workspaceFolders.length > 0; +} + +export function getClients(): ClientCollection { + return clients; +} + +export function getActiveClient(): Client { + return clients.ActiveClient; +} + +export function UpdateInsidersAccess(): void { + let installPrerelease: boolean = false; + + // Only move them to the new prerelease mechanism if using updateChannel of Insiders. + const settings: CppSettings = new CppSettings(); + const migratedInsiders: PersistentState = new PersistentState("CPP.migratedInsiders", false); + if (settings.updateChannel === "Insiders") { + // Don't do anything while the user has autoUpdate disabled, so we do not cause the extension to be updated. + if (!migratedInsiders.Value && vscode.workspace.getConfiguration("extensions", null).get("autoUpdate")) { + installPrerelease = true; + migratedInsiders.Value = true; + } + } else { + // Reset persistent value, so we register again if they switch to "Insiders" again. + if (migratedInsiders.Value) { + migratedInsiders.Value = false; + } + } + + // Mitigate an issue with VS Code not recognizing a programmatically installed VSIX as Prerelease. + // If using VS Code Insiders, and updateChannel is not explicitly set, default to Prerelease. + // Only do this once. If the user manually switches to Release, we don't want to switch them back to Prerelease again. + if (util.isVsCodeInsiders()) { + const insidersMitigationDone: PersistentState = new PersistentState("CPP.insidersMitigationDone", false); + if (!insidersMitigationDone.Value) { + if (vscode.workspace.getConfiguration("extensions", null).get("autoUpdate")) { + if (settings.getStringWithUndefinedDefault("updateChannel") === undefined) { + installPrerelease = true; + } + } + insidersMitigationDone.Value = true; + } + } + + if (installPrerelease) { + void vscode.commands.executeCommand("workbench.extensions.installExtension", "ms-vscode.cpptools", { installPreReleaseVersion: true }).then(undefined, logAndReturn.undefined); + } +} + +export async function preReleaseCheck(): Promise { + const displayedPreReleasePrompt: PersistentState = new PersistentState("CPP.displayedPreReleasePrompt", false); + const isOnPreRelease: PersistentState = new PersistentState("CPP.isOnPreRelease", false); + + if (util.getCppToolsTargetPopulation() === TargetPopulation.Insiders) { + isOnPreRelease.Value = true; + return; + } + + // First we need to make sure the user isn't already on a pre-release version and hasn't dismissed this prompt before. + if (!isOnPreRelease.Value && !displayedPreReleasePrompt.Value && util.getCppToolsTargetPopulation() === TargetPopulation.Public) { + // Get the info on the latest version from the marketplace to check if there is a pre-release version available. + const response = await fetch('https://marketplace.visualstudio.com/_apis/public/gallery/extensionquery', { + method: 'POST', + headers: { + Accept: 'application/json; api-version=3.0-preview', + 'Content-Type': 'application/json', + 'User-Agent': 'vscode-cpptools' + }, + body: '{"filters": [{"criteria": [{"filterType": 7, "value": "ms-vscode.cpptools"}]}], "flags": 529}' + }).catch(logAndReturn.undefined); + + telemetry.logLanguageServerEvent("marketplaceFetch", undefined, { status: response?.status ?? 0 }); + + const data: any = await response?.json().catch(logAndReturn.undefined); + + const preReleaseAvailable = data?.results[0].extensions[0].versions[0].properties.some((e: object) => Object.values(e).includes("Microsoft.VisualStudio.Code.PreRelease")); + + // If the user isn't on the pre-release version, but one is available, prompt them to install it. + if (preReleaseAvailable) { + displayedPreReleasePrompt.Value = true; + const message: string = localize("prerelease.message", "A pre-release version of the C/C++ extension is available. Would you like to switch to it?"); + const yes: string = localize("yes.button", "Yes"); + const no: string = localize("no.button", "No"); + void vscode.window.showInformationMessage(message, yes, no).then((selection) => { + if (selection === yes) { + void vscode.commands.executeCommand("workbench.extensions.installExtension", "ms-vscode.cpptools", { installPreReleaseVersion: true }).then(undefined, logAndReturn.undefined); + } + }); + } + } +} diff --git a/Extension/src/LanguageServer/lmTool.ts b/Extension/src/LanguageServer/lmTool.ts index c3fad8b6eb..38c1e5b154 100644 --- a/Extension/src/LanguageServer/lmTool.ts +++ b/Extension/src/LanguageServer/lmTool.ts @@ -7,7 +7,7 @@ import * as vscode from 'vscode'; import { localize } from 'vscode-nls'; import * as util from '../common'; -import * as logger from '../logger'; +import { getOutputChannelLogger } from '../logger'; import * as telemetry from '../telemetry'; import { ChatContextResult } from './client'; import { getClients } from './extension'; @@ -106,7 +106,7 @@ export class CppConfigurationLanguageModelTool implements vscode.LanguageModelTo private async reportError(): Promise { try { - logger.getOutputChannelLogger().appendLine(localize("copilot.cppcontext.error", "Error while retrieving the #cpp context.")); + getOutputChannelLogger().appendLine(localize("copilot.cppcontext.error", "Error while retrieving the #cpp context.")); } catch { // Intentionally swallow any exception. diff --git a/Extension/src/LanguageServer/references.ts b/Extension/src/LanguageServer/references.ts index 0fc3d2fbb6..ba541c1c64 100644 --- a/Extension/src/LanguageServer/references.ts +++ b/Extension/src/LanguageServer/references.ts @@ -8,7 +8,7 @@ import * as vscode from 'vscode'; import { Position, TextDocumentIdentifier } from 'vscode-languageclient'; import * as nls from 'vscode-nls'; import * as util from '../common'; -import * as logger from '../logger'; +import { getOutputChannel } from '../logger'; import * as telemetry from '../telemetry'; import { DefaultClient } from './client'; import { PersistentState } from './persistentState'; @@ -478,7 +478,7 @@ export class ReferencesManager { this.referencesChannel.show(true); } } else if (this.client.ReferencesCommandMode === ReferencesCommandMode.Find) { - const logChannel: vscode.OutputChannel = logger.getOutputChannel(); + const logChannel: vscode.OutputChannel = getOutputChannel(); logChannel.appendLine(msg); logChannel.appendLine(""); logChannel.show(true); diff --git a/Extension/src/common.ts b/Extension/src/common.ts index dbd0bcdd63..d2cf7b513b 100644 --- a/Extension/src/common.ts +++ b/Extension/src/common.ts @@ -1,1820 +1,1817 @@ -/* -------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All Rights Reserved. - * See 'LICENSE' in the project root for license information. - * ------------------------------------------------------------------------------------------ */ - -import * as assert from 'assert'; -import * as child_process from 'child_process'; -import * as jsonc from 'comment-json'; -import * as fs from 'fs'; -import * as os from 'os'; -import * as path from 'path'; -import * as tmp from 'tmp'; -import * as vscode from 'vscode'; -import { DocumentFilter, Range } from 'vscode-languageclient'; -import * as nls from 'vscode-nls'; -import { TargetPopulation } from 'vscode-tas-client'; -import * as which from "which"; -import { ManualPromise } from './Utility/Async/manualPromise'; -import { isWindows } from './constants'; -import { getOutputChannelLogger, showOutputChannel } from './logger'; -import { PlatformInformation } from './platform'; -import * as Telemetry from './telemetry'; - -nls.config({ messageFormat: nls.MessageFormat.bundle, bundleFormat: nls.BundleFormat.standalone })(); -const localize: nls.LocalizeFunc = nls.loadMessageBundle(); -export const failedToParseJson: string = localize("failed.to.parse.json", "Failed to parse json file, possibly due to comments or trailing commas."); - -export type Mutable = { - // eslint-disable-next-line @typescript-eslint/array-type - -readonly [P in keyof T]: T[P] extends ReadonlyArray ? Mutable[] : Mutable -}; - -export let extensionPath: string; -export let extensionContext: vscode.ExtensionContext | undefined; -export function setExtensionContext(context: vscode.ExtensionContext): void { - extensionContext = context; - extensionPath = extensionContext.extensionPath; -} - -export function setExtensionPath(path: string): void { - extensionPath = path; -} - -let cachedClangFormatPath: string | undefined; -export function getCachedClangFormatPath(): string | undefined { - return cachedClangFormatPath; -} - -export function setCachedClangFormatPath(path: string): void { - cachedClangFormatPath = path; -} - -let cachedClangTidyPath: string | undefined; -export function getCachedClangTidyPath(): string | undefined { - return cachedClangTidyPath; -} - -export function setCachedClangTidyPath(path: string): void { - cachedClangTidyPath = path; -} - -// Use this package.json to read values -export const packageJson: any = vscode.extensions.getExtension("ms-vscode.cpptools")?.packageJSON; - -// Use getRawSetting to get subcategorized settings from package.json. -// This prevents having to iterate every time we search. -let flattenedPackageJson: Map; -export function getRawSetting(key: string, breakIfMissing: boolean = false): any { - if (flattenedPackageJson === undefined) { - flattenedPackageJson = new Map(); - for (const subheading of packageJson.contributes.configuration) { - for (const setting in subheading.properties) { - flattenedPackageJson.set(setting, subheading.properties[setting]); - } - } - } - const result = flattenedPackageJson.get(key); - if (result === undefined && breakIfMissing) { - // eslint-disable-next-line no-debugger - debugger; // The setting does not exist in package.json. Check the `key`. - } - return result; -} - -export async function getRawJson(path: string | undefined): Promise { - if (!path) { - return {}; - } - const fileExists: boolean = await checkFileExists(path); - if (!fileExists) { - return {}; - } - - const fileContents: string = await readFileText(path); - let rawElement: any = {}; - try { - rawElement = jsonc.parse(fileContents, undefined, true); - } catch (error) { - throw new Error(failedToParseJson); - } - return rawElement; -} - -// This function is used to stringify the rawPackageJson. -// Do not use with util.packageJson or else the expanded -// package.json will be written back. -export function stringifyPackageJson(packageJson: string): string { - return JSON.stringify(packageJson, null, 2); -} - -export function getExtensionFilePath(extensionfile: string): string { - return path.resolve(extensionPath, extensionfile); -} - -export function getPackageJsonPath(): string { - return getExtensionFilePath("package.json"); -} - -export function getJsonPath(jsonFilaName: string, workspaceFolder?: vscode.WorkspaceFolder): string | undefined { - const editor: vscode.TextEditor | undefined = vscode.window.activeTextEditor; - if (!editor) { - return undefined; - } - const folder: vscode.WorkspaceFolder | undefined = workspaceFolder ? workspaceFolder : vscode.workspace.getWorkspaceFolder(editor.document.uri); - if (!folder) { - return undefined; - } - return path.join(folder.uri.fsPath, ".vscode", jsonFilaName); -} - -export function getVcpkgPathDescriptorFile(): string { - if (process.platform === 'win32') { - const pathPrefix: string | undefined = process.env.LOCALAPPDATA; - if (!pathPrefix) { - throw new Error("Unable to read process.env.LOCALAPPDATA"); - } - return path.join(pathPrefix, "vcpkg/vcpkg.path.txt"); - } else { - const pathPrefix: string = os.homedir(); - return path.join(pathPrefix, ".vcpkg/vcpkg.path.txt"); - } -} - -let vcpkgRoot: string | undefined; -export function getVcpkgRoot(): string { - if (!vcpkgRoot && vcpkgRoot !== "") { - vcpkgRoot = ""; - // Check for vcpkg instance. - if (fs.existsSync(getVcpkgPathDescriptorFile())) { - let vcpkgRootTemp: string = fs.readFileSync(getVcpkgPathDescriptorFile()).toString(); - vcpkgRootTemp = vcpkgRootTemp.trim(); - if (fs.existsSync(vcpkgRootTemp)) { - vcpkgRoot = path.join(vcpkgRootTemp, "/installed").replace(/\\/g, "/"); - } - } - } - return vcpkgRoot; -} - -/** - * This is a fuzzy determination of whether a uri represents a header file. - * For the purposes of this function, a header file has no extension, or an extension that begins with the letter 'h'. - * @param document The document to check. - */ -export function isHeaderFile(uri: vscode.Uri): boolean { - const fileExt: string = path.extname(uri.fsPath); - const fileExtLower: string = fileExt.toLowerCase(); - return !fileExt || [".cuh", ".hpp", ".hh", ".hxx", ".h++", ".hp", ".h", ".ii", ".inl", ".idl", ""].some(ext => fileExtLower === ext); -} - -export function isCppFile(uri: vscode.Uri): boolean { - const fileExt: string = path.extname(uri.fsPath); - const fileExtLower: string = fileExt.toLowerCase(); - return (fileExt === ".C") || [".cu", ".cpp", ".cc", ".cxx", ".c++", ".cp", ".ino", ".ipp", ".tcc"].some(ext => fileExtLower === ext); -} - -export function isCFile(uri: vscode.Uri): boolean { - const fileExt: string = path.extname(uri.fsPath); - const fileExtLower: string = fileExt.toLowerCase(); - return (fileExt === ".C") || fileExtLower === ".c"; -} - -export function isCppOrCFile(uri: vscode.Uri | undefined): boolean { - if (!uri) { - return false; - } - return isCppFile(uri) || isCFile(uri); -} - -export function isFolderOpen(uri: vscode.Uri): boolean { - const folder: vscode.WorkspaceFolder | undefined = vscode.workspace.getWorkspaceFolder(uri); - return folder ? true : false; -} - -export function isEditorFileCpp(file: string): boolean { - const editor: vscode.TextEditor | undefined = vscode.window.visibleTextEditors.find(e => e.document.uri.toString() === file); - if (!editor) { - return false; - } - return editor.document.languageId === "cpp"; -} - -// If it's C, C++, or Cuda. -export function isCpp(document: vscode.TextDocument): boolean { - return document.uri.scheme === "file" && - (document.languageId === "c" || document.languageId === "cpp" || document.languageId === "cuda-cpp"); -} - -export function isCppPropertiesJson(document: vscode.TextDocument): boolean { - return document.uri.scheme === "file" && (document.languageId === "json" || document.languageId === "jsonc") && - document.fileName.endsWith("c_cpp_properties.json"); -} -let isWorkspaceCpp: boolean = false; -export function setWorkspaceIsCpp(): void { - if (!isWorkspaceCpp) { - isWorkspaceCpp = true; - } -} - -export function getWorkspaceIsCpp(): boolean { - return isWorkspaceCpp; -} - -export function isCppOrRelated(document: vscode.TextDocument): boolean { - return isCpp(document) || isCppPropertiesJson(document) || (document.uri.scheme === "output" && document.uri.fsPath.startsWith("extension-output-ms-vscode.cpptools")) || - (isWorkspaceCpp && (document.languageId === "json" || document.languageId === "jsonc") && - ((document.fileName.endsWith("settings.json") && (document.uri.scheme === "file" || document.uri.scheme === "vscode-userdata")) || - (document.uri.scheme === "file" && document.fileName.endsWith(".code-workspace")))); -} - -let isExtensionNotReadyPromptDisplayed: boolean = false; -export const extensionNotReadyString: string = localize("extension.not.ready", 'The C/C++ extension is still installing. See the output window for more information.'); - -export function displayExtensionNotReadyPrompt(): void { - if (!isExtensionNotReadyPromptDisplayed) { - isExtensionNotReadyPromptDisplayed = true; - showOutputChannel(); - - void getOutputChannelLogger().showInformationMessage(extensionNotReadyString).then( - () => { isExtensionNotReadyPromptDisplayed = false; }, - () => { isExtensionNotReadyPromptDisplayed = false; } - ); - } -} - -// This Progress global state tracks how far users are able to get before getting blocked. -// Users start with a progress of 0 and it increases as they get further along in using the tool. -// This eliminates noise/problems due to re-installs, terminated installs that don't send errors, -// errors followed by workarounds that lead to success, etc. -const progressInstallSuccess: number = 100; -const progressExecutableStarted: number = 150; -const progressExecutableSuccess: number = 200; -const progressParseRootSuccess: number = 300; -const progressIntelliSenseNoSquiggles: number = 1000; -// Might add more IntelliSense progress measurements later. -// IntelliSense progress is separate from the install progress, because parse root can occur afterwards. - -const installProgressStr: string = "CPP." + packageJson.version + ".Progress"; -const intelliSenseProgressStr: string = "CPP." + packageJson.version + ".IntelliSenseProgress"; - -export function getProgress(): number { - return extensionContext ? extensionContext.globalState.get(installProgressStr, -1) : -1; -} - -export function getIntelliSenseProgress(): number { - return extensionContext ? extensionContext.globalState.get(intelliSenseProgressStr, -1) : -1; -} - -export function setProgress(progress: number): void { - if (extensionContext && getProgress() < progress) { - void extensionContext.globalState.update(installProgressStr, progress); - const telemetryProperties: Record = {}; - let progressName: string | undefined; - switch (progress) { - case 0: progressName = "install started"; break; - case progressInstallSuccess: progressName = "install succeeded"; break; - case progressExecutableStarted: progressName = "executable started"; break; - case progressExecutableSuccess: progressName = "executable succeeded"; break; - case progressParseRootSuccess: progressName = "parse root succeeded"; break; - } - if (progressName) { - telemetryProperties.progress = progressName; - } - Telemetry.logDebuggerEvent("progress", telemetryProperties); - } -} - -export function setIntelliSenseProgress(progress: number): void { - if (extensionContext && getIntelliSenseProgress() < progress) { - void extensionContext.globalState.update(intelliSenseProgressStr, progress); - const telemetryProperties: Record = {}; - let progressName: string | undefined; - switch (progress) { - case progressIntelliSenseNoSquiggles: progressName = "IntelliSense no squiggles"; break; - } - if (progressName) { - telemetryProperties.progress = progressName; - } - Telemetry.logDebuggerEvent("progress", telemetryProperties); - } -} - -export function getProgressInstallSuccess(): number { return progressInstallSuccess; } // Download/install was successful (i.e. not blocked by component acquisition). -export function getProgressExecutableStarted(): number { return progressExecutableStarted; } // The extension was activated and starting the executable was attempted. -export function getProgressExecutableSuccess(): number { return progressExecutableSuccess; } // Starting the exe was successful (i.e. not blocked by 32-bit or glibc < 2.18 on Linux) -export function getProgressParseRootSuccess(): number { return progressParseRootSuccess; } // Parse root was successful (i.e. not blocked by processing taking too long). -export function getProgressIntelliSenseNoSquiggles(): number { return progressIntelliSenseNoSquiggles; } // IntelliSense was successful and the user got no squiggles. - -export function isUri(input: any): input is vscode.Uri { - return input && input instanceof vscode.Uri; -} - -export function isString(input: any): input is string { - return typeof input === "string"; -} - -export function isNumber(input: any): input is number { - return typeof input === "number"; -} - -export function isBoolean(input: any): input is boolean { - return typeof input === "boolean"; -} - -export function isObject(input: any): boolean { - return input !== null && typeof input === "object" && !isArray(input); -} - -export function isArray(input: any): input is any[] { - return Array.isArray(input); -} - -export function isOptionalString(input: any): input is string | undefined { - return input === undefined || isString(input); -} - -export function isArrayOfString(input: any): input is string[] { - return isArray(input) && input.every(isString); -} - -// Validates whether the given object is a valid mapping of key and value type. -// EX: {"key": true, "key2": false} should return true for keyType = string and valueType = boolean. -export function isValidMapping(value: any, isValidKey: (key: any) => boolean, isValidValue: (value: any) => boolean): value is object { - if (isObject(value)) { - return Object.entries(value).every(([key, val]) => isValidKey(key) && isValidValue(val)); - } - return false; -} - -export function isOptionalArrayOfString(input: any): input is string[] | undefined { - return input === undefined || isArrayOfString(input); -} - -export function resolveCachePath(input: string | undefined, additionalEnvironment: Record): string { - let resolvedPath: string = ""; - if (!input || input.trim() === "") { - // If no path is set, return empty string to language service process, where it will set the default path as - // Windows: %LocalAppData%/Microsoft/vscode-cpptools/ - // Linux and Mac: ~/.vscode-cpptools/ - return resolvedPath; - } - - resolvedPath = resolveVariables(input, additionalEnvironment); - return resolvedPath; -} - -export function defaultExePath(): string { - const exePath: string = path.join('${fileDirname}', '${fileBasenameNoExtension}'); - return isWindows ? exePath + '.exe' : exePath; -} - -// Pass in 'arrayResults' if a string[] result is possible and a delimited string result is undesirable. -// The string[] result will be copied into 'arrayResults'. -export function resolveVariables(input: string | undefined, additionalEnvironment?: Record, arrayResults?: string[]): string { - if (!input) { - return ""; - } - - // jsonc parser may assign a non-string object to a string. - // TODO: https://github.com/microsoft/vscode-cpptools/issues/9414 - if (!isString(input)) { - const inputAny: any = input; - input = inputAny.toString(); - return input ?? ""; - } - - // Replace environment and configuration variables. - const regexp: () => RegExp = () => /\$\{((env|config|workspaceFolder)(\.|:))?(.*?)\}/g; - let ret: string = input; - const cycleCache = new Set(); - while (!cycleCache.has(ret)) { - cycleCache.add(ret); - ret = ret.replace(regexp(), (match: string, ignored1: string, varType: string, ignored2: string, name: string) => { - // Historically, if the variable didn't have anything before the "." or ":" - // it was assumed to be an environment variable - if (!varType) { - varType = "env"; - } - let newValue: string | undefined; - switch (varType) { - case "env": { - if (additionalEnvironment) { - const v: string | string[] | undefined = additionalEnvironment[name]; - if (isString(v)) { - newValue = v; - } else if (input === match && isArrayOfString(v)) { - if (arrayResults !== undefined) { - arrayResults.push(...v); - newValue = ""; - break; - } else { - newValue = v.join(path.delimiter); - } - } - } - if (newValue === undefined) { - newValue = process.env[name]; - } - break; - } - case "config": { - const config: vscode.WorkspaceConfiguration = vscode.workspace.getConfiguration(); - if (config) { - newValue = config.get(name); - } - break; - } - case "workspaceFolder": { - // Only replace ${workspaceFolder:name} variables for now. - // We may consider doing replacement of ${workspaceFolder} here later, but we would have to update the language server and also - // intercept messages with paths in them and add the ${workspaceFolder} variable back in (e.g. for light bulb suggestions) - if (name && vscode.workspace && vscode.workspace.workspaceFolders) { - const folder: vscode.WorkspaceFolder | undefined = vscode.workspace.workspaceFolders.find(folder => folder.name.toLocaleLowerCase() === name.toLocaleLowerCase()); - if (folder) { - newValue = folder.uri.fsPath; - } - } - break; - } - default: { assert.fail("unknown varType matched"); } - } - return newValue !== undefined ? newValue : match; - }); - } - - return resolveHome(ret); -} - -export function resolveVariablesArray(variables: string[] | undefined, additionalEnvironment?: Record): string[] { - let result: string[] = []; - if (variables) { - variables.forEach(variable => { - const variablesResolved: string[] = []; - const variableResolved: string = resolveVariables(variable, additionalEnvironment, variablesResolved); - result = result.concat(variablesResolved.length === 0 ? variableResolved : variablesResolved); - }); - } - return result; -} - -// Resolve '~' at the start of the path. -export function resolveHome(filePath: string): string { - return filePath.replace(/^\~/g, os.homedir()); -} - -export function asFolder(uri: vscode.Uri): string { - let result: string = uri.toString(); - if (!result.endsWith('/')) { - result += '/'; - } - return result; -} - -/** - * get the default open command for the current platform - */ -export function getOpenCommand(): string { - if (os.platform() === 'win32') { - return 'explorer'; - } else if (os.platform() === 'darwin') { - return '/usr/bin/open'; - } else { - return '/usr/bin/xdg-open'; - } -} - -export function getDebugAdaptersPath(file: string): string { - return path.resolve(getExtensionFilePath("debugAdapters"), file); -} - -export async function fsStat(filePath: fs.PathLike): Promise { - let stats: fs.Stats | undefined; - try { - stats = await fs.promises.stat(filePath); - } catch (e) { - // File doesn't exist - return undefined; - } - return stats; -} - -export async function checkPathExists(filePath: string): Promise { - return !!await fsStat(filePath); -} - -/** Test whether a file exists */ -export async function checkFileExists(filePath: string): Promise { - const stats: fs.Stats | undefined = await fsStat(filePath); - return !!stats && stats.isFile(); -} - -/** Test whether a file exists */ -export async function checkExecutableWithoutExtensionExists(filePath: string): Promise { - if (await checkFileExists(filePath)) { - return true; - } - if (os.platform() === 'win32') { - if (filePath.length > 4) { - const possibleExtension: string = filePath.substring(filePath.length - 4).toLowerCase(); - if (possibleExtension === ".exe" || possibleExtension === ".cmd" || possibleExtension === ".bat") { - return false; - } - } - if (await checkFileExists(filePath + ".exe")) { - return true; - } - if (await checkFileExists(filePath + ".cmd")) { - return true; - } - if (await checkFileExists(filePath + ".bat")) { - return true; - } - } - return false; -} - -/** Test whether a directory exists */ -export async function checkDirectoryExists(dirPath: string): Promise { - const stats: fs.Stats | undefined = await fsStat(dirPath); - return !!stats && stats.isDirectory(); -} - -export function createDirIfNotExistsSync(filePath: string | undefined): void { - if (!filePath) { - return; - } - const dirPath: string = path.dirname(filePath); - if (!checkDirectoryExistsSync(dirPath)) { - fs.mkdirSync(dirPath, { recursive: true }); - } -} - -export function checkFileExistsSync(filePath: string): boolean { - try { - return fs.statSync(filePath).isFile(); - } catch (e) { - return false; - } -} - -export function checkExecutableWithoutExtensionExistsSync(filePath: string): boolean { - if (checkFileExistsSync(filePath)) { - return true; - } - if (os.platform() === 'win32') { - if (filePath.length > 4) { - const possibleExtension: string = filePath.substring(filePath.length - 4).toLowerCase(); - if (possibleExtension === ".exe" || possibleExtension === ".cmd" || possibleExtension === ".bat") { - return false; - } - } - if (checkFileExistsSync(filePath + ".exe")) { - return true; - } - if (checkFileExistsSync(filePath + ".cmd")) { - return true; - } - if (checkFileExistsSync(filePath + ".bat")) { - return true; - } - } - return false; -} - -/** Test whether a directory exists */ -export function checkDirectoryExistsSync(dirPath: string): boolean { - try { - return fs.statSync(dirPath).isDirectory(); - } catch (e) { - return false; - } -} - -/** Test whether a relative path exists */ -export function checkPathExistsSync(path: string, relativePath: string, _isWindows: boolean, isCompilerPath: boolean): { pathExists: boolean; path: string } { - let pathExists: boolean = true; - const existsWithExeAdded: (path: string) => boolean = (path: string) => isCompilerPath && _isWindows && fs.existsSync(path + ".exe"); - if (!fs.existsSync(path)) { - if (existsWithExeAdded(path)) { - path += ".exe"; - } else if (!relativePath) { - pathExists = false; - } else { - // Check again for a relative path. - relativePath = relativePath + path; - if (!fs.existsSync(relativePath)) { - if (existsWithExeAdded(path)) { - path += ".exe"; - } else { - pathExists = false; - } - } else { - path = relativePath; - } - } - } - return { pathExists, path }; -} - -/** Read the files in a directory */ -export function readDir(dirPath: string): Promise { - return new Promise((resolve) => { - fs.readdir(dirPath, (err, list) => { - resolve(list); - }); - }); -} - -/** Reads the content of a text file */ -export function readFileText(filePath: string, encoding: BufferEncoding = "utf8"): Promise { - return new Promise((resolve, reject) => { - fs.readFile(filePath, { encoding }, (err: any, data: any) => { - if (err) { - reject(err); - } else { - resolve(data); - } - }); - }); -} - -/** Writes content to a text file */ -export function writeFileText(filePath: string, content: string, encoding: BufferEncoding = "utf8"): Promise { - const folders: string[] = filePath.split(path.sep).slice(0, -1); - if (folders.length) { - // create folder path if it doesn't exist - folders.reduce((previous, folder) => { - const folderPath: string = previous + path.sep + folder; - if (!fs.existsSync(folderPath)) { - fs.mkdirSync(folderPath); - } - return folderPath; - }); - } - - return new Promise((resolve, reject) => { - fs.writeFile(filePath, content, { encoding }, (err) => { - if (err) { - reject(err); - } else { - resolve(); - } - }); - }); -} - -export function deleteFile(filePath: string): Promise { - return new Promise((resolve, reject) => { - if (fs.existsSync(filePath)) { - fs.unlink(filePath, (err) => { - if (err) { - reject(err); - } else { - resolve(); - } - }); - } else { - resolve(); - } - }); -} - -export function deleteDirectory(directoryPath: string): Promise { - return new Promise((resolve, reject) => { - if (fs.existsSync(directoryPath)) { - fs.rmdir(directoryPath, (err) => { - if (err) { - reject(err); - } else { - resolve(); - } - }); - } else { - resolve(); - } - }); -} - -export function getReadmeMessage(): string { - const readmePath: string = getExtensionFilePath("README.md"); - const readmeMessage: string = localize("refer.read.me", "Please refer to {0} for troubleshooting information. Issues can be created at {1}", readmePath, "https://github.com/Microsoft/vscode-cpptools/issues"); - return readmeMessage; -} - -/** Used for diagnostics only */ -export function logToFile(message: string): void { - const logFolder: string = getExtensionFilePath("extension.log"); - fs.writeFileSync(logFolder, `${message}${os.EOL}`, { flag: 'a' }); -} - -export function execChildProcess(process: string, workingDirectory?: string, channel?: vscode.OutputChannel): Promise { - return new Promise((resolve, reject) => { - child_process.exec(process, { cwd: workingDirectory, maxBuffer: 500 * 1024 }, (error: Error | null, stdout: string, stderr: string) => { - if (channel) { - let message: string = ""; - let err: boolean = false; - if (stdout && stdout.length > 0) { - message += stdout; - } - - if (stderr && stderr.length > 0) { - message += stderr; - err = true; - } - - if (error) { - message += error.message; - err = true; - } - - if (err) { - channel.append(message); - channel.show(); - } - } - - if (error) { - reject(error); - return; - } - - if (stderr && stderr.length > 0) { - reject(new Error(stderr)); - return; - } - - resolve(stdout); - }); - }); -} - -export interface ProcessReturnType { - succeeded: boolean; - exitCode?: number | NodeJS.Signals; - output: string; - outputError: string; -} - -export async function spawnChildProcess(program: string, args: string[] = [], continueOn?: string, skipLogging?: boolean, cancellationToken?: vscode.CancellationToken): Promise { - // Do not use CppSettings to avoid circular require() - if (skipLogging === undefined || !skipLogging) { - const settings: vscode.WorkspaceConfiguration = vscode.workspace.getConfiguration("C_Cpp", null); - if (getNumericLoggingLevel(settings.get("loggingLevel")) >= 5) { - getOutputChannelLogger().appendLine(`$ ${program} ${args.join(' ')}`); - } - } - const programOutput: ProcessOutput = await spawnChildProcessImpl(program, args, continueOn, skipLogging, cancellationToken); - const exitCode: number | NodeJS.Signals | undefined = programOutput.exitCode; - if (programOutput.exitCode) { - return { succeeded: false, exitCode, outputError: programOutput.stderr, output: programOutput.stderr || programOutput.stdout || localize('process.exited', 'Process exited with code {0}', exitCode) }; - } else { - let stdout: string; - if (programOutput.stdout.length) { - // Type system doesn't work very well here, so we need call toString - stdout = programOutput.stdout; - } else { - stdout = localize('process.succeeded', 'Process executed successfully.'); - } - return { succeeded: true, exitCode, outputError: programOutput.stderr, output: stdout }; - } -} - -interface ProcessOutput { - exitCode?: number | NodeJS.Signals; - stdout: string; - stderr: string; -} - -async function spawnChildProcessImpl(program: string, args: string[], continueOn?: string, skipLogging?: boolean, cancellationToken?: vscode.CancellationToken): Promise { - const result = new ManualPromise(); - - // Do not use CppSettings to avoid circular require() - const settings: vscode.WorkspaceConfiguration = vscode.workspace.getConfiguration("C_Cpp", null); - const loggingLevel: number = (skipLogging === undefined || !skipLogging) ? getNumericLoggingLevel(settings.get("loggingLevel")) : 0; - - let proc: child_process.ChildProcess; - if (await isExecutable(program)) { - proc = child_process.spawn(`.${isWindows ? '\\' : '/'}${path.basename(program)}`, args, { shell: true, cwd: path.dirname(program) }); - } else { - proc = child_process.spawn(program, args, { shell: true }); - } - - const cancellationTokenListener: vscode.Disposable | undefined = cancellationToken?.onCancellationRequested(() => { - getOutputChannelLogger().appendLine(localize('killing.process', 'Killing process {0}', program)); - proc.kill(); - }); - - const clean = () => { - proc.removeAllListeners(); - if (cancellationTokenListener) { - cancellationTokenListener.dispose(); - } - }; - - let stdout: string = ''; - let stderr: string = ''; - if (proc.stdout) { - proc.stdout.on('data', data => { - const str: string = data.toString(); - if (loggingLevel > 0) { - getOutputChannelLogger().append(str); - } - stdout += str; - if (continueOn) { - const continueOnReg: string = escapeStringForRegex(continueOn); - if (stdout.search(continueOnReg)) { - result.resolve({ stdout: stdout.trim(), stderr: stderr.trim() }); - } - } - }); - } - if (proc.stderr) { - proc.stderr.on('data', data => stderr += data.toString()); - } - proc.on('close', (code, signal) => { - clean(); - result.resolve({ exitCode: code || signal || undefined, stdout: stdout.trim(), stderr: stderr.trim() }); - }); - proc.on('error', error => { - clean(); - result.reject(error); - }); - return result; -} - -/** - * @param permission fs file access constants: https://nodejs.org/api/fs.html#file-access-constants - */ -export function pathAccessible(filePath: string, permission: number = fs.constants.F_OK): Promise { - if (!filePath) { return Promise.resolve(false); } - return new Promise(resolve => fs.access(filePath, permission, err => resolve(!err))); -} - -export function isExecutable(file: string): Promise { - return pathAccessible(file, fs.constants.X_OK); -} - -export async function allowExecution(file: string): Promise { - if (process.platform !== 'win32') { - const exists: boolean = await checkFileExists(file); - if (exists) { - const isExec: boolean = await isExecutable(file); - if (!isExec) { - await chmodAsync(file, '755'); - } - } else { - getOutputChannelLogger().appendLine(""); - getOutputChannelLogger().appendLine(localize("warning.file.missing", "Warning: Expected file {0} is missing.", file)); - } - } -} - -export async function chmodAsync(path: fs.PathLike, mode: fs.Mode): Promise { - return new Promise((resolve, reject) => { - fs.chmod(path, mode, (err: NodeJS.ErrnoException | null) => { - if (err) { - return reject(err); - } - return resolve(); - }); - }); -} - -export function removePotentialPII(str: string): string { - const words: string[] = str.split(" "); - let result: string = ""; - for (const word of words) { - if (!word.includes(".") && !word.includes("/") && !word.includes("\\") && !word.includes(":")) { - result += word + " "; - } else { - result += "? "; - } - } - return result; -} - -export function checkDistro(platformInfo: PlatformInformation): void { - if (platformInfo.platform !== 'win32' && platformInfo.platform !== 'linux' && platformInfo.platform !== 'darwin') { - // this should never happen because VSCode doesn't run on FreeBSD - // or SunOS (the other platforms supported by node) - getOutputChannelLogger().appendLine(localize("warning.debugging.not.tested", "Warning: Debugging has not been tested for this platform.") + " " + getReadmeMessage()); - } -} - -export async function unlinkAsync(fileName: string): Promise { - return new Promise((resolve, reject) => { - fs.unlink(fileName, err => { - if (err) { - return reject(err); - } - return resolve(); - }); - }); -} - -export async function renameAsync(oldName: string, newName: string): Promise { - return new Promise((resolve, reject) => { - fs.rename(oldName, newName, err => { - if (err) { - return reject(err); - } - return resolve(); - }); - }); -} - -export async function promptForReloadWindowDueToSettingsChange(): Promise { - await promptReloadWindow(localize("reload.workspace.for.changes", "Reload the workspace for the settings change to take effect.")); -} - -export async function promptReloadWindow(message: string): Promise { - const reload: string = localize("reload.string", "Reload"); - const value: string | undefined = await vscode.window.showInformationMessage(message, reload); - if (value === reload) { - return vscode.commands.executeCommand("workbench.action.reloadWindow"); - } -} - -export function createTempFileWithPostfix(postfix: string): Promise { - return new Promise((resolve, reject) => { - tmp.file({ postfix: postfix }, (err, path, fd, cleanupCallback) => { - if (err) { - return reject(err); - } - return resolve({ name: path, fd: fd, removeCallback: cleanupCallback } as tmp.FileResult); - }); - }); -} - -function resolveWindowsEnvironmentVariables(str: string): string { - return str.replace(/%([^%]+)%/g, (withPercents, withoutPercents) => { - const found: string | undefined = process.env[withoutPercents]; - return found || withPercents; - }); -} - -function legacyExtractArgs(argsString: string): string[] { - const result: string[] = []; - let currentArg: string = ""; - let isWithinDoubleQuote: boolean = false; - let isWithinSingleQuote: boolean = false; - for (let i: number = 0; i < argsString.length; i++) { - const c: string = argsString[i]; - if (c === '\\') { - currentArg += c; - if (++i === argsString.length) { - if (currentArg !== "") { - result.push(currentArg); - } - return result; - } - currentArg += argsString[i]; - continue; - } - if (c === '"') { - if (!isWithinSingleQuote) { - isWithinDoubleQuote = !isWithinDoubleQuote; - } - } else if (c === '\'') { - // On Windows, a single quote string is not allowed to join multiple args into a single arg - if (!isWindows) { - if (!isWithinDoubleQuote) { - isWithinSingleQuote = !isWithinSingleQuote; - } - } - } else if (c === ' ') { - if (!isWithinDoubleQuote && !isWithinSingleQuote) { - if (currentArg !== "") { - result.push(currentArg); - currentArg = ""; - } - continue; - } - } - currentArg += c; - } - if (currentArg !== "") { - result.push(currentArg); - } - return result; -} - -function extractArgs(argsString: string): string[] { - argsString = argsString.trim(); - if (os.platform() === 'win32') { - argsString = resolveWindowsEnvironmentVariables(argsString); - const result: string[] = []; - let currentArg: string = ""; - let isInQuote: boolean = false; - let wasInQuote: boolean = false; - let i: number = 0; - while (i < argsString.length) { - let c: string = argsString[i]; - if (c === '\"') { - if (!isInQuote) { - isInQuote = true; - wasInQuote = true; - ++i; - continue; - } - // Need to peek at next character. - if (++i === argsString.length) { - break; - } - c = argsString[i]; - if (c !== '\"') { - isInQuote = false; - } - // Fall through. If c was a quote character, it will be added as a literal. - } - if (c === '\\') { - let backslashCount: number = 1; - let reachedEnd: boolean = true; - while (++i !== argsString.length) { - c = argsString[i]; - if (c !== '\\') { - reachedEnd = false; - break; - } - ++backslashCount; - } - const still_escaping: boolean = (backslashCount % 2) !== 0; - if (!reachedEnd && c === '\"') { - backslashCount = Math.floor(backslashCount / 2); - } - while (backslashCount--) { - currentArg += '\\'; - } - if (reachedEnd) { - break; - } - // If not still escaping and a quote was found, it needs to be handled above. - if (!still_escaping && c === '\"') { - continue; - } - // Otherwise, fall through to handle c as a literal. - } - if (c === ' ' || c === '\t' || c === '\r' || c === '\n') { - if (!isInQuote) { - if (currentArg !== "" || wasInQuote) { - wasInQuote = false; - result.push(currentArg); - currentArg = ""; - } - i++; - continue; - } - } - currentArg += c; - i++; - } - if (currentArg !== "" || wasInQuote) { - result.push(currentArg); - } - return result; - } else { - try { - const wordexpResult: any = child_process.execFileSync(getExtensionFilePath("bin/cpptools-wordexp"), [argsString], { shell: false }); - if (wordexpResult === undefined) { - return []; - } - const jsonText: string = wordexpResult.toString(); - return jsonc.parse(jsonText, undefined, true) as any; - } catch { - return []; - } - } -} - -export function isCl(compilerPath: string): boolean { - const compilerPathLowercase: string = compilerPath.toLowerCase(); - return compilerPathLowercase === "cl" || compilerPathLowercase === "cl.exe" - || compilerPathLowercase.endsWith("\\cl.exe") || compilerPathLowercase.endsWith("/cl.exe") - || compilerPathLowercase.endsWith("\\cl") || compilerPathLowercase.endsWith("/cl"); -} - -/** CompilerPathAndArgs retains original casing of text input for compiler path and args */ -export interface CompilerPathAndArgs { - compilerPath?: string | null; - compilerName: string; - compilerArgs?: string[]; - compilerArgsFromCommandLineInPath: string[]; - allCompilerArgs: string[]; -} - -export function extractCompilerPathAndArgs(useLegacyBehavior: boolean, inputCompilerPath?: string | null, compilerArgs?: string[]): CompilerPathAndArgs { - let compilerPath: string | undefined | null = inputCompilerPath; - let compilerName: string = ""; - let compilerArgsFromCommandLineInPath: string[] = []; - if (compilerPath) { - compilerPath = compilerPath.trim(); - if (isCl(compilerPath) || checkExecutableWithoutExtensionExistsSync(compilerPath)) { - // If the path ends with cl, or if a file is found at that path, accept it without further validation. - compilerName = path.basename(compilerPath); - } else if (compilerPath.startsWith("\"") || (os.platform() !== 'win32' && compilerPath.startsWith("'"))) { - // If the string starts with a quote, treat it as a command line. - // Otherwise, a path with a leading quote would not be valid. - if (useLegacyBehavior) { - compilerArgsFromCommandLineInPath = legacyExtractArgs(compilerPath); - if (compilerArgsFromCommandLineInPath.length > 0) { - compilerPath = compilerArgsFromCommandLineInPath.shift(); - if (compilerPath) { - // Try to trim quotes from compiler path. - const tempCompilerPath: string[] | undefined = extractArgs(compilerPath); - if (tempCompilerPath && compilerPath.length > 0) { - compilerPath = tempCompilerPath[0]; - } - compilerName = path.basename(compilerPath); - } - } - } else { - compilerArgsFromCommandLineInPath = extractArgs(compilerPath); - if (compilerArgsFromCommandLineInPath.length > 0) { - compilerPath = compilerArgsFromCommandLineInPath.shift(); - if (compilerPath) { - compilerName = path.basename(compilerPath); - } - } - } - } else { - const spaceStart: number = compilerPath.lastIndexOf(" "); - if (spaceStart !== -1) { - // There is no leading quote, but a space suggests it might be a command line. - // Try processing it as a command line, and validate that by checking for the executable. - const potentialArgs: string[] = useLegacyBehavior ? legacyExtractArgs(compilerPath) : extractArgs(compilerPath); - let potentialCompilerPath: string | undefined = potentialArgs.shift(); - if (useLegacyBehavior) { - if (potentialCompilerPath) { - const tempCompilerPath: string[] | undefined = extractArgs(potentialCompilerPath); - if (tempCompilerPath && compilerPath.length > 0) { - potentialCompilerPath = tempCompilerPath[0]; - } - } - } - if (potentialCompilerPath) { - if (isCl(potentialCompilerPath) || checkExecutableWithoutExtensionExistsSync(potentialCompilerPath)) { - compilerArgsFromCommandLineInPath = potentialArgs; - compilerPath = potentialCompilerPath; - compilerName = path.basename(compilerPath); - } - } - } - } - } - let allCompilerArgs: string[] = !compilerArgs ? [] : compilerArgs; - allCompilerArgs = allCompilerArgs.concat(compilerArgsFromCommandLineInPath); - return { compilerPath, compilerName, compilerArgs, compilerArgsFromCommandLineInPath, allCompilerArgs }; -} - -export function escapeForSquiggles(s: string): string { - // Replace all \ with \\, except for \" - // Otherwise, the JSON.parse result will have the \ missing. - let newResults: string = ""; - let lastWasBackslash: boolean = false; - let lastBackslashWasEscaped: boolean = false; - for (let i: number = 0; i < s.length; i++) { - if (s[i] === '\\') { - if (lastWasBackslash) { - newResults += "\\"; - lastBackslashWasEscaped = !lastBackslashWasEscaped; - } else { - lastBackslashWasEscaped = false; - } - newResults += "\\"; - lastWasBackslash = true; - } else { - if (lastWasBackslash && (lastBackslashWasEscaped || (s[i] !== '"'))) { - newResults += "\\"; - } - lastWasBackslash = false; - lastBackslashWasEscaped = false; - newResults += s[i]; - } - } - if (lastWasBackslash) { - newResults += "\\"; - } - return newResults; -} - -export function getSenderType(sender?: any): string { - if (isString(sender)) { - return sender; - } else if (isUri(sender)) { - return 'contextMenu'; - } - return 'commandPalette'; -} - -function decodeUCS16(input: string): number[] { - const output: number[] = []; - let counter: number = 0; - const length: number = input.length; - let value: number; - let extra: number; - while (counter < length) { - value = input.charCodeAt(counter++); - // eslint-disable-next-line no-bitwise - if ((value & 0xF800) === 0xD800 && counter < length) { - // high surrogate, and there is a next character - extra = input.charCodeAt(counter++); - // eslint-disable-next-line no-bitwise - if ((extra & 0xFC00) === 0xDC00) { // low surrogate - // eslint-disable-next-line no-bitwise - output.push(((value & 0x3FF) << 10) + (extra & 0x3FF) + 0x10000); - } else { - output.push(value, extra); - } - } else { - output.push(value); - } - } - return output; -} - -const allowedIdentifierUnicodeRanges: number[][] = [ - [0x0030, 0x0039], // digits - [0x0041, 0x005A], // upper case letters - [0x005F, 0x005F], // underscore - [0x0061, 0x007A], // lower case letters - [0x00A8, 0x00A8], // DIARESIS - [0x00AA, 0x00AA], // FEMININE ORDINAL INDICATOR - [0x00AD, 0x00AD], // SOFT HYPHEN - [0x00AF, 0x00AF], // MACRON - [0x00B2, 0x00B5], // SUPERSCRIPT TWO - MICRO SIGN - [0x00B7, 0x00BA], // MIDDLE DOT - MASCULINE ORDINAL INDICATOR - [0x00BC, 0x00BE], // VULGAR FRACTION ONE QUARTER - VULGAR FRACTION THREE QUARTERS - [0x00C0, 0x00D6], // LATIN CAPITAL LETTER A WITH GRAVE - LATIN CAPITAL LETTER O WITH DIAERESIS - [0x00D8, 0x00F6], // LATIN CAPITAL LETTER O WITH STROKE - LATIN SMALL LETTER O WITH DIAERESIS - [0x00F8, 0x167F], // LATIN SMALL LETTER O WITH STROKE - CANADIAN SYLLABICS BLACKFOOT W - [0x1681, 0x180D], // OGHAM LETTER BEITH - MONGOLIAN FREE VARIATION SELECTOR THREE - [0x180F, 0x1FFF], // SYRIAC LETTER BETH - GREEK DASIA - [0x200B, 0x200D], // ZERO WIDTH SPACE - ZERO WIDTH JOINER - [0x202A, 0x202E], // LEFT-TO-RIGHT EMBEDDING - RIGHT-TO-LEFT OVERRIDE - [0x203F, 0x2040], // UNDERTIE - CHARACTER TIE - [0x2054, 0x2054], // INVERTED UNDERTIE - [0x2060, 0x218F], // WORD JOINER - TURNED DIGIT THREE - [0x2460, 0x24FF], // CIRCLED DIGIT ONE - NEGATIVE CIRCLED DIGIT ZERO - [0x2776, 0x2793], // DINGBAT NEGATIVE CIRCLED DIGIT ONE - DINGBAT NEGATIVE CIRCLED SANS-SERIF NUMBER TEN - [0x2C00, 0x2DFF], // GLAGOLITIC CAPITAL LETTER AZU - COMBINING CYRILLIC LETTER IOTIFIED BIG YUS - [0x2E80, 0x2FFF], // CJK RADICAL REPEAT - IDEOGRAPHIC DESCRIPTION CHARACTER OVERLAID - [0x3004, 0x3007], // JAPANESE INDUSTRIAL STANDARD SYMBOL - IDEOGRAPHIC NUMBER ZERO - [0x3021, 0x302F], // HANGZHOU NUMERAL ONE - HANGUL DOUBLE DOT TONE MARK - [0x3031, 0xD7FF], // VERTICAL KANA REPEAT MARK - HANGUL JONGSEONG PHIEUPH-THIEUTH - [0xF900, 0xFD3D], // CJK COMPATIBILITY IDEOGRAPH-F900 - ARABIC LIGATURE ALEF WITH FATHATAN ISOLATED FORM - [0xFD40, 0xFDCF], // ARABIC LIGATURE TEH WITH JEEM WITH MEEM INITIAL FORM - ARABIC LIGATURE NOON WITH JEEM WITH YEH FINAL FORM - [0xFDF0, 0xFE44], // ARABIC LIGATURE SALLA USED AS KORANIC STOP SIGN ISOLATED FORM - PRESENTATION FORM FOR VERTICAL RIGHT WHITE CORNER BRACKET - [0xFE47, 0xFFFD], // PRESENTATION FORM FOR VERTICAL LEFT SQUARE BRACKET - REPLACEMENT CHARACTER - [0x10000, 0x1FFFD], // LINEAR B SYLLABLE B008 A - CHEESE WEDGE (U+1F9C0) - [0x20000, 0x2FFFD], // - [0x30000, 0x3FFFD], // - [0x40000, 0x4FFFD], // - [0x50000, 0x5FFFD], // - [0x60000, 0x6FFFD], // - [0x70000, 0x7FFFD], // - [0x80000, 0x8FFFD], // - [0x90000, 0x9FFFD], // - [0xA0000, 0xAFFFD], // - [0xB0000, 0xBFFFD], // - [0xC0000, 0xCFFFD], // - [0xD0000, 0xDFFFD], // - [0xE0000, 0xEFFFD] // LANGUAGE TAG (U+E0001) - VARIATION SELECTOR-256 (U+E01EF) -]; - -const disallowedFirstCharacterIdentifierUnicodeRanges: number[][] = [ - [0x0030, 0x0039], // digits - [0x0300, 0x036F], // COMBINING GRAVE ACCENT - COMBINING LATIN SMALL LETTER X - [0x1DC0, 0x1DFF], // COMBINING DOTTED GRAVE ACCENT - COMBINING RIGHT ARROWHEAD AND DOWN ARROWHEAD BELOW - [0x20D0, 0x20FF], // COMBINING LEFT HARPOON ABOVE - COMBINING ASTERISK ABOVE - [0xFE20, 0xFE2F] // COMBINING LIGATURE LEFT HALF - COMBINING CYRILLIC TITLO RIGHT HALF -]; - -export function isValidIdentifier(candidate: string): boolean { - if (!candidate) { - return false; - } - const decoded: number[] = decodeUCS16(candidate); - if (!decoded || !decoded.length) { - return false; - } - - // Reject if first character is disallowed - for (let i: number = 0; i < disallowedFirstCharacterIdentifierUnicodeRanges.length; i++) { - const disallowedCharacters: number[] = disallowedFirstCharacterIdentifierUnicodeRanges[i]; - if (decoded[0] >= disallowedCharacters[0] && decoded[0] <= disallowedCharacters[1]) { - return false; - } - } - - for (let position: number = 0; position < decoded.length; position++) { - let found: boolean = false; - for (let i: number = 0; i < allowedIdentifierUnicodeRanges.length; i++) { - const allowedCharacters: number[] = allowedIdentifierUnicodeRanges[i]; - if (decoded[position] >= allowedCharacters[0] && decoded[position] <= allowedCharacters[1]) { - found = true; - break; - } - } - if (!found) { - return false; - } - } - return true; -} - -export function getCacheStoragePath(): string { - let defaultCachePath: string = ""; - let pathEnvironmentVariable: string | undefined; - switch (os.platform()) { - case 'win32': - defaultCachePath = "Microsoft\\vscode-cpptools\\"; - pathEnvironmentVariable = process.env.LOCALAPPDATA; - break; - case 'darwin': - defaultCachePath = "Library/Caches/vscode-cpptools/"; - pathEnvironmentVariable = os.homedir(); - break; - default: // Linux - defaultCachePath = "vscode-cpptools/"; - pathEnvironmentVariable = process.env.XDG_CACHE_HOME; - if (!pathEnvironmentVariable) { - pathEnvironmentVariable = path.join(os.homedir(), ".cache"); - } - break; - } - - return pathEnvironmentVariable ? path.join(pathEnvironmentVariable, defaultCachePath) : ""; -} - -function getUniqueWorkspaceNameHelper(workspaceFolder: vscode.WorkspaceFolder, addSubfolder: boolean): string { - const workspaceFolderName: string = workspaceFolder ? workspaceFolder.name : "untitled"; - if (!workspaceFolder || workspaceFolder.index < 1) { - return workspaceFolderName; // No duplicate names to search for. - } - for (let i: number = 0; i < workspaceFolder.index; ++i) { - if (vscode.workspace.workspaceFolders && vscode.workspace.workspaceFolders.length > 0 && vscode.workspace.workspaceFolders[i].name === workspaceFolderName) { - return addSubfolder ? path.join(workspaceFolderName, String(workspaceFolder.index)) : // Use the index as a subfolder. - workspaceFolderName + String(workspaceFolder.index); - } - } - return workspaceFolderName; // No duplicate names found. -} - -export function getUniqueWorkspaceName(workspaceFolder: vscode.WorkspaceFolder): string { - return getUniqueWorkspaceNameHelper(workspaceFolder, false); -} - -export function getUniqueWorkspaceStorageName(workspaceFolder: vscode.WorkspaceFolder): string { - return getUniqueWorkspaceNameHelper(workspaceFolder, true); -} - -export function isCodespaces(): boolean { - return !!process.env.CODESPACES; -} - -// Sequentially Resolve Promises. -export function sequentialResolve(items: T[], promiseBuilder: (item: T) => Promise): Promise { - return items.reduce(async (previousPromise, nextItem) => { - await previousPromise; - return promiseBuilder(nextItem); - }, Promise.resolve()); -} - -export function quoteArgument(argument: string): string { - // Return the argument as is if it's empty - if (!argument) { - return argument; - } - - if (os.platform() === "win32") { - // Windows-style quoting logic - if (!/[\s\t\n\v\"\\&%^]/.test(argument)) { - return argument; - } - - let quotedArgument = '"'; - let backslashCount = 0; - - for (const char of argument) { - if (char === '\\') { - backslashCount++; - } else { - if (char === '"') { - quotedArgument += '\\'.repeat(backslashCount * 2 + 1); - } else { - quotedArgument += '\\'.repeat(backslashCount); - } - quotedArgument += char; - backslashCount = 0; - } - } - - quotedArgument += '\\'.repeat(backslashCount * 2); - quotedArgument += '"'; - return quotedArgument; - } else { - // Unix-style quoting logic - if (!/[\s\t\n\v\"'\\$`|;&(){}<>*?!\[\]~^#%]/.test(argument)) { - return argument; - } - - let quotedArgument = "'"; - for (const c of argument) { - if (c === "'") { - quotedArgument += "'\\''"; - } else { - quotedArgument += c; - } - } - - quotedArgument += "'"; - return quotedArgument; - } -} - -/** - * Find PowerShell executable from PATH (for Windows only). - */ -export function findPowerShell(): string | undefined { - const dirs: string[] = (process.env.PATH || '').replace(/"+/g, '').split(';').filter(x => x); - const exts: string[] = (process.env.PATHEXT || '').split(';'); - const names: string[] = ['pwsh', 'powershell']; - for (const name of names) { - const candidates: string[] = dirs.reduce((paths, dir) => [ - ...paths, ...exts.map(ext => path.join(dir, name + ext)) - ], []); - for (const candidate of candidates) { - try { - if (fs.statSync(candidate).isFile()) { - return name; - } - } catch (e) { - return undefined; - } - } - } -} - -export function getCppToolsTargetPopulation(): TargetPopulation { - // If insiders.flag is present, consider this an insiders build. - // If release.flag is present, consider this a release build. - // Otherwise, consider this an internal build. - if (checkFileExistsSync(getExtensionFilePath("insiders.flag"))) { - return TargetPopulation.Insiders; - } else if (checkFileExistsSync(getExtensionFilePath("release.flag"))) { - return TargetPopulation.Public; - } - return TargetPopulation.Internal; -} - -export function isVsCodeInsiders(): boolean { - return extensionPath.includes(".vscode-insiders") || - extensionPath.includes(".vscode-server-insiders") || - extensionPath.includes(".vscode-exploration") || - extensionPath.includes(".vscode-server-exploration"); -} - -export function stripEscapeSequences(str: string): string { - return str - // eslint-disable-next-line no-control-regex - .replace(/\x1b\[\??[0-9]{0,3}(;[0-9]{1,3})?[a-zA-Z]/g, '') - // eslint-disable-next-line no-control-regex - .replace(/\u0008/g, '') - .replace(/\r/g, ''); -} - -export function splitLines(data: string): string[] { - return data.split(/\r?\n/g); -} - -export function escapeStringForRegex(str: string): string { - return str.replace(/([.*+?^=!:${}()|\[\]\/\\])/g, '\\$1'); -} - -export function replaceAll(str: string, searchValue: string, replaceValue: string): string { - const pattern: string = escapeStringForRegex(searchValue); - const re: RegExp = new RegExp(pattern, 'g'); - return str.replace(re, replaceValue); -} - -export interface ISshHostInfo { - hostName: string; - user?: string; - port?: number | string; -} - -export interface ISshConfigHostInfo extends ISshHostInfo { - file: string; -} - -/** user@host */ -export function getFullHostAddressNoPort(host: ISshHostInfo): string { - return host.user ? `${host.user}@${host.hostName}` : `${host.hostName}`; -} - -export function getFullHostAddress(host: ISshHostInfo): string { - const fullHostName: string = getFullHostAddressNoPort(host); - return host.port ? `${fullHostName}:${host.port}` : fullHostName; -} - -export interface ISshLocalForwardInfo { - bindAddress?: string; - port?: number | string; - host?: string; - hostPort?: number | string; - localSocket?: string; - remoteSocket?: string; -} - -export function whichAsync(name: string): Promise { - return new Promise(resolve => { - which(name, (err, resolved) => { - if (err) { - resolve(undefined); - } else { - resolve(resolved); - } - }); - }); -} - -export const documentSelector: DocumentFilter[] = [ - { scheme: 'file', language: 'c' }, - { scheme: 'file', language: 'cpp' }, - { scheme: 'file', language: 'cuda-cpp' } -]; - -export function hasMsvcEnvironment(): boolean { - const msvcEnvVars: string[] = [ - 'DevEnvDir', - 'Framework40Version', - 'FrameworkDir', - 'FrameworkVersion', - 'INCLUDE', - 'LIB', - 'LIBPATH', - 'NETFXSDKDir', - 'UCRTVersion', - 'UniversalCRTSdkDir', - 'VCIDEInstallDir', - 'VCINSTALLDIR', - 'VCToolsRedistDir', - 'VisualStudioVersion', - 'VSINSTALLDIR', - 'WindowsLibPath', - 'WindowsSdkBinPath', - 'WindowsSdkDir', - 'WindowsSDKLibVersion', - 'WindowsSDKVersion' - ]; - return msvcEnvVars.every((envVarName) => process.env[envVarName] !== undefined && process.env[envVarName] !== ''); -} - -function isIntegral(str: string): boolean { - const regex = /^-?\d+$/; - return regex.test(str); -} - -export function getNumericLoggingLevel(loggingLevel: string | undefined): number { - if (!loggingLevel) { - return 1; - } - if (isIntegral(loggingLevel)) { - return parseInt(loggingLevel, 10); - } - const lowerCaseLoggingLevel: string = loggingLevel.toLowerCase(); - switch (lowerCaseLoggingLevel) { - case "error": - return 1; - case "warning": - return 3; - case "information": - return 5; - case "debug": - return 6; - case "none": - return 0; - default: - return -1; - } -} - -export function mergeOverlappingRanges(ranges: Range[]): Range[] { - // Fix any reversed ranges. Not sure if this is needed, but ensures the input is sanitized. - const mergedRanges: Range[] = ranges.map(range => { - if (range.start.line > range.end.line || (range.start.line === range.end.line && range.start.character > range.end.character)) { - return Range.create(range.end, range.start); - } - return range; - }); - - // Merge overlapping ranges. - mergedRanges.sort((a, b) => a.start.line - b.start.line || a.start.character - b.start.character); - let lastMergedIndex = 0; // Index to keep track of the last merged range - for (let currentIndex = 0; currentIndex < ranges.length; currentIndex++) { - const currentRange = ranges[currentIndex]; // No need for a shallow copy, since we're not modifying the ranges we haven't read yet. - let nextIndex = currentIndex + 1; - while (nextIndex < ranges.length) { - const nextRange = ranges[nextIndex]; - // Check for non-overlapping ranges first - if (nextRange.start.line > currentRange.end.line || - (nextRange.start.line === currentRange.end.line && nextRange.start.character > currentRange.end.character)) { - break; - } - // Otherwise, merge the overlapping ranges - currentRange.end = { - line: Math.max(currentRange.end.line, nextRange.end.line), - character: Math.max(currentRange.end.character, nextRange.end.character) - }; - nextIndex++; - } - // Overwrite the array in-place - mergedRanges[lastMergedIndex] = currentRange; - lastMergedIndex++; - currentIndex = nextIndex - 1; // Skip the merged ranges - } - mergedRanges.length = lastMergedIndex; - return mergedRanges; -} - -// Arg quoting utility functions, copied from VS Code with minor changes. - -export interface IShellQuotingOptions { - /** - * The character used to do character escaping. - */ - escape?: string | { - escapeChar: string; - charsToEscape: string; - }; - - /** - * The character used for string quoting. - */ - strong?: string; - - /** - * The character used for weak quoting. - */ - weak?: string; -} - -export interface IQuotedString { - value: string; - quoting: 'escape' | 'strong' | 'weak'; -} - -export type CommandString = string | IQuotedString; - -export function buildShellCommandLine(originalCommand: CommandString, command: CommandString, args: CommandString[]): string { - - let shellQuoteOptions: IShellQuotingOptions; - const isWindows: boolean = os.platform() === 'win32'; - if (isWindows) { - shellQuoteOptions = { - strong: '"' - }; - } else { - shellQuoteOptions = { - escape: { - escapeChar: '\\', - charsToEscape: ' "\'' - }, - strong: '\'', - weak: '"' - }; - } - - // TODO: Support launching with PowerShell - // For PowerShell: - // { - // escape: { - // escapeChar: '`', - // charsToEscape: ' "\'()' - // }, - // strong: '\'', - // weak: '"' - // }, - - function needsQuotes(value: string): boolean { - if (value.length >= 2) { - const first = value[0] === shellQuoteOptions.strong ? shellQuoteOptions.strong : value[0] === shellQuoteOptions.weak ? shellQuoteOptions.weak : undefined; - if (first === value[value.length - 1]) { - return false; - } - } - let quote: string | undefined; - for (let i = 0; i < value.length; i++) { - // We found the end quote. - const ch = value[i]; - if (ch === quote) { - quote = undefined; - } else if (quote !== undefined) { - // skip the character. We are quoted. - continue; - } else if (ch === shellQuoteOptions.escape) { - // Skip the next character - i++; - } else if (ch === shellQuoteOptions.strong || ch === shellQuoteOptions.weak) { - quote = ch; - } else if (ch === ' ') { - return true; - } - } - return false; - } - - function quote(value: string, kind: 'escape' | 'strong' | 'weak'): [string, boolean] { - if (kind === "strong" && shellQuoteOptions.strong) { - return [shellQuoteOptions.strong + value + shellQuoteOptions.strong, true]; - } else if (kind === "weak" && shellQuoteOptions.weak) { - return [shellQuoteOptions.weak + value + shellQuoteOptions.weak, true]; - } else if (kind === "escape" && shellQuoteOptions.escape) { - if (isString(shellQuoteOptions.escape)) { - return [value.replace(/ /g, shellQuoteOptions.escape + ' '), true]; - } else { - const buffer: string[] = []; - for (const ch of shellQuoteOptions.escape.charsToEscape) { - buffer.push(`\\${ch}`); - } - const regexp: RegExp = new RegExp('[' + buffer.join(',') + ']', 'g'); - const escapeChar = shellQuoteOptions.escape.escapeChar; - return [value.replace(regexp, (match) => escapeChar + match), true]; - } - } - return [value, false]; - } - - function quoteIfNecessary(value: CommandString): [string, boolean] { - if (isString(value)) { - if (needsQuotes(value)) { - return quote(value, "strong"); - } else { - return [value, false]; - } - } else { - return quote(value.value, value.quoting); - } - } - - // If we have no args and the command is a string then use the command to stay backwards compatible with the old command line - // model. To allow variable resolving with spaces we do continue if the resolved value is different than the original one - // and the resolved one needs quoting. - if ((!args || args.length === 0) && isString(command) && (command === originalCommand as string || needsQuotes(originalCommand as string))) { - return command; - } - - const result: string[] = []; - let commandQuoted = false; - let argQuoted = false; - let value: string; - let quoted: boolean; - [value, quoted] = quoteIfNecessary(command); - result.push(value); - commandQuoted = quoted; - for (const arg of args) { - [value, quoted] = quoteIfNecessary(arg); - result.push(value); - argQuoted = argQuoted || quoted; - } - - let commandLine = result.join(' '); - // There are special rules quoted command line in cmd.exe - if (isWindows) { - commandLine = `chcp 65001>nul && ${commandLine}`; - if (commandQuoted && argQuoted) { - commandLine = '"' + commandLine + '"'; - } - commandLine = `cmd /c ${commandLine}`; - } - - return commandLine; -} - -export function findExePathInArgs(args: CommandString[]): string | undefined { - const isWindows: boolean = os.platform() === 'win32'; - let previousArg: string | undefined; - - for (const arg of args) { - const argValue = isString(arg) ? arg : arg.value; - if (previousArg === '-o') { - return argValue; - } - if (isWindows && argValue.includes('.exe')) { - if (argValue.startsWith('/Fe')) { - return argValue.substring(3); - } else if (argValue.toLowerCase().startsWith('/out:')) { - return argValue.substring(5); - } - } - - previousArg = argValue; - } - - return undefined; -} - -export function getVsCodeVersion(): number[] { - return vscode.version.split('.').map(num => parseInt(num, undefined)); -} +/* -------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All Rights Reserved. + * See 'LICENSE' in the project root for license information. + * ------------------------------------------------------------------------------------------ */ + +import * as assert from 'assert'; +import * as child_process from 'child_process'; +import * as jsonc from 'comment-json'; +import * as fs from 'fs'; +import * as os from 'os'; +import * as path from 'path'; +import * as tmp from 'tmp'; +import * as vscode from 'vscode'; +import { DocumentFilter, Range } from 'vscode-languageclient'; +import * as nls from 'vscode-nls'; +import { TargetPopulation } from 'vscode-tas-client'; +import * as which from "which"; +import { ManualPromise } from './Utility/Async/manualPromise'; +import { isWindows } from './constants'; +import { getOutputChannelLogger, showOutputChannel } from './logger'; +import { PlatformInformation } from './platform'; +import * as Telemetry from './telemetry'; + +nls.config({ messageFormat: nls.MessageFormat.bundle, bundleFormat: nls.BundleFormat.standalone })(); +const localize: nls.LocalizeFunc = nls.loadMessageBundle(); +export const failedToParseJson: string = localize("failed.to.parse.json", "Failed to parse json file, possibly due to comments or trailing commas."); + +export type Mutable = { + // eslint-disable-next-line @typescript-eslint/array-type + -readonly [P in keyof T]: T[P] extends ReadonlyArray ? Mutable[] : Mutable +}; + +export let extensionPath: string; +export let extensionContext: vscode.ExtensionContext | undefined; +export function setExtensionContext(context: vscode.ExtensionContext): void { + extensionContext = context; + extensionPath = extensionContext.extensionPath; +} + +export function setExtensionPath(path: string): void { + extensionPath = path; +} + +let cachedClangFormatPath: string | undefined; +export function getCachedClangFormatPath(): string | undefined { + return cachedClangFormatPath; +} + +export function setCachedClangFormatPath(path: string): void { + cachedClangFormatPath = path; +} + +let cachedClangTidyPath: string | undefined; +export function getCachedClangTidyPath(): string | undefined { + return cachedClangTidyPath; +} + +export function setCachedClangTidyPath(path: string): void { + cachedClangTidyPath = path; +} + +// Use this package.json to read values +export const packageJson: any = vscode.extensions.getExtension("ms-vscode.cpptools")?.packageJSON; + +// Use getRawSetting to get subcategorized settings from package.json. +// This prevents having to iterate every time we search. +let flattenedPackageJson: Map; +export function getRawSetting(key: string, breakIfMissing: boolean = false): any { + if (flattenedPackageJson === undefined) { + flattenedPackageJson = new Map(); + for (const subheading of packageJson.contributes.configuration) { + for (const setting in subheading.properties) { + flattenedPackageJson.set(setting, subheading.properties[setting]); + } + } + } + const result = flattenedPackageJson.get(key); + if (result === undefined && breakIfMissing) { + // eslint-disable-next-line no-debugger + debugger; // The setting does not exist in package.json. Check the `key`. + } + return result; +} + +export async function getRawJson(path: string | undefined): Promise { + if (!path) { + return {}; + } + const fileExists: boolean = await checkFileExists(path); + if (!fileExists) { + return {}; + } + + const fileContents: string = await readFileText(path); + let rawElement: any = {}; + try { + rawElement = jsonc.parse(fileContents, undefined, true); + } catch (error) { + throw new Error(failedToParseJson); + } + return rawElement; +} + +// This function is used to stringify the rawPackageJson. +// Do not use with util.packageJson or else the expanded +// package.json will be written back. +export function stringifyPackageJson(packageJson: string): string { + return JSON.stringify(packageJson, null, 2); +} + +export function getExtensionFilePath(extensionfile: string): string { + return path.resolve(extensionPath, extensionfile); +} + +export function getPackageJsonPath(): string { + return getExtensionFilePath("package.json"); +} + +export function getJsonPath(jsonFilaName: string, workspaceFolder?: vscode.WorkspaceFolder): string | undefined { + const editor: vscode.TextEditor | undefined = vscode.window.activeTextEditor; + if (!editor) { + return undefined; + } + const folder: vscode.WorkspaceFolder | undefined = workspaceFolder ? workspaceFolder : vscode.workspace.getWorkspaceFolder(editor.document.uri); + if (!folder) { + return undefined; + } + return path.join(folder.uri.fsPath, ".vscode", jsonFilaName); +} + +export function getVcpkgPathDescriptorFile(): string { + if (process.platform === 'win32') { + const pathPrefix: string | undefined = process.env.LOCALAPPDATA; + if (!pathPrefix) { + throw new Error("Unable to read process.env.LOCALAPPDATA"); + } + return path.join(pathPrefix, "vcpkg/vcpkg.path.txt"); + } else { + const pathPrefix: string = os.homedir(); + return path.join(pathPrefix, ".vcpkg/vcpkg.path.txt"); + } +} + +let vcpkgRoot: string | undefined; +export function getVcpkgRoot(): string { + if (!vcpkgRoot && vcpkgRoot !== "") { + vcpkgRoot = ""; + // Check for vcpkg instance. + if (fs.existsSync(getVcpkgPathDescriptorFile())) { + let vcpkgRootTemp: string = fs.readFileSync(getVcpkgPathDescriptorFile()).toString(); + vcpkgRootTemp = vcpkgRootTemp.trim(); + if (fs.existsSync(vcpkgRootTemp)) { + vcpkgRoot = path.join(vcpkgRootTemp, "/installed").replace(/\\/g, "/"); + } + } + } + return vcpkgRoot; +} + +/** + * This is a fuzzy determination of whether a uri represents a header file. + * For the purposes of this function, a header file has no extension, or an extension that begins with the letter 'h'. + * @param document The document to check. + */ +export function isHeaderFile(uri: vscode.Uri): boolean { + const fileExt: string = path.extname(uri.fsPath); + const fileExtLower: string = fileExt.toLowerCase(); + return !fileExt || [".cuh", ".hpp", ".hh", ".hxx", ".h++", ".hp", ".h", ".ii", ".inl", ".idl", ""].some(ext => fileExtLower === ext); +} + +export function isCppFile(uri: vscode.Uri): boolean { + const fileExt: string = path.extname(uri.fsPath); + const fileExtLower: string = fileExt.toLowerCase(); + return (fileExt === ".C") || [".cu", ".cpp", ".cc", ".cxx", ".c++", ".cp", ".ino", ".ipp", ".tcc"].some(ext => fileExtLower === ext); +} + +export function isCFile(uri: vscode.Uri): boolean { + const fileExt: string = path.extname(uri.fsPath); + const fileExtLower: string = fileExt.toLowerCase(); + return (fileExt === ".C") || fileExtLower === ".c"; +} + +export function isCppOrCFile(uri: vscode.Uri | undefined): boolean { + if (!uri) { + return false; + } + return isCppFile(uri) || isCFile(uri); +} + +export function isFolderOpen(uri: vscode.Uri): boolean { + const folder: vscode.WorkspaceFolder | undefined = vscode.workspace.getWorkspaceFolder(uri); + return folder ? true : false; +} + +export function isEditorFileCpp(file: string): boolean { + const editor: vscode.TextEditor | undefined = vscode.window.visibleTextEditors.find(e => e.document.uri.toString() === file); + if (!editor) { + return false; + } + return editor.document.languageId === "cpp"; +} + +// If it's C, C++, or Cuda. +export function isCpp(document: vscode.TextDocument): boolean { + return document.uri.scheme === "file" && + (document.languageId === "c" || document.languageId === "cpp" || document.languageId === "cuda-cpp"); +} + +export function isCppPropertiesJson(document: vscode.TextDocument): boolean { + return document.uri.scheme === "file" && (document.languageId === "json" || document.languageId === "jsonc") && + document.fileName.endsWith("c_cpp_properties.json"); +} +let isWorkspaceCpp: boolean = false; +export function setWorkspaceIsCpp(): void { + if (!isWorkspaceCpp) { + isWorkspaceCpp = true; + } +} + +export function getWorkspaceIsCpp(): boolean { + return isWorkspaceCpp; +} + +export function isCppOrRelated(document: vscode.TextDocument): boolean { + return isCpp(document) || isCppPropertiesJson(document) || (document.uri.scheme === "output" && document.uri.fsPath.startsWith("extension-output-ms-vscode.cpptools")) || + (isWorkspaceCpp && (document.languageId === "json" || document.languageId === "jsonc") && + ((document.fileName.endsWith("settings.json") && (document.uri.scheme === "file" || document.uri.scheme === "vscode-userdata")) || + (document.uri.scheme === "file" && document.fileName.endsWith(".code-workspace")))); +} + +let isExtensionNotReadyPromptDisplayed: boolean = false; +export const extensionNotReadyString: string = localize("extension.not.ready", 'The C/C++ extension is still installing. See the output window for more information.'); + +export function displayExtensionNotReadyPrompt(): void { + if (!isExtensionNotReadyPromptDisplayed) { + isExtensionNotReadyPromptDisplayed = true; + showOutputChannel(); + + void getOutputChannelLogger().showInformationMessage(extensionNotReadyString).then( + () => { isExtensionNotReadyPromptDisplayed = false; }, + () => { isExtensionNotReadyPromptDisplayed = false; } + ); + } +} + +// This Progress global state tracks how far users are able to get before getting blocked. +// Users start with a progress of 0 and it increases as they get further along in using the tool. +// This eliminates noise/problems due to re-installs, terminated installs that don't send errors, +// errors followed by workarounds that lead to success, etc. +const progressInstallSuccess: number = 100; +const progressExecutableStarted: number = 150; +const progressExecutableSuccess: number = 200; +const progressParseRootSuccess: number = 300; +const progressIntelliSenseNoSquiggles: number = 1000; +// Might add more IntelliSense progress measurements later. +// IntelliSense progress is separate from the install progress, because parse root can occur afterwards. + +const installProgressStr: string = "CPP." + packageJson.version + ".Progress"; +const intelliSenseProgressStr: string = "CPP." + packageJson.version + ".IntelliSenseProgress"; + +export function getProgress(): number { + return extensionContext ? extensionContext.globalState.get(installProgressStr, -1) : -1; +} + +export function getIntelliSenseProgress(): number { + return extensionContext ? extensionContext.globalState.get(intelliSenseProgressStr, -1) : -1; +} + +export function setProgress(progress: number): void { + if (extensionContext && getProgress() < progress) { + void extensionContext.globalState.update(installProgressStr, progress); + const telemetryProperties: Record = {}; + let progressName: string | undefined; + switch (progress) { + case 0: progressName = "install started"; break; + case progressInstallSuccess: progressName = "install succeeded"; break; + case progressExecutableStarted: progressName = "executable started"; break; + case progressExecutableSuccess: progressName = "executable succeeded"; break; + case progressParseRootSuccess: progressName = "parse root succeeded"; break; + } + if (progressName) { + telemetryProperties.progress = progressName; + } + Telemetry.logDebuggerEvent("progress", telemetryProperties); + } +} + +export function setIntelliSenseProgress(progress: number): void { + if (extensionContext && getIntelliSenseProgress() < progress) { + void extensionContext.globalState.update(intelliSenseProgressStr, progress); + const telemetryProperties: Record = {}; + let progressName: string | undefined; + switch (progress) { + case progressIntelliSenseNoSquiggles: progressName = "IntelliSense no squiggles"; break; + } + if (progressName) { + telemetryProperties.progress = progressName; + } + Telemetry.logDebuggerEvent("progress", telemetryProperties); + } +} + +export function getProgressInstallSuccess(): number { return progressInstallSuccess; } // Download/install was successful (i.e. not blocked by component acquisition). +export function getProgressExecutableStarted(): number { return progressExecutableStarted; } // The extension was activated and starting the executable was attempted. +export function getProgressExecutableSuccess(): number { return progressExecutableSuccess; } // Starting the exe was successful (i.e. not blocked by 32-bit or glibc < 2.18 on Linux) +export function getProgressParseRootSuccess(): number { return progressParseRootSuccess; } // Parse root was successful (i.e. not blocked by processing taking too long). +export function getProgressIntelliSenseNoSquiggles(): number { return progressIntelliSenseNoSquiggles; } // IntelliSense was successful and the user got no squiggles. + +export function isUri(input: any): input is vscode.Uri { + return input && input instanceof vscode.Uri; +} + +export function isString(input: any): input is string { + return typeof input === "string"; +} + +export function isNumber(input: any): input is number { + return typeof input === "number"; +} + +export function isBoolean(input: any): input is boolean { + return typeof input === "boolean"; +} + +export function isObject(input: any): boolean { + return input !== null && typeof input === "object" && !isArray(input); +} + +export function isArray(input: any): input is any[] { + return Array.isArray(input); +} + +export function isOptionalString(input: any): input is string | undefined { + return input === undefined || isString(input); +} + +export function isArrayOfString(input: any): input is string[] { + return isArray(input) && input.every(isString); +} + +// Validates whether the given object is a valid mapping of key and value type. +// EX: {"key": true, "key2": false} should return true for keyType = string and valueType = boolean. +export function isValidMapping(value: any, isValidKey: (key: any) => boolean, isValidValue: (value: any) => boolean): value is object { + if (isObject(value)) { + return Object.entries(value).every(([key, val]) => isValidKey(key) && isValidValue(val)); + } + return false; +} + +export function isOptionalArrayOfString(input: any): input is string[] | undefined { + return input === undefined || isArrayOfString(input); +} + +export function resolveCachePath(input: string | undefined, additionalEnvironment: Record): string { + let resolvedPath: string = ""; + if (!input || input.trim() === "") { + // If no path is set, return empty string to language service process, where it will set the default path as + // Windows: %LocalAppData%/Microsoft/vscode-cpptools/ + // Linux and Mac: ~/.vscode-cpptools/ + return resolvedPath; + } + + resolvedPath = resolveVariables(input, additionalEnvironment); + return resolvedPath; +} + +export function defaultExePath(): string { + const exePath: string = path.join('${fileDirname}', '${fileBasenameNoExtension}'); + return isWindows ? exePath + '.exe' : exePath; +} + +// Pass in 'arrayResults' if a string[] result is possible and a delimited string result is undesirable. +// The string[] result will be copied into 'arrayResults'. +export function resolveVariables(input: string | undefined, additionalEnvironment?: Record, arrayResults?: string[]): string { + if (!input) { + return ""; + } + + // jsonc parser may assign a non-string object to a string. + // TODO: https://github.com/microsoft/vscode-cpptools/issues/9414 + if (!isString(input)) { + const inputAny: any = input; + input = inputAny.toString(); + return input ?? ""; + } + + // Replace environment and configuration variables. + const regexp: () => RegExp = () => /\$\{((env|config|workspaceFolder)(\.|:))?(.*?)\}/g; + let ret: string = input; + const cycleCache = new Set(); + while (!cycleCache.has(ret)) { + cycleCache.add(ret); + ret = ret.replace(regexp(), (match: string, ignored1: string, varType: string, ignored2: string, name: string) => { + // Historically, if the variable didn't have anything before the "." or ":" + // it was assumed to be an environment variable + if (!varType) { + varType = "env"; + } + let newValue: string | undefined; + switch (varType) { + case "env": { + if (additionalEnvironment) { + const v: string | string[] | undefined = additionalEnvironment[name]; + if (isString(v)) { + newValue = v; + } else if (input === match && isArrayOfString(v)) { + if (arrayResults !== undefined) { + arrayResults.push(...v); + newValue = ""; + break; + } else { + newValue = v.join(path.delimiter); + } + } + } + if (newValue === undefined) { + newValue = process.env[name]; + } + break; + } + case "config": { + const config: vscode.WorkspaceConfiguration = vscode.workspace.getConfiguration(); + if (config) { + newValue = config.get(name); + } + break; + } + case "workspaceFolder": { + // Only replace ${workspaceFolder:name} variables for now. + // We may consider doing replacement of ${workspaceFolder} here later, but we would have to update the language server and also + // intercept messages with paths in them and add the ${workspaceFolder} variable back in (e.g. for light bulb suggestions) + if (name && vscode.workspace && vscode.workspace.workspaceFolders) { + const folder: vscode.WorkspaceFolder | undefined = vscode.workspace.workspaceFolders.find(folder => folder.name.toLocaleLowerCase() === name.toLocaleLowerCase()); + if (folder) { + newValue = folder.uri.fsPath; + } + } + break; + } + default: { assert.fail("unknown varType matched"); } + } + return newValue !== undefined ? newValue : match; + }); + } + + return resolveHome(ret); +} + +export function resolveVariablesArray(variables: string[] | undefined, additionalEnvironment?: Record): string[] { + let result: string[] = []; + if (variables) { + variables.forEach(variable => { + const variablesResolved: string[] = []; + const variableResolved: string = resolveVariables(variable, additionalEnvironment, variablesResolved); + result = result.concat(variablesResolved.length === 0 ? variableResolved : variablesResolved); + }); + } + return result; +} + +// Resolve '~' at the start of the path. +export function resolveHome(filePath: string): string { + return filePath.replace(/^\~/g, os.homedir()); +} + +export function asFolder(uri: vscode.Uri): string { + let result: string = uri.toString(); + if (!result.endsWith('/')) { + result += '/'; + } + return result; +} + +/** + * get the default open command for the current platform + */ +export function getOpenCommand(): string { + if (os.platform() === 'win32') { + return 'explorer'; + } else if (os.platform() === 'darwin') { + return '/usr/bin/open'; + } else { + return '/usr/bin/xdg-open'; + } +} + +export function getDebugAdaptersPath(file: string): string { + return path.resolve(getExtensionFilePath("debugAdapters"), file); +} + +export async function fsStat(filePath: fs.PathLike): Promise { + let stats: fs.Stats | undefined; + try { + stats = await fs.promises.stat(filePath); + } catch (e) { + // File doesn't exist + return undefined; + } + return stats; +} + +export async function checkPathExists(filePath: string): Promise { + return !!await fsStat(filePath); +} + +/** Test whether a file exists */ +export async function checkFileExists(filePath: string): Promise { + const stats: fs.Stats | undefined = await fsStat(filePath); + return !!stats && stats.isFile(); +} + +/** Test whether a file exists */ +export async function checkExecutableWithoutExtensionExists(filePath: string): Promise { + if (await checkFileExists(filePath)) { + return true; + } + if (os.platform() === 'win32') { + if (filePath.length > 4) { + const possibleExtension: string = filePath.substring(filePath.length - 4).toLowerCase(); + if (possibleExtension === ".exe" || possibleExtension === ".cmd" || possibleExtension === ".bat") { + return false; + } + } + if (await checkFileExists(filePath + ".exe")) { + return true; + } + if (await checkFileExists(filePath + ".cmd")) { + return true; + } + if (await checkFileExists(filePath + ".bat")) { + return true; + } + } + return false; +} + +/** Test whether a directory exists */ +export async function checkDirectoryExists(dirPath: string): Promise { + const stats: fs.Stats | undefined = await fsStat(dirPath); + return !!stats && stats.isDirectory(); +} + +export function createDirIfNotExistsSync(filePath: string | undefined): void { + if (!filePath) { + return; + } + const dirPath: string = path.dirname(filePath); + if (!checkDirectoryExistsSync(dirPath)) { + fs.mkdirSync(dirPath, { recursive: true }); + } +} + +export function checkFileExistsSync(filePath: string): boolean { + try { + return fs.statSync(filePath).isFile(); + } catch (e) { + return false; + } +} + +export function checkExecutableWithoutExtensionExistsSync(filePath: string): boolean { + if (checkFileExistsSync(filePath)) { + return true; + } + if (os.platform() === 'win32') { + if (filePath.length > 4) { + const possibleExtension: string = filePath.substring(filePath.length - 4).toLowerCase(); + if (possibleExtension === ".exe" || possibleExtension === ".cmd" || possibleExtension === ".bat") { + return false; + } + } + if (checkFileExistsSync(filePath + ".exe")) { + return true; + } + if (checkFileExistsSync(filePath + ".cmd")) { + return true; + } + if (checkFileExistsSync(filePath + ".bat")) { + return true; + } + } + return false; +} + +/** Test whether a directory exists */ +export function checkDirectoryExistsSync(dirPath: string): boolean { + try { + return fs.statSync(dirPath).isDirectory(); + } catch (e) { + return false; + } +} + +/** Test whether a relative path exists */ +export function checkPathExistsSync(path: string, relativePath: string, _isWindows: boolean, isCompilerPath: boolean): { pathExists: boolean; path: string } { + let pathExists: boolean = true; + const existsWithExeAdded: (path: string) => boolean = (path: string) => isCompilerPath && _isWindows && fs.existsSync(path + ".exe"); + if (!fs.existsSync(path)) { + if (existsWithExeAdded(path)) { + path += ".exe"; + } else if (!relativePath) { + pathExists = false; + } else { + // Check again for a relative path. + relativePath = relativePath + path; + if (!fs.existsSync(relativePath)) { + if (existsWithExeAdded(path)) { + path += ".exe"; + } else { + pathExists = false; + } + } else { + path = relativePath; + } + } + } + return { pathExists, path }; +} + +/** Read the files in a directory */ +export function readDir(dirPath: string): Promise { + return new Promise((resolve) => { + fs.readdir(dirPath, (err, list) => { + resolve(list); + }); + }); +} + +/** Reads the content of a text file */ +export function readFileText(filePath: string, encoding: BufferEncoding = "utf8"): Promise { + return new Promise((resolve, reject) => { + fs.readFile(filePath, { encoding }, (err: any, data: any) => { + if (err) { + reject(err); + } else { + resolve(data); + } + }); + }); +} + +/** Writes content to a text file */ +export function writeFileText(filePath: string, content: string, encoding: BufferEncoding = "utf8"): Promise { + const folders: string[] = filePath.split(path.sep).slice(0, -1); + if (folders.length) { + // create folder path if it doesn't exist + folders.reduce((previous, folder) => { + const folderPath: string = previous + path.sep + folder; + if (!fs.existsSync(folderPath)) { + fs.mkdirSync(folderPath); + } + return folderPath; + }); + } + + return new Promise((resolve, reject) => { + fs.writeFile(filePath, content, { encoding }, (err) => { + if (err) { + reject(err); + } else { + resolve(); + } + }); + }); +} + +export function deleteFile(filePath: string): Promise { + return new Promise((resolve, reject) => { + if (fs.existsSync(filePath)) { + fs.unlink(filePath, (err) => { + if (err) { + reject(err); + } else { + resolve(); + } + }); + } else { + resolve(); + } + }); +} + +export function deleteDirectory(directoryPath: string): Promise { + return new Promise((resolve, reject) => { + if (fs.existsSync(directoryPath)) { + fs.rmdir(directoryPath, (err) => { + if (err) { + reject(err); + } else { + resolve(); + } + }); + } else { + resolve(); + } + }); +} + +export function getReadmeMessage(): string { + const readmePath: string = getExtensionFilePath("README.md"); + const readmeMessage: string = localize("refer.read.me", "Please refer to {0} for troubleshooting information. Issues can be created at {1}", readmePath, "https://github.com/Microsoft/vscode-cpptools/issues"); + return readmeMessage; +} + +/** Used for diagnostics only */ +export function logToFile(message: string): void { + const logFolder: string = getExtensionFilePath("extension.log"); + fs.writeFileSync(logFolder, `${message}${os.EOL}`, { flag: 'a' }); +} + +export function execChildProcess(process: string, workingDirectory?: string, channel?: vscode.OutputChannel): Promise { + return new Promise((resolve, reject) => { + child_process.exec(process, { cwd: workingDirectory, maxBuffer: 500 * 1024 }, (error: Error | null, stdout: string, stderr: string) => { + if (channel) { + let message: string = ""; + let err: boolean = false; + if (stdout && stdout.length > 0) { + message += stdout; + } + + if (stderr && stderr.length > 0) { + message += stderr; + err = true; + } + + if (error) { + message += error.message; + err = true; + } + + if (err) { + channel.append(message); + channel.show(); + } + } + + if (error) { + reject(error); + return; + } + + if (stderr && stderr.length > 0) { + reject(new Error(stderr)); + return; + } + + resolve(stdout); + }); + }); +} + +export interface ProcessReturnType { + succeeded: boolean; + exitCode?: number | NodeJS.Signals; + output: string; + outputError: string; +} + +export async function spawnChildProcess(program: string, args: string[] = [], continueOn?: string, skipLogging?: boolean, cancellationToken?: vscode.CancellationToken): Promise { + // Do not use CppSettings to avoid circular require() + if (skipLogging === undefined || !skipLogging) { + getOutputChannelLogger().appendLine(5, `$ ${program} ${args.join(' ')}`); + } + const programOutput: ProcessOutput = await spawnChildProcessImpl(program, args, continueOn, skipLogging, cancellationToken); + const exitCode: number | NodeJS.Signals | undefined = programOutput.exitCode; + if (programOutput.exitCode) { + return { succeeded: false, exitCode, outputError: programOutput.stderr, output: programOutput.stderr || programOutput.stdout || localize('process.exited', 'Process exited with code {0}', exitCode) }; + } else { + let stdout: string; + if (programOutput.stdout.length) { + // Type system doesn't work very well here, so we need call toString + stdout = programOutput.stdout; + } else { + stdout = localize('process.succeeded', 'Process executed successfully.'); + } + return { succeeded: true, exitCode, outputError: programOutput.stderr, output: stdout }; + } +} + +interface ProcessOutput { + exitCode?: number | NodeJS.Signals; + stdout: string; + stderr: string; +} + +async function spawnChildProcessImpl(program: string, args: string[], continueOn?: string, skipLogging?: boolean, cancellationToken?: vscode.CancellationToken): Promise { + const result = new ManualPromise(); + + let proc: child_process.ChildProcess; + if (await isExecutable(program)) { + proc = child_process.spawn(`.${isWindows ? '\\' : '/'}${path.basename(program)}`, args, { shell: true, cwd: path.dirname(program) }); + } else { + proc = child_process.spawn(program, args, { shell: true }); + } + + const cancellationTokenListener: vscode.Disposable | undefined = cancellationToken?.onCancellationRequested(() => { + getOutputChannelLogger().appendLine(localize('killing.process', 'Killing process {0}', program)); + proc.kill(); + }); + + const clean = () => { + proc.removeAllListeners(); + if (cancellationTokenListener) { + cancellationTokenListener.dispose(); + } + }; + + let stdout: string = ''; + let stderr: string = ''; + if (proc.stdout) { + proc.stdout.on('data', data => { + const str: string = data.toString(); + if (skipLogging === undefined || !skipLogging) { + getOutputChannelLogger().append(1, str); + } + stdout += str; + if (continueOn) { + const continueOnReg: string = escapeStringForRegex(continueOn); + if (stdout.search(continueOnReg)) { + result.resolve({ stdout: stdout.trim(), stderr: stderr.trim() }); + } + } + }); + } + if (proc.stderr) { + proc.stderr.on('data', data => stderr += data.toString()); + } + proc.on('close', (code, signal) => { + clean(); + result.resolve({ exitCode: code || signal || undefined, stdout: stdout.trim(), stderr: stderr.trim() }); + }); + proc.on('error', error => { + clean(); + result.reject(error); + }); + return result; +} + +/** + * @param permission fs file access constants: https://nodejs.org/api/fs.html#file-access-constants + */ +export function pathAccessible(filePath: string, permission: number = fs.constants.F_OK): Promise { + if (!filePath) { return Promise.resolve(false); } + return new Promise(resolve => fs.access(filePath, permission, err => resolve(!err))); +} + +export function isExecutable(file: string): Promise { + return pathAccessible(file, fs.constants.X_OK); +} + +export async function allowExecution(file: string): Promise { + if (process.platform !== 'win32') { + const exists: boolean = await checkFileExists(file); + if (exists) { + const isExec: boolean = await isExecutable(file); + if (!isExec) { + await chmodAsync(file, '755'); + } + } else { + getOutputChannelLogger().appendLine(""); + getOutputChannelLogger().appendLine(localize("warning.file.missing", "Warning: Expected file {0} is missing.", file)); + } + } +} + +export async function chmodAsync(path: fs.PathLike, mode: fs.Mode): Promise { + return new Promise((resolve, reject) => { + fs.chmod(path, mode, (err: NodeJS.ErrnoException | null) => { + if (err) { + return reject(err); + } + return resolve(); + }); + }); +} + +export function removePotentialPII(str: string): string { + const words: string[] = str.split(" "); + let result: string = ""; + for (const word of words) { + if (!word.includes(".") && !word.includes("/") && !word.includes("\\") && !word.includes(":")) { + result += word + " "; + } else { + result += "? "; + } + } + return result; +} + +export function checkDistro(platformInfo: PlatformInformation): void { + if (platformInfo.platform !== 'win32' && platformInfo.platform !== 'linux' && platformInfo.platform !== 'darwin') { + // this should never happen because VSCode doesn't run on FreeBSD + // or SunOS (the other platforms supported by node) + getOutputChannelLogger().appendLine(localize("warning.debugging.not.tested", "Warning: Debugging has not been tested for this platform.") + " " + getReadmeMessage()); + } +} + +export async function unlinkAsync(fileName: string): Promise { + return new Promise((resolve, reject) => { + fs.unlink(fileName, err => { + if (err) { + return reject(err); + } + return resolve(); + }); + }); +} + +export async function renameAsync(oldName: string, newName: string): Promise { + return new Promise((resolve, reject) => { + fs.rename(oldName, newName, err => { + if (err) { + return reject(err); + } + return resolve(); + }); + }); +} + +export async function promptForReloadWindowDueToSettingsChange(): Promise { + await promptReloadWindow(localize("reload.workspace.for.changes", "Reload the workspace for the settings change to take effect.")); +} + +export async function promptReloadWindow(message: string): Promise { + const reload: string = localize("reload.string", "Reload"); + const value: string | undefined = await vscode.window.showInformationMessage(message, reload); + if (value === reload) { + return vscode.commands.executeCommand("workbench.action.reloadWindow"); + } +} + +export function createTempFileWithPostfix(postfix: string): Promise { + return new Promise((resolve, reject) => { + tmp.file({ postfix: postfix }, (err, path, fd, cleanupCallback) => { + if (err) { + return reject(err); + } + return resolve({ name: path, fd: fd, removeCallback: cleanupCallback } as tmp.FileResult); + }); + }); +} + +function resolveWindowsEnvironmentVariables(str: string): string { + return str.replace(/%([^%]+)%/g, (withPercents, withoutPercents) => { + const found: string | undefined = process.env[withoutPercents]; + return found || withPercents; + }); +} + +function legacyExtractArgs(argsString: string): string[] { + const result: string[] = []; + let currentArg: string = ""; + let isWithinDoubleQuote: boolean = false; + let isWithinSingleQuote: boolean = false; + for (let i: number = 0; i < argsString.length; i++) { + const c: string = argsString[i]; + if (c === '\\') { + currentArg += c; + if (++i === argsString.length) { + if (currentArg !== "") { + result.push(currentArg); + } + return result; + } + currentArg += argsString[i]; + continue; + } + if (c === '"') { + if (!isWithinSingleQuote) { + isWithinDoubleQuote = !isWithinDoubleQuote; + } + } else if (c === '\'') { + // On Windows, a single quote string is not allowed to join multiple args into a single arg + if (!isWindows) { + if (!isWithinDoubleQuote) { + isWithinSingleQuote = !isWithinSingleQuote; + } + } + } else if (c === ' ') { + if (!isWithinDoubleQuote && !isWithinSingleQuote) { + if (currentArg !== "") { + result.push(currentArg); + currentArg = ""; + } + continue; + } + } + currentArg += c; + } + if (currentArg !== "") { + result.push(currentArg); + } + return result; +} + +function extractArgs(argsString: string): string[] { + argsString = argsString.trim(); + if (os.platform() === 'win32') { + argsString = resolveWindowsEnvironmentVariables(argsString); + const result: string[] = []; + let currentArg: string = ""; + let isInQuote: boolean = false; + let wasInQuote: boolean = false; + let i: number = 0; + while (i < argsString.length) { + let c: string = argsString[i]; + if (c === '\"') { + if (!isInQuote) { + isInQuote = true; + wasInQuote = true; + ++i; + continue; + } + // Need to peek at next character. + if (++i === argsString.length) { + break; + } + c = argsString[i]; + if (c !== '\"') { + isInQuote = false; + } + // Fall through. If c was a quote character, it will be added as a literal. + } + if (c === '\\') { + let backslashCount: number = 1; + let reachedEnd: boolean = true; + while (++i !== argsString.length) { + c = argsString[i]; + if (c !== '\\') { + reachedEnd = false; + break; + } + ++backslashCount; + } + const still_escaping: boolean = (backslashCount % 2) !== 0; + if (!reachedEnd && c === '\"') { + backslashCount = Math.floor(backslashCount / 2); + } + while (backslashCount--) { + currentArg += '\\'; + } + if (reachedEnd) { + break; + } + // If not still escaping and a quote was found, it needs to be handled above. + if (!still_escaping && c === '\"') { + continue; + } + // Otherwise, fall through to handle c as a literal. + } + if (c === ' ' || c === '\t' || c === '\r' || c === '\n') { + if (!isInQuote) { + if (currentArg !== "" || wasInQuote) { + wasInQuote = false; + result.push(currentArg); + currentArg = ""; + } + i++; + continue; + } + } + currentArg += c; + i++; + } + if (currentArg !== "" || wasInQuote) { + result.push(currentArg); + } + return result; + } else { + try { + const wordexpResult: any = child_process.execFileSync(getExtensionFilePath("bin/cpptools-wordexp"), [argsString], { shell: false }); + if (wordexpResult === undefined) { + return []; + } + const jsonText: string = wordexpResult.toString(); + return jsonc.parse(jsonText, undefined, true) as any; + } catch { + return []; + } + } +} + +export function isCl(compilerPath: string): boolean { + const compilerPathLowercase: string = compilerPath.toLowerCase(); + return compilerPathLowercase === "cl" || compilerPathLowercase === "cl.exe" + || compilerPathLowercase.endsWith("\\cl.exe") || compilerPathLowercase.endsWith("/cl.exe") + || compilerPathLowercase.endsWith("\\cl") || compilerPathLowercase.endsWith("/cl"); +} + +/** CompilerPathAndArgs retains original casing of text input for compiler path and args */ +export interface CompilerPathAndArgs { + compilerPath?: string | null; + compilerName: string; + compilerArgs?: string[]; + compilerArgsFromCommandLineInPath: string[]; + allCompilerArgs: string[]; +} + +export function extractCompilerPathAndArgs(useLegacyBehavior: boolean, inputCompilerPath?: string | null, compilerArgs?: string[]): CompilerPathAndArgs { + let compilerPath: string | undefined | null = inputCompilerPath; + let compilerName: string = ""; + let compilerArgsFromCommandLineInPath: string[] = []; + if (compilerPath) { + compilerPath = compilerPath.trim(); + if (isCl(compilerPath) || checkExecutableWithoutExtensionExistsSync(compilerPath)) { + // If the path ends with cl, or if a file is found at that path, accept it without further validation. + compilerName = path.basename(compilerPath); + } else if (compilerPath.startsWith("\"") || (os.platform() !== 'win32' && compilerPath.startsWith("'"))) { + // If the string starts with a quote, treat it as a command line. + // Otherwise, a path with a leading quote would not be valid. + if (useLegacyBehavior) { + compilerArgsFromCommandLineInPath = legacyExtractArgs(compilerPath); + if (compilerArgsFromCommandLineInPath.length > 0) { + compilerPath = compilerArgsFromCommandLineInPath.shift(); + if (compilerPath) { + // Try to trim quotes from compiler path. + const tempCompilerPath: string[] | undefined = extractArgs(compilerPath); + if (tempCompilerPath && compilerPath.length > 0) { + compilerPath = tempCompilerPath[0]; + } + compilerName = path.basename(compilerPath); + } + } + } else { + compilerArgsFromCommandLineInPath = extractArgs(compilerPath); + if (compilerArgsFromCommandLineInPath.length > 0) { + compilerPath = compilerArgsFromCommandLineInPath.shift(); + if (compilerPath) { + compilerName = path.basename(compilerPath); + } + } + } + } else { + const spaceStart: number = compilerPath.lastIndexOf(" "); + if (spaceStart !== -1) { + // There is no leading quote, but a space suggests it might be a command line. + // Try processing it as a command line, and validate that by checking for the executable. + const potentialArgs: string[] = useLegacyBehavior ? legacyExtractArgs(compilerPath) : extractArgs(compilerPath); + let potentialCompilerPath: string | undefined = potentialArgs.shift(); + if (useLegacyBehavior) { + if (potentialCompilerPath) { + const tempCompilerPath: string[] | undefined = extractArgs(potentialCompilerPath); + if (tempCompilerPath && compilerPath.length > 0) { + potentialCompilerPath = tempCompilerPath[0]; + } + } + } + if (potentialCompilerPath) { + if (isCl(potentialCompilerPath) || checkExecutableWithoutExtensionExistsSync(potentialCompilerPath)) { + compilerArgsFromCommandLineInPath = potentialArgs; + compilerPath = potentialCompilerPath; + compilerName = path.basename(compilerPath); + } + } + } + } + } + let allCompilerArgs: string[] = !compilerArgs ? [] : compilerArgs; + allCompilerArgs = allCompilerArgs.concat(compilerArgsFromCommandLineInPath); + return { compilerPath, compilerName, compilerArgs, compilerArgsFromCommandLineInPath, allCompilerArgs }; +} + +export function escapeForSquiggles(s: string): string { + // Replace all \ with \\, except for \" + // Otherwise, the JSON.parse result will have the \ missing. + let newResults: string = ""; + let lastWasBackslash: boolean = false; + let lastBackslashWasEscaped: boolean = false; + for (let i: number = 0; i < s.length; i++) { + if (s[i] === '\\') { + if (lastWasBackslash) { + newResults += "\\"; + lastBackslashWasEscaped = !lastBackslashWasEscaped; + } else { + lastBackslashWasEscaped = false; + } + newResults += "\\"; + lastWasBackslash = true; + } else { + if (lastWasBackslash && (lastBackslashWasEscaped || (s[i] !== '"'))) { + newResults += "\\"; + } + lastWasBackslash = false; + lastBackslashWasEscaped = false; + newResults += s[i]; + } + } + if (lastWasBackslash) { + newResults += "\\"; + } + return newResults; +} + +export function getSenderType(sender?: any): string { + if (isString(sender)) { + return sender; + } else if (isUri(sender)) { + return 'contextMenu'; + } + return 'commandPalette'; +} + +function decodeUCS16(input: string): number[] { + const output: number[] = []; + let counter: number = 0; + const length: number = input.length; + let value: number; + let extra: number; + while (counter < length) { + value = input.charCodeAt(counter++); + // eslint-disable-next-line no-bitwise + if ((value & 0xF800) === 0xD800 && counter < length) { + // high surrogate, and there is a next character + extra = input.charCodeAt(counter++); + // eslint-disable-next-line no-bitwise + if ((extra & 0xFC00) === 0xDC00) { // low surrogate + // eslint-disable-next-line no-bitwise + output.push(((value & 0x3FF) << 10) + (extra & 0x3FF) + 0x10000); + } else { + output.push(value, extra); + } + } else { + output.push(value); + } + } + return output; +} + +const allowedIdentifierUnicodeRanges: number[][] = [ + [0x0030, 0x0039], // digits + [0x0041, 0x005A], // upper case letters + [0x005F, 0x005F], // underscore + [0x0061, 0x007A], // lower case letters + [0x00A8, 0x00A8], // DIARESIS + [0x00AA, 0x00AA], // FEMININE ORDINAL INDICATOR + [0x00AD, 0x00AD], // SOFT HYPHEN + [0x00AF, 0x00AF], // MACRON + [0x00B2, 0x00B5], // SUPERSCRIPT TWO - MICRO SIGN + [0x00B7, 0x00BA], // MIDDLE DOT - MASCULINE ORDINAL INDICATOR + [0x00BC, 0x00BE], // VULGAR FRACTION ONE QUARTER - VULGAR FRACTION THREE QUARTERS + [0x00C0, 0x00D6], // LATIN CAPITAL LETTER A WITH GRAVE - LATIN CAPITAL LETTER O WITH DIAERESIS + [0x00D8, 0x00F6], // LATIN CAPITAL LETTER O WITH STROKE - LATIN SMALL LETTER O WITH DIAERESIS + [0x00F8, 0x167F], // LATIN SMALL LETTER O WITH STROKE - CANADIAN SYLLABICS BLACKFOOT W + [0x1681, 0x180D], // OGHAM LETTER BEITH - MONGOLIAN FREE VARIATION SELECTOR THREE + [0x180F, 0x1FFF], // SYRIAC LETTER BETH - GREEK DASIA + [0x200B, 0x200D], // ZERO WIDTH SPACE - ZERO WIDTH JOINER + [0x202A, 0x202E], // LEFT-TO-RIGHT EMBEDDING - RIGHT-TO-LEFT OVERRIDE + [0x203F, 0x2040], // UNDERTIE - CHARACTER TIE + [0x2054, 0x2054], // INVERTED UNDERTIE + [0x2060, 0x218F], // WORD JOINER - TURNED DIGIT THREE + [0x2460, 0x24FF], // CIRCLED DIGIT ONE - NEGATIVE CIRCLED DIGIT ZERO + [0x2776, 0x2793], // DINGBAT NEGATIVE CIRCLED DIGIT ONE - DINGBAT NEGATIVE CIRCLED SANS-SERIF NUMBER TEN + [0x2C00, 0x2DFF], // GLAGOLITIC CAPITAL LETTER AZU - COMBINING CYRILLIC LETTER IOTIFIED BIG YUS + [0x2E80, 0x2FFF], // CJK RADICAL REPEAT - IDEOGRAPHIC DESCRIPTION CHARACTER OVERLAID + [0x3004, 0x3007], // JAPANESE INDUSTRIAL STANDARD SYMBOL - IDEOGRAPHIC NUMBER ZERO + [0x3021, 0x302F], // HANGZHOU NUMERAL ONE - HANGUL DOUBLE DOT TONE MARK + [0x3031, 0xD7FF], // VERTICAL KANA REPEAT MARK - HANGUL JONGSEONG PHIEUPH-THIEUTH + [0xF900, 0xFD3D], // CJK COMPATIBILITY IDEOGRAPH-F900 - ARABIC LIGATURE ALEF WITH FATHATAN ISOLATED FORM + [0xFD40, 0xFDCF], // ARABIC LIGATURE TEH WITH JEEM WITH MEEM INITIAL FORM - ARABIC LIGATURE NOON WITH JEEM WITH YEH FINAL FORM + [0xFDF0, 0xFE44], // ARABIC LIGATURE SALLA USED AS KORANIC STOP SIGN ISOLATED FORM - PRESENTATION FORM FOR VERTICAL RIGHT WHITE CORNER BRACKET + [0xFE47, 0xFFFD], // PRESENTATION FORM FOR VERTICAL LEFT SQUARE BRACKET - REPLACEMENT CHARACTER + [0x10000, 0x1FFFD], // LINEAR B SYLLABLE B008 A - CHEESE WEDGE (U+1F9C0) + [0x20000, 0x2FFFD], // + [0x30000, 0x3FFFD], // + [0x40000, 0x4FFFD], // + [0x50000, 0x5FFFD], // + [0x60000, 0x6FFFD], // + [0x70000, 0x7FFFD], // + [0x80000, 0x8FFFD], // + [0x90000, 0x9FFFD], // + [0xA0000, 0xAFFFD], // + [0xB0000, 0xBFFFD], // + [0xC0000, 0xCFFFD], // + [0xD0000, 0xDFFFD], // + [0xE0000, 0xEFFFD] // LANGUAGE TAG (U+E0001) - VARIATION SELECTOR-256 (U+E01EF) +]; + +const disallowedFirstCharacterIdentifierUnicodeRanges: number[][] = [ + [0x0030, 0x0039], // digits + [0x0300, 0x036F], // COMBINING GRAVE ACCENT - COMBINING LATIN SMALL LETTER X + [0x1DC0, 0x1DFF], // COMBINING DOTTED GRAVE ACCENT - COMBINING RIGHT ARROWHEAD AND DOWN ARROWHEAD BELOW + [0x20D0, 0x20FF], // COMBINING LEFT HARPOON ABOVE - COMBINING ASTERISK ABOVE + [0xFE20, 0xFE2F] // COMBINING LIGATURE LEFT HALF - COMBINING CYRILLIC TITLO RIGHT HALF +]; + +export function isValidIdentifier(candidate: string): boolean { + if (!candidate) { + return false; + } + const decoded: number[] = decodeUCS16(candidate); + if (!decoded || !decoded.length) { + return false; + } + + // Reject if first character is disallowed + for (let i: number = 0; i < disallowedFirstCharacterIdentifierUnicodeRanges.length; i++) { + const disallowedCharacters: number[] = disallowedFirstCharacterIdentifierUnicodeRanges[i]; + if (decoded[0] >= disallowedCharacters[0] && decoded[0] <= disallowedCharacters[1]) { + return false; + } + } + + for (let position: number = 0; position < decoded.length; position++) { + let found: boolean = false; + for (let i: number = 0; i < allowedIdentifierUnicodeRanges.length; i++) { + const allowedCharacters: number[] = allowedIdentifierUnicodeRanges[i]; + if (decoded[position] >= allowedCharacters[0] && decoded[position] <= allowedCharacters[1]) { + found = true; + break; + } + } + if (!found) { + return false; + } + } + return true; +} + +export function getCacheStoragePath(): string { + let defaultCachePath: string = ""; + let pathEnvironmentVariable: string | undefined; + switch (os.platform()) { + case 'win32': + defaultCachePath = "Microsoft\\vscode-cpptools\\"; + pathEnvironmentVariable = process.env.LOCALAPPDATA; + break; + case 'darwin': + defaultCachePath = "Library/Caches/vscode-cpptools/"; + pathEnvironmentVariable = os.homedir(); + break; + default: // Linux + defaultCachePath = "vscode-cpptools/"; + pathEnvironmentVariable = process.env.XDG_CACHE_HOME; + if (!pathEnvironmentVariable) { + pathEnvironmentVariable = path.join(os.homedir(), ".cache"); + } + break; + } + + return pathEnvironmentVariable ? path.join(pathEnvironmentVariable, defaultCachePath) : ""; +} + +function getUniqueWorkspaceNameHelper(workspaceFolder: vscode.WorkspaceFolder, addSubfolder: boolean): string { + const workspaceFolderName: string = workspaceFolder ? workspaceFolder.name : "untitled"; + if (!workspaceFolder || workspaceFolder.index < 1) { + return workspaceFolderName; // No duplicate names to search for. + } + for (let i: number = 0; i < workspaceFolder.index; ++i) { + if (vscode.workspace.workspaceFolders && vscode.workspace.workspaceFolders.length > 0 && vscode.workspace.workspaceFolders[i].name === workspaceFolderName) { + return addSubfolder ? path.join(workspaceFolderName, String(workspaceFolder.index)) : // Use the index as a subfolder. + workspaceFolderName + String(workspaceFolder.index); + } + } + return workspaceFolderName; // No duplicate names found. +} + +export function getUniqueWorkspaceName(workspaceFolder: vscode.WorkspaceFolder): string { + return getUniqueWorkspaceNameHelper(workspaceFolder, false); +} + +export function getUniqueWorkspaceStorageName(workspaceFolder: vscode.WorkspaceFolder): string { + return getUniqueWorkspaceNameHelper(workspaceFolder, true); +} + +export function isCodespaces(): boolean { + return !!process.env.CODESPACES; +} + +// Sequentially Resolve Promises. +export function sequentialResolve(items: T[], promiseBuilder: (item: T) => Promise): Promise { + return items.reduce(async (previousPromise, nextItem) => { + await previousPromise; + return promiseBuilder(nextItem); + }, Promise.resolve()); +} + +export function quoteArgument(argument: string): string { + // Return the argument as is if it's empty + if (!argument) { + return argument; + } + + if (os.platform() === "win32") { + // Windows-style quoting logic + if (!/[\s\t\n\v\"\\&%^]/.test(argument)) { + return argument; + } + + let quotedArgument = '"'; + let backslashCount = 0; + + for (const char of argument) { + if (char === '\\') { + backslashCount++; + } else { + if (char === '"') { + quotedArgument += '\\'.repeat(backslashCount * 2 + 1); + } else { + quotedArgument += '\\'.repeat(backslashCount); + } + quotedArgument += char; + backslashCount = 0; + } + } + + quotedArgument += '\\'.repeat(backslashCount * 2); + quotedArgument += '"'; + return quotedArgument; + } else { + // Unix-style quoting logic + if (!/[\s\t\n\v\"'\\$`|;&(){}<>*?!\[\]~^#%]/.test(argument)) { + return argument; + } + + let quotedArgument = "'"; + for (const c of argument) { + if (c === "'") { + quotedArgument += "'\\''"; + } else { + quotedArgument += c; + } + } + + quotedArgument += "'"; + return quotedArgument; + } +} + +/** + * Find PowerShell executable from PATH (for Windows only). + */ +export function findPowerShell(): string | undefined { + const dirs: string[] = (process.env.PATH || '').replace(/"+/g, '').split(';').filter(x => x); + const exts: string[] = (process.env.PATHEXT || '').split(';'); + const names: string[] = ['pwsh', 'powershell']; + for (const name of names) { + const candidates: string[] = dirs.reduce((paths, dir) => [ + ...paths, ...exts.map(ext => path.join(dir, name + ext)) + ], []); + for (const candidate of candidates) { + try { + if (fs.statSync(candidate).isFile()) { + return name; + } + } catch (e) { + return undefined; + } + } + } +} + +export function getCppToolsTargetPopulation(): TargetPopulation { + // If insiders.flag is present, consider this an insiders build. + // If release.flag is present, consider this a release build. + // Otherwise, consider this an internal build. + if (checkFileExistsSync(getExtensionFilePath("insiders.flag"))) { + return TargetPopulation.Insiders; + } else if (checkFileExistsSync(getExtensionFilePath("release.flag"))) { + return TargetPopulation.Public; + } + return TargetPopulation.Internal; +} + +export function isVsCodeInsiders(): boolean { + return extensionPath.includes(".vscode-insiders") || + extensionPath.includes(".vscode-server-insiders") || + extensionPath.includes(".vscode-exploration") || + extensionPath.includes(".vscode-server-exploration"); +} + +export function stripEscapeSequences(str: string): string { + return str + // eslint-disable-next-line no-control-regex + .replace(/\x1b\[\??[0-9]{0,3}(;[0-9]{1,3})?[a-zA-Z]/g, '') + // eslint-disable-next-line no-control-regex + .replace(/\u0008/g, '') + .replace(/\r/g, ''); +} + +export function splitLines(data: string): string[] { + return data.split(/\r?\n/g); +} + +export function escapeStringForRegex(str: string): string { + return str.replace(/([.*+?^=!:${}()|\[\]\/\\])/g, '\\$1'); +} + +export function replaceAll(str: string, searchValue: string, replaceValue: string): string { + const pattern: string = escapeStringForRegex(searchValue); + const re: RegExp = new RegExp(pattern, 'g'); + return str.replace(re, replaceValue); +} + +export interface ISshHostInfo { + hostName: string; + user?: string; + port?: number | string; +} + +export interface ISshConfigHostInfo extends ISshHostInfo { + file: string; +} + +/** user@host */ +export function getFullHostAddressNoPort(host: ISshHostInfo): string { + return host.user ? `${host.user}@${host.hostName}` : `${host.hostName}`; +} + +export function getFullHostAddress(host: ISshHostInfo): string { + const fullHostName: string = getFullHostAddressNoPort(host); + return host.port ? `${fullHostName}:${host.port}` : fullHostName; +} + +export interface ISshLocalForwardInfo { + bindAddress?: string; + port?: number | string; + host?: string; + hostPort?: number | string; + localSocket?: string; + remoteSocket?: string; +} + +export function whichAsync(name: string): Promise { + return new Promise(resolve => { + which(name, (err, resolved) => { + if (err) { + resolve(undefined); + } else { + resolve(resolved); + } + }); + }); +} + +export const documentSelector: DocumentFilter[] = [ + { scheme: 'file', language: 'c' }, + { scheme: 'file', language: 'cpp' }, + { scheme: 'file', language: 'cuda-cpp' } +]; + +export function hasMsvcEnvironment(): boolean { + const msvcEnvVars: string[] = [ + 'DevEnvDir', + 'Framework40Version', + 'FrameworkDir', + 'FrameworkVersion', + 'INCLUDE', + 'LIB', + 'LIBPATH', + 'NETFXSDKDir', + 'UCRTVersion', + 'UniversalCRTSdkDir', + 'VCIDEInstallDir', + 'VCINSTALLDIR', + 'VCToolsRedistDir', + 'VisualStudioVersion', + 'VSINSTALLDIR', + 'WindowsLibPath', + 'WindowsSdkBinPath', + 'WindowsSdkDir', + 'WindowsSDKLibVersion', + 'WindowsSDKVersion' + ]; + return msvcEnvVars.every((envVarName) => process.env[envVarName] !== undefined && process.env[envVarName] !== ''); +} + +function isIntegral(str: string): boolean { + const regex = /^-?\d+$/; + return regex.test(str); +} + +export function getLoggingLevel() { + return getNumericLoggingLevel(vscode.workspace.getConfiguration("C_Cpp", null).get("loggingLevel")); +} + +export function getNumericLoggingLevel(loggingLevel: string | undefined): number { + if (!loggingLevel) { + return 1; + } + if (isIntegral(loggingLevel)) { + return parseInt(loggingLevel, 10); + } + const lowerCaseLoggingLevel: string = loggingLevel.toLowerCase(); + switch (lowerCaseLoggingLevel) { + case "error": + return 1; + case "warning": + return 3; + case "information": + return 5; + case "debug": + return 6; + case "none": + return 0; + default: + return -1; + } +} + +export function mergeOverlappingRanges(ranges: Range[]): Range[] { + // Fix any reversed ranges. Not sure if this is needed, but ensures the input is sanitized. + const mergedRanges: Range[] = ranges.map(range => { + if (range.start.line > range.end.line || (range.start.line === range.end.line && range.start.character > range.end.character)) { + return Range.create(range.end, range.start); + } + return range; + }); + + // Merge overlapping ranges. + mergedRanges.sort((a, b) => a.start.line - b.start.line || a.start.character - b.start.character); + let lastMergedIndex = 0; // Index to keep track of the last merged range + for (let currentIndex = 0; currentIndex < ranges.length; currentIndex++) { + const currentRange = ranges[currentIndex]; // No need for a shallow copy, since we're not modifying the ranges we haven't read yet. + let nextIndex = currentIndex + 1; + while (nextIndex < ranges.length) { + const nextRange = ranges[nextIndex]; + // Check for non-overlapping ranges first + if (nextRange.start.line > currentRange.end.line || + (nextRange.start.line === currentRange.end.line && nextRange.start.character > currentRange.end.character)) { + break; + } + // Otherwise, merge the overlapping ranges + currentRange.end = { + line: Math.max(currentRange.end.line, nextRange.end.line), + character: Math.max(currentRange.end.character, nextRange.end.character) + }; + nextIndex++; + } + // Overwrite the array in-place + mergedRanges[lastMergedIndex] = currentRange; + lastMergedIndex++; + currentIndex = nextIndex - 1; // Skip the merged ranges + } + mergedRanges.length = lastMergedIndex; + return mergedRanges; +} + +// Arg quoting utility functions, copied from VS Code with minor changes. + +export interface IShellQuotingOptions { + /** + * The character used to do character escaping. + */ + escape?: string | { + escapeChar: string; + charsToEscape: string; + }; + + /** + * The character used for string quoting. + */ + strong?: string; + + /** + * The character used for weak quoting. + */ + weak?: string; +} + +export interface IQuotedString { + value: string; + quoting: 'escape' | 'strong' | 'weak'; +} + +export type CommandString = string | IQuotedString; + +export function buildShellCommandLine(originalCommand: CommandString, command: CommandString, args: CommandString[]): string { + + let shellQuoteOptions: IShellQuotingOptions; + const isWindows: boolean = os.platform() === 'win32'; + if (isWindows) { + shellQuoteOptions = { + strong: '"' + }; + } else { + shellQuoteOptions = { + escape: { + escapeChar: '\\', + charsToEscape: ' "\'' + }, + strong: '\'', + weak: '"' + }; + } + + // TODO: Support launching with PowerShell + // For PowerShell: + // { + // escape: { + // escapeChar: '`', + // charsToEscape: ' "\'()' + // }, + // strong: '\'', + // weak: '"' + // }, + + function needsQuotes(value: string): boolean { + if (value.length >= 2) { + const first = value[0] === shellQuoteOptions.strong ? shellQuoteOptions.strong : value[0] === shellQuoteOptions.weak ? shellQuoteOptions.weak : undefined; + if (first === value[value.length - 1]) { + return false; + } + } + let quote: string | undefined; + for (let i = 0; i < value.length; i++) { + // We found the end quote. + const ch = value[i]; + if (ch === quote) { + quote = undefined; + } else if (quote !== undefined) { + // skip the character. We are quoted. + continue; + } else if (ch === shellQuoteOptions.escape) { + // Skip the next character + i++; + } else if (ch === shellQuoteOptions.strong || ch === shellQuoteOptions.weak) { + quote = ch; + } else if (ch === ' ') { + return true; + } + } + return false; + } + + function quote(value: string, kind: 'escape' | 'strong' | 'weak'): [string, boolean] { + if (kind === "strong" && shellQuoteOptions.strong) { + return [shellQuoteOptions.strong + value + shellQuoteOptions.strong, true]; + } else if (kind === "weak" && shellQuoteOptions.weak) { + return [shellQuoteOptions.weak + value + shellQuoteOptions.weak, true]; + } else if (kind === "escape" && shellQuoteOptions.escape) { + if (isString(shellQuoteOptions.escape)) { + return [value.replace(/ /g, shellQuoteOptions.escape + ' '), true]; + } else { + const buffer: string[] = []; + for (const ch of shellQuoteOptions.escape.charsToEscape) { + buffer.push(`\\${ch}`); + } + const regexp: RegExp = new RegExp('[' + buffer.join(',') + ']', 'g'); + const escapeChar = shellQuoteOptions.escape.escapeChar; + return [value.replace(regexp, (match) => escapeChar + match), true]; + } + } + return [value, false]; + } + + function quoteIfNecessary(value: CommandString): [string, boolean] { + if (isString(value)) { + if (needsQuotes(value)) { + return quote(value, "strong"); + } else { + return [value, false]; + } + } else { + return quote(value.value, value.quoting); + } + } + + // If we have no args and the command is a string then use the command to stay backwards compatible with the old command line + // model. To allow variable resolving with spaces we do continue if the resolved value is different than the original one + // and the resolved one needs quoting. + if ((!args || args.length === 0) && isString(command) && (command === originalCommand as string || needsQuotes(originalCommand as string))) { + return command; + } + + const result: string[] = []; + let commandQuoted = false; + let argQuoted = false; + let value: string; + let quoted: boolean; + [value, quoted] = quoteIfNecessary(command); + result.push(value); + commandQuoted = quoted; + for (const arg of args) { + [value, quoted] = quoteIfNecessary(arg); + result.push(value); + argQuoted = argQuoted || quoted; + } + + let commandLine = result.join(' '); + // There are special rules quoted command line in cmd.exe + if (isWindows) { + commandLine = `chcp 65001>nul && ${commandLine}`; + if (commandQuoted && argQuoted) { + commandLine = '"' + commandLine + '"'; + } + commandLine = `cmd /c ${commandLine}`; + } + + return commandLine; +} + +export function findExePathInArgs(args: CommandString[]): string | undefined { + const isWindows: boolean = os.platform() === 'win32'; + let previousArg: string | undefined; + + for (const arg of args) { + const argValue = isString(arg) ? arg : arg.value; + if (previousArg === '-o') { + return argValue; + } + if (isWindows && argValue.includes('.exe')) { + if (argValue.startsWith('/Fe')) { + return argValue.substring(3); + } else if (argValue.toLowerCase().startsWith('/out:')) { + return argValue.substring(5); + } + } + + previousArg = argValue; + } + + return undefined; +} + +export function getVsCodeVersion(): number[] { + return vscode.version.split('.').map(num => parseInt(num, undefined)); +} diff --git a/Extension/src/cppTools.ts b/Extension/src/cppTools.ts index ca6bb98613..ccc6036564 100644 --- a/Extension/src/cppTools.ts +++ b/Extension/src/cppTools.ts @@ -1,135 +1,130 @@ -/* -------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All Rights Reserved. - * See 'LICENSE' in the project root for license information. - * ------------------------------------------------------------------------------------------ */ -'use strict'; - -import { CustomConfigurationProvider, Version } from 'vscode-cpptools'; -import { CppToolsTestApi, CppToolsTestHook } from 'vscode-cpptools/out/testApi'; -import * as nls from 'vscode-nls'; -import { CustomConfigurationProvider1, CustomConfigurationProviderCollection, getCustomConfigProviders } from './LanguageServer/customProviders'; -import * as LanguageServer from './LanguageServer/extension'; -import { CppSettings } from './LanguageServer/settings'; -import { getNumericLoggingLevel } from './common'; -import { getOutputChannel } from './logger'; -import * as test from './testHook'; - -nls.config({ messageFormat: nls.MessageFormat.bundle, bundleFormat: nls.BundleFormat.standalone })(); -const localize: nls.LocalizeFunc = nls.loadMessageBundle(); - -export class CppTools implements CppToolsTestApi { - private version: Version; - private providers: CustomConfigurationProvider1[] = []; - private failedRegistrations: CustomConfigurationProvider[] = []; - private timers = new Map(); - - constructor(version: Version) { - if (version > Version.latest) { - console.warn(`version ${version} is not supported by this version of cpptools`); - console.warn(` using ${Version.latest} instead`); - version = Version.latest; - } - this.version = version; - } - - private addNotifyReadyTimer(provider: CustomConfigurationProvider1): void { - if (this.version >= Version.v2) { - const timeout: number = 30; - const timer: NodeJS.Timeout = global.setTimeout(() => { - console.warn(`registered provider ${provider.extensionId} did not call 'notifyReady' within ${timeout} seconds`); - }, timeout * 1000); - this.timers.set(provider.extensionId, timer); - } - } - - private removeNotifyReadyTimer(provider: CustomConfigurationProvider1): void { - if (this.version >= Version.v2) { - const timer: NodeJS.Timeout | undefined = this.timers.get(provider.extensionId); - if (timer) { - this.timers.delete(provider.extensionId); - clearTimeout(timer); - } - } - } - - public getVersion(): Version { - return this.version; - } - - public registerCustomConfigurationProvider(provider: CustomConfigurationProvider): void { - const providers: CustomConfigurationProviderCollection = getCustomConfigProviders(); - if (providers.add(provider, this.version)) { - const added: CustomConfigurationProvider1 | undefined = providers.get(provider); - if (added) { - const settings: CppSettings = new CppSettings(); - if (getNumericLoggingLevel(settings.loggingLevel) >= 5) { - getOutputChannel().appendLine(localize("provider.registered", "Custom configuration provider '{0}' registered", added.name)); - } - this.providers.push(added); - LanguageServer.getClients().forEach(client => void client.onRegisterCustomConfigurationProvider(added)); - this.addNotifyReadyTimer(added); - } - } else { - this.failedRegistrations.push(provider); - } - } - - public notifyReady(provider: CustomConfigurationProvider): void { - const providers: CustomConfigurationProviderCollection = getCustomConfigProviders(); - const p: CustomConfigurationProvider1 | undefined = providers.get(provider); - - if (p) { - this.removeNotifyReadyTimer(p); - p.isReady = true; - LanguageServer.getClients().forEach(client => { - void client.updateCustomBrowseConfiguration(p); - void client.updateCustomConfigurations(p); - }); - } else if (this.failedRegistrations.find(p => p === provider)) { - console.warn("provider not successfully registered; 'notifyReady' ignored"); - } else { - console.warn("provider should be registered before signaling it's ready to provide configurations"); - } - } - - public didChangeCustomConfiguration(provider: CustomConfigurationProvider): void { - const providers: CustomConfigurationProviderCollection = getCustomConfigProviders(); - const p: CustomConfigurationProvider1 | undefined = providers.get(provider); - - if (p) { - if (!p.isReady) { - console.warn("didChangeCustomConfiguration was invoked before notifyReady"); - } - LanguageServer.getClients().forEach(client => void client.updateCustomConfigurations(p)); - } else if (this.failedRegistrations.find(p => p === provider)) { - console.warn("provider not successfully registered, 'didChangeCustomConfiguration' ignored"); - } else { - console.warn("provider should be registered before sending config change messages"); - } - } - - public didChangeCustomBrowseConfiguration(provider: CustomConfigurationProvider): void { - const providers: CustomConfigurationProviderCollection = getCustomConfigProviders(); - const p: CustomConfigurationProvider1 | undefined = providers.get(provider); - - if (p) { - LanguageServer.getClients().forEach(client => void client.updateCustomBrowseConfiguration(p)); - } else if (this.failedRegistrations.find(p => p === provider)) { - console.warn("provider not successfully registered, 'didChangeCustomBrowseConfiguration' ignored"); - } else { - console.warn("provider should be registered before sending config change messages"); - } - } - - public dispose(): void { - this.providers.forEach(provider => { - getCustomConfigProviders().remove(provider); - provider.dispose(); - }); - this.providers = []; - } - - public getTestHook(): CppToolsTestHook { - return test.getTestHook(); - } -} +/* -------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All Rights Reserved. + * See 'LICENSE' in the project root for license information. + * ------------------------------------------------------------------------------------------ */ +'use strict'; + +import { CustomConfigurationProvider, Version } from 'vscode-cpptools'; +import { CppToolsTestApi, CppToolsTestHook } from 'vscode-cpptools/out/testApi'; +import * as nls from 'vscode-nls'; +import { CustomConfigurationProvider1, CustomConfigurationProviderCollection, getCustomConfigProviders } from './LanguageServer/customProviders'; +import * as LanguageServer from './LanguageServer/extension'; +import { getOutputChannelLogger } from './logger'; +import * as test from './testHook'; + +nls.config({ messageFormat: nls.MessageFormat.bundle, bundleFormat: nls.BundleFormat.standalone })(); +const localize: nls.LocalizeFunc = nls.loadMessageBundle(); + +export class CppTools implements CppToolsTestApi { + private version: Version; + private providers: CustomConfigurationProvider1[] = []; + private failedRegistrations: CustomConfigurationProvider[] = []; + private timers = new Map(); + + constructor(version: Version) { + if (version > Version.latest) { + console.warn(`version ${version} is not supported by this version of cpptools`); + console.warn(` using ${Version.latest} instead`); + version = Version.latest; + } + this.version = version; + } + + private addNotifyReadyTimer(provider: CustomConfigurationProvider1): void { + if (this.version >= Version.v2) { + const timeout: number = 30; + const timer: NodeJS.Timeout = global.setTimeout(() => { + console.warn(`registered provider ${provider.extensionId} did not call 'notifyReady' within ${timeout} seconds`); + }, timeout * 1000); + this.timers.set(provider.extensionId, timer); + } + } + + private removeNotifyReadyTimer(provider: CustomConfigurationProvider1): void { + if (this.version >= Version.v2) { + const timer: NodeJS.Timeout | undefined = this.timers.get(provider.extensionId); + if (timer) { + this.timers.delete(provider.extensionId); + clearTimeout(timer); + } + } + } + + public getVersion(): Version { + return this.version; + } + + public registerCustomConfigurationProvider(provider: CustomConfigurationProvider): void { + const providers: CustomConfigurationProviderCollection = getCustomConfigProviders(); + if (providers.add(provider, this.version)) { + const added: CustomConfigurationProvider1 | undefined = providers.get(provider); + if (added) { + getOutputChannelLogger().appendLine(5, localize("provider.registered", "Custom configuration provider '{0}' registered", added.name)); + this.providers.push(added); + LanguageServer.getClients().forEach(client => void client.onRegisterCustomConfigurationProvider(added)); + this.addNotifyReadyTimer(added); + } + } else { + this.failedRegistrations.push(provider); + } + } + + public notifyReady(provider: CustomConfigurationProvider): void { + const providers: CustomConfigurationProviderCollection = getCustomConfigProviders(); + const p: CustomConfigurationProvider1 | undefined = providers.get(provider); + + if (p) { + this.removeNotifyReadyTimer(p); + p.isReady = true; + LanguageServer.getClients().forEach(client => { + void client.updateCustomBrowseConfiguration(p); + void client.updateCustomConfigurations(p); + }); + } else if (this.failedRegistrations.find(p => p === provider)) { + console.warn("provider not successfully registered; 'notifyReady' ignored"); + } else { + console.warn("provider should be registered before signaling it's ready to provide configurations"); + } + } + + public didChangeCustomConfiguration(provider: CustomConfigurationProvider): void { + const providers: CustomConfigurationProviderCollection = getCustomConfigProviders(); + const p: CustomConfigurationProvider1 | undefined = providers.get(provider); + + if (p) { + if (!p.isReady) { + console.warn("didChangeCustomConfiguration was invoked before notifyReady"); + } + LanguageServer.getClients().forEach(client => void client.updateCustomConfigurations(p)); + } else if (this.failedRegistrations.find(p => p === provider)) { + console.warn("provider not successfully registered, 'didChangeCustomConfiguration' ignored"); + } else { + console.warn("provider should be registered before sending config change messages"); + } + } + + public didChangeCustomBrowseConfiguration(provider: CustomConfigurationProvider): void { + const providers: CustomConfigurationProviderCollection = getCustomConfigProviders(); + const p: CustomConfigurationProvider1 | undefined = providers.get(provider); + + if (p) { + LanguageServer.getClients().forEach(client => void client.updateCustomBrowseConfiguration(p)); + } else if (this.failedRegistrations.find(p => p === provider)) { + console.warn("provider not successfully registered, 'didChangeCustomBrowseConfiguration' ignored"); + } else { + console.warn("provider should be registered before sending config change messages"); + } + } + + public dispose(): void { + this.providers.forEach(provider => { + getCustomConfigProviders().remove(provider); + provider.dispose(); + }); + this.providers = []; + } + + public getTestHook(): CppToolsTestHook { + return test.getTestHook(); + } +} diff --git a/Extension/src/instrumentation.ts b/Extension/src/instrumentation.ts new file mode 100644 index 0000000000..0a0a770f98 --- /dev/null +++ b/Extension/src/instrumentation.ts @@ -0,0 +1,69 @@ +/* -------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All Rights Reserved. + * See 'LICENSE' in the project root for license information. + * ------------------------------------------------------------------------------------------ */ +'use strict'; + +/* eslint @typescript-eslint/no-var-requires: "off" */ + +export interface PerfMessage | undefined> { + /** this is the 'name' of the event */ + name: string; + + /** this indicates the context or origin of the message */ + context: Record>; + + /** if the message contains complex data, it should be in here */ + data?: TInput; + + /** if the message is just a text message, this is the contents of the message */ + text?: string; + + /** the message can have a numeric value that indicates the 'level' or 'severity' etc */ + level?: number; +} + +const services = { + instrument: >(_instance: T, _options?: { ignore?: string[]; name?: string }): T => instance, + message: (_message: PerfMessage) => { }, + init: (_vscode: any) => { }, + launchSettings: undefined as Record | undefined +}; + +export function instrument>(instance: T, options?: { ignore?: string[]; name?: string }): T { + return services.instrument(instance, options); +} + +/** sends a perf message to the monitor */ +export function sendInstrumentation(message: PerfMessage): void { + services.message(message); +} + +/** verifies that the instrumentation is loaded into the global namespace */ +export function isInstrumentationEnabled(): boolean { + return !!(global as any).instrumentation; +} + +// if the instrumentation code is not globally loaded yet, then load it now +if (!isInstrumentationEnabled()) { + // pull the launch settings from the environment if the variable has been set. + if (services.launchSettings === undefined) { + services.launchSettings = process.env.PERFECTO_LAUNCH_SETTINGS ? JSON.parse(process.env.PERFECTO_LAUNCH_SETTINGS) as Record : { tests: [], collector: undefined }; + } + + // this loads the bootstrap module (global-instrumentation-support) which adds some global functions + if (services.launchSettings?.bootstrapModule) { + void require(services.launchSettings.bootstrapModule); + } +} + +// If the instrumentation object was loaded then we can set the services from the global object +if ((global as any).instrumentation) { + // instrumentation services provided by the tool + services.instrument = (global as any).instrumentation.instrument; + services.message = (global as any).instrumentation.message; + services.init = (global as any).instrumentation.init; + + services.init(require('vscode')); +} + diff --git a/Extension/src/logger.ts b/Extension/src/logger.ts index 4dba0e1d3e..e85ef10538 100644 --- a/Extension/src/logger.ts +++ b/Extension/src/logger.ts @@ -1,190 +1,194 @@ -/* -------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All Rights Reserved. - * See 'LICENSE' in the project root for license information. - * ------------------------------------------------------------------------------------------ */ -'use strict'; - -import * as os from 'os'; -import * as vscode from 'vscode'; -import * as nls from 'vscode-nls'; -import { getNumericLoggingLevel } from './common'; -import { CppSourceStr } from './LanguageServer/extension'; -import { getLocalizedString, LocalizeStringParams } from './LanguageServer/localization'; - -nls.config({ messageFormat: nls.MessageFormat.bundle, bundleFormat: nls.BundleFormat.standalone })(); -const localize: nls.LocalizeFunc = nls.loadMessageBundle(); - -// This is used for testing purposes -let Subscriber: (message: string) => void; -export function subscribeToAllLoggers(subscriber: (message: string) => void): void { - Subscriber = subscriber; -} - -export class Logger { - private writer: (message: string) => void; - - constructor(writer: (message: string) => void) { - this.writer = writer; - } - - public append(message: string): void { - this.writer(message); - if (Subscriber) { - Subscriber(message); - } - } - - public appendLine(message: string): void { - this.writer(message + os.EOL); - if (Subscriber) { - Subscriber(message + os.EOL); - } - } - - // We should not await on this function. - public showInformationMessage(message: string, items?: string[]): Thenable { - this.appendLine(message); - - if (!items) { - return vscode.window.showInformationMessage(message); - } - return vscode.window.showInformationMessage(message, ...items); - } - - // We should not await on this function. - public showWarningMessage(message: string, items?: string[]): Thenable { - this.appendLine(message); - - if (!items) { - return vscode.window.showWarningMessage(message); - } - return vscode.window.showWarningMessage(message, ...items); - } - - // We should not await on this function. - public showErrorMessage(message: string, items?: string[]): Thenable { - this.appendLine(message); - - if (!items) { - return vscode.window.showErrorMessage(message); - } - return vscode.window.showErrorMessage(message, ...items); - } -} - -export let outputChannel: vscode.OutputChannel | undefined; -export let diagnosticsChannel: vscode.OutputChannel | undefined; -export let crashCallStacksChannel: vscode.OutputChannel | undefined; -export let debugChannel: vscode.OutputChannel | undefined; -export let warningChannel: vscode.OutputChannel | undefined; -export let sshChannel: vscode.OutputChannel | undefined; - -export function getOutputChannel(): vscode.OutputChannel { - if (!outputChannel) { - outputChannel = vscode.window.createOutputChannel(CppSourceStr); - // Do not use CppSettings to avoid circular require() - const settings: vscode.WorkspaceConfiguration = vscode.workspace.getConfiguration("C_Cpp", null); - const loggingLevel: string | undefined = settings.get("loggingLevel"); - if (getNumericLoggingLevel(loggingLevel) > 1) { - outputChannel.appendLine(`loggingLevel: ${loggingLevel}`); - } - } - return outputChannel; -} - -export function getDiagnosticsChannel(): vscode.OutputChannel { - if (!diagnosticsChannel) { - diagnosticsChannel = vscode.window.createOutputChannel(localize("c.cpp.diagnostics", "C/C++ Diagnostics")); - } - return diagnosticsChannel; -} - -export function getCrashCallStacksChannel(): vscode.OutputChannel { - if (!crashCallStacksChannel) { - crashCallStacksChannel = vscode.window.createOutputChannel(localize("c.cpp.crash.call.stacks.title", "C/C++ Crash Call Stacks")); - crashCallStacksChannel.appendLine(localize({ key: "c.cpp.crash.call.stacks.description", comment: ["{0} is a URL."] }, - "A C/C++ extension process has crashed. The crashing process name, date/time, signal, and call stack are below -- it would be helpful to include that in a bug report at {0}.", - "https://github.com/Microsoft/vscode-cpptools/issues")); - } - return crashCallStacksChannel; -} - -export function getSshChannel(): vscode.OutputChannel { - if (!sshChannel) { - sshChannel = vscode.window.createOutputChannel(localize("c.cpp.ssh.channel", "{0}: SSH", "Cpptools")); - } - return sshChannel; -} - -export function showOutputChannel(): void { - getOutputChannel().show(); -} - -let outputChannelLogger: Logger | undefined; - -export function getOutputChannelLogger(): Logger { - if (!outputChannelLogger) { - outputChannelLogger = new Logger(message => getOutputChannel().append(message)); - } - return outputChannelLogger; -} - -export function log(output: string): void { - if (!outputChannel) { - outputChannel = getOutputChannel(); - } - outputChannel.appendLine(`${output}`); -} - -export interface DebugProtocolParams { - jsonrpc: string; - method: string; - params?: any; -} - -export function logDebugProtocol(output: DebugProtocolParams): void { - if (!debugChannel) { - debugChannel = vscode.window.createOutputChannel(`${localize("c.cpp.debug.protocol", "C/C++ Debug Protocol")}`); - } - debugChannel.appendLine(""); - debugChannel.appendLine("************************************************************************************************************************"); - debugChannel.append(`${output}`); -} - -export interface ShowWarningParams { - localizeStringParams: LocalizeStringParams; -} - -export function showWarning(params: ShowWarningParams): void { - const message: string = getLocalizedString(params.localizeStringParams); - let showChannel: boolean = false; - if (!warningChannel) { - warningChannel = vscode.window.createOutputChannel(`${localize("c.cpp.warnings", "C/C++ Configuration Warnings")}`); - showChannel = true; - } - // Append before showing the channel, to avoid a delay. - warningChannel.appendLine(`[${new Date().toLocaleString()}] ${message}`); - if (showChannel) { - warningChannel.show(true); - } -} - -export function logLocalized(params: LocalizeStringParams): void { - const output: string = getLocalizedString(params); - log(output); -} - -export function disposeOutputChannels(): void { - if (outputChannel) { - outputChannel.dispose(); - } - if (diagnosticsChannel) { - diagnosticsChannel.dispose(); - } - if (debugChannel) { - debugChannel.dispose(); - } - if (warningChannel) { - warningChannel.dispose(); - } -} +/* -------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All Rights Reserved. + * See 'LICENSE' in the project root for license information. + * ------------------------------------------------------------------------------------------ */ +'use strict'; + +import * as os from 'os'; +import * as vscode from 'vscode'; +import * as nls from 'vscode-nls'; +import { getLoggingLevel } from './common'; +import { sendInstrumentation } from './instrumentation'; +import { CppSourceStr } from './LanguageServer/extension'; +import { getLocalizedString, LocalizeStringParams } from './LanguageServer/localization'; + +nls.config({ messageFormat: nls.MessageFormat.bundle, bundleFormat: nls.BundleFormat.standalone })(); +const localize: nls.LocalizeFunc = nls.loadMessageBundle(); + +// This is used for testing purposes +let Subscriber: (message: string) => void; +export function subscribeToAllLoggers(subscriber: (message: string) => void): void { + Subscriber = subscriber; +} + +export class Logger { + private writer: (message: string) => void; + + constructor(writer: (message: string) => void) { + this.writer = writer; + } + + public append(level: number, message: string): void; + public append(message: string): void; + public append(levelOrMessage: string | number, message?: string): void { + message = message || levelOrMessage as string; + const hasLevel = typeof levelOrMessage === 'number'; + + if (!hasLevel || getLoggingLevel() >= levelOrMessage) { + this.writer(message); + if (Subscriber) { + Subscriber(message); + } + } + sendInstrumentation({ name: 'log', text: message, context: { channel: 'log', source: 'extension' }, level: hasLevel ? levelOrMessage : undefined }); + } + + public appendLine(level: number, message: string): void; + public appendLine(message: string): void; + public appendLine(levelOrMessage: string | number, message?: string): void { + this.append(levelOrMessage as number, message + os.EOL); + } + + // We should not await on this function. + public showInformationMessage(message: string, items?: string[]): Thenable { + this.appendLine(message); + + if (!items) { + return vscode.window.showInformationMessage(message); + } + return vscode.window.showInformationMessage(message, ...items); + } + + // We should not await on this function. + public showWarningMessage(message: string, items?: string[]): Thenable { + this.appendLine(message); + + if (!items) { + return vscode.window.showWarningMessage(message); + } + return vscode.window.showWarningMessage(message, ...items); + } + + // We should not await on this function. + public showErrorMessage(message: string, items?: string[]): Thenable { + this.appendLine(message); + + if (!items) { + return vscode.window.showErrorMessage(message); + } + return vscode.window.showErrorMessage(message, ...items); + } +} + +export let outputChannel: vscode.OutputChannel | undefined; +export let diagnosticsChannel: vscode.OutputChannel | undefined; +export let crashCallStacksChannel: vscode.OutputChannel | undefined; +export let debugChannel: vscode.OutputChannel | undefined; +export let warningChannel: vscode.OutputChannel | undefined; +export let sshChannel: vscode.OutputChannel | undefined; + +export function getOutputChannel(): vscode.OutputChannel { + if (!outputChannel) { + outputChannel = vscode.window.createOutputChannel(CppSourceStr); + // Do not use CppSettings to avoid circular require() + const loggingLevel = getLoggingLevel(); + if (loggingLevel > 1) { + outputChannel.appendLine(`loggingLevel: ${loggingLevel}`); + } + } + return outputChannel; +} + +export function getDiagnosticsChannel(): vscode.OutputChannel { + if (!diagnosticsChannel) { + diagnosticsChannel = vscode.window.createOutputChannel(localize("c.cpp.diagnostics", "C/C++ Diagnostics")); + } + return diagnosticsChannel; +} + +export function getCrashCallStacksChannel(): vscode.OutputChannel { + if (!crashCallStacksChannel) { + crashCallStacksChannel = vscode.window.createOutputChannel(localize("c.cpp.crash.call.stacks.title", "C/C++ Crash Call Stacks")); + crashCallStacksChannel.appendLine(localize({ key: "c.cpp.crash.call.stacks.description", comment: ["{0} is a URL."] }, + "A C/C++ extension process has crashed. The crashing process name, date/time, signal, and call stack are below -- it would be helpful to include that in a bug report at {0}.", + "https://github.com/Microsoft/vscode-cpptools/issues")); + } + return crashCallStacksChannel; +} + +export function getSshChannel(): vscode.OutputChannel { + if (!sshChannel) { + sshChannel = vscode.window.createOutputChannel(localize("c.cpp.ssh.channel", "{0}: SSH", "Cpptools")); + } + return sshChannel; +} + +export function showOutputChannel(): void { + getOutputChannel().show(); +} + +let outputChannelLogger: Logger | undefined; + +export function getOutputChannelLogger(): Logger { + if (!outputChannelLogger) { + outputChannelLogger = new Logger(message => getOutputChannel().append(message)); + } + return outputChannelLogger; +} + +export function log(output: string): void { + getOutputChannel().appendLine(`${output}`); +} + +export interface DebugProtocolParams { + jsonrpc: string; + method: string; + params?: any; +} + +export function logDebugProtocol(output: DebugProtocolParams): void { + if (!debugChannel) { + debugChannel = vscode.window.createOutputChannel(`${localize("c.cpp.debug.protocol", "C/C++ Debug Protocol")}`); + } + debugChannel.appendLine(""); + debugChannel.appendLine("************************************************************************************************************************"); + debugChannel.append(`${output}`); +} + +export interface ShowWarningParams { + localizeStringParams: LocalizeStringParams; +} + +export function showWarning(params: ShowWarningParams): void { + const message: string = getLocalizedString(params.localizeStringParams); + let showChannel: boolean = false; + if (!warningChannel) { + warningChannel = vscode.window.createOutputChannel(`${localize("c.cpp.warnings", "C/C++ Configuration Warnings")}`); + showChannel = true; + } + // Append before showing the channel, to avoid a delay. + warningChannel.appendLine(`[${new Date().toLocaleString()}] ${message}`); + if (showChannel) { + warningChannel.show(true); + } +} + +export function logLocalized(params: LocalizeStringParams): void { + const output: string = getLocalizedString(params); + log(output); +} + +export function disposeOutputChannels(): void { + if (outputChannel) { + outputChannel.dispose(); + } + if (diagnosticsChannel) { + diagnosticsChannel.dispose(); + } + if (debugChannel) { + debugChannel.dispose(); + } + if (warningChannel) { + warningChannel.dispose(); + } +} diff --git a/Extension/src/main.ts b/Extension/src/main.ts index e15389ddac..66cc486118 100644 --- a/Extension/src/main.ts +++ b/Extension/src/main.ts @@ -1,318 +1,328 @@ -/* -------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All Rights Reserved. - * See 'LICENSE' in the project root for license information. - * ------------------------------------------------------------------------------------------ */ -'use strict'; - -import * as os from 'os'; -import * as path from 'path'; -import * as vscode from 'vscode'; -import * as DebuggerExtension from './Debugger/extension'; -import * as LanguageServer from './LanguageServer/extension'; -import * as util from './common'; -import * as Telemetry from './telemetry'; - -import * as semver from 'semver'; -import { CppToolsApi, CppToolsExtension } from 'vscode-cpptools'; -import * as nls from 'vscode-nls'; -import { TargetPopulation } from 'vscode-tas-client'; -import { CppBuildTaskProvider, cppBuildTaskProvider } from './LanguageServer/cppBuildTaskProvider'; -import { getLocaleId, getLocalizedHtmlPath } from './LanguageServer/localization'; -import { PersistentState } from './LanguageServer/persistentState'; -import { CppSettings } from './LanguageServer/settings'; -import { logAndReturn, returns } from './Utility/Async/returns'; -import { CppTools1 } from './cppTools1'; -import { logMachineIdMappings } from './id'; -import { disposeOutputChannels, log } from './logger'; -import { PlatformInformation } from './platform'; - -nls.config({ messageFormat: nls.MessageFormat.bundle, bundleFormat: nls.BundleFormat.standalone })(); -const localize: nls.LocalizeFunc = nls.loadMessageBundle(); - -const cppTools: CppTools1 = new CppTools1(); -let languageServiceDisabled: boolean = false; -let reloadMessageShown: boolean = false; -const disposables: vscode.Disposable[] = []; - -export async function activate(context: vscode.ExtensionContext): Promise { - util.setExtensionContext(context); - Telemetry.activate(); - util.setProgress(0); - - // Register a protocol handler to serve localized versions of the schema for c_cpp_properties.json - class SchemaProvider implements vscode.TextDocumentContentProvider { - public async provideTextDocumentContent(uri: vscode.Uri): Promise { - console.assert(uri.path[0] === '/', "A preceding slash is expected on schema uri path"); - const fileName: string = uri.path.substring(1); - const locale: string = getLocaleId(); - let localizedFilePath: string = util.getExtensionFilePath(path.join("dist/schema/", locale, fileName)); - const fileExists: boolean = await util.checkFileExists(localizedFilePath); - if (!fileExists) { - localizedFilePath = util.getExtensionFilePath(fileName); - } - return util.readFileText(localizedFilePath); - } - } - - vscode.workspace.registerTextDocumentContentProvider('cpptools-schema', new SchemaProvider()); - - // Initialize the DebuggerExtension and register the related commands and providers. - await DebuggerExtension.initialize(context); - - const info: PlatformInformation = await PlatformInformation.GetPlatformInformation(); - sendTelemetry(info); - - // Always attempt to make the binaries executable, not just when installedVersion changes. - // The user may have uninstalled and reinstalled the same version. - await makeBinariesExecutable(); - - // Notify users if debugging may not be supported on their OS. - util.checkDistro(info); - await checkVsixCompatibility(); - LanguageServer.UpdateInsidersAccess(); - - const ignoreRecommendations: boolean | undefined = vscode.workspace.getConfiguration("extensions", null).get("ignoreRecommendations"); - if (ignoreRecommendations !== true) { - await LanguageServer.preReleaseCheck(); - } - - const settings: CppSettings = new CppSettings((vscode.workspace.workspaceFolders && vscode.workspace.workspaceFolders.length > 0) ? vscode.workspace.workspaceFolders[0]?.uri : undefined); - let isOldMacOs: boolean = false; - if (info.platform === 'darwin') { - const releaseParts: string[] = os.release().split("."); - if (releaseParts.length >= 1) { - isOldMacOs = parseInt(releaseParts[0]) < 16; - } - } - - // Read the setting and determine whether we should activate the language server prior to installing callbacks, - // to ensure there is no potential race condition. LanguageServer.activate() is called near the end of this - // function, to allow any further setup to occur here, prior to activation. - const isIntelliSenseEngineDisabled: boolean = settings.intelliSenseEngine === "disabled"; - const shouldActivateLanguageServer: boolean = !isIntelliSenseEngineDisabled && !isOldMacOs; - - if (isOldMacOs) { - languageServiceDisabled = true; - void vscode.window.showErrorMessage(localize("macos.version.deprecated", "Versions of the C/C++ extension more recent than {0} require at least macOS version {1}.", "1.9.8", "10.12")); - } else { - if (settings.intelliSenseEngine === "disabled") { - languageServiceDisabled = true; - } - let currentIntelliSenseEngineValue: string | undefined = settings.intelliSenseEngine; - disposables.push(vscode.workspace.onDidChangeConfiguration(() => { - const settings: CppSettings = new CppSettings((vscode.workspace.workspaceFolders && vscode.workspace.workspaceFolders.length > 0) ? vscode.workspace.workspaceFolders[0]?.uri : undefined); - if (!reloadMessageShown && settings.intelliSenseEngine !== currentIntelliSenseEngineValue) { - if (currentIntelliSenseEngineValue === "disabled") { - // If switching from disabled to enabled, we can continue activation. - currentIntelliSenseEngineValue = settings.intelliSenseEngine; - languageServiceDisabled = false; - return LanguageServer.activate(); - } else { - // We can't deactivate or change engines on the fly, so prompt for window reload. - reloadMessageShown = true; - void util.promptForReloadWindowDueToSettingsChange(); - } - } - })); - } - - if (vscode.workspace.workspaceFolders && vscode.workspace.workspaceFolders.length > 0) { - for (let i: number = 0; i < vscode.workspace.workspaceFolders.length; ++i) { - const config: string = path.join(vscode.workspace.workspaceFolders[i].uri.fsPath, ".vscode/c_cpp_properties.json"); - if (await util.checkFileExists(config)) { - const doc: vscode.TextDocument = await vscode.workspace.openTextDocument(config); - void vscode.languages.setTextDocumentLanguage(doc, "jsonc"); - util.setWorkspaceIsCpp(); - } - } - } - - disposables.push(vscode.tasks.registerTaskProvider(CppBuildTaskProvider.CppBuildScriptType, cppBuildTaskProvider)); - - vscode.tasks.onDidStartTask(event => { - if (event.execution.task.definition.type === CppBuildTaskProvider.CppBuildScriptType - || event.execution.task.name.startsWith(LanguageServer.configPrefix)) { - Telemetry.logLanguageServerEvent('buildTaskStarted'); - } - }); - - vscode.tasks.onDidEndTask(event => { - if (event.execution.task.definition.type === CppBuildTaskProvider.CppBuildScriptType - || event.execution.task.name.startsWith(LanguageServer.configPrefix)) { - Telemetry.logLanguageServerEvent('buildTaskFinished'); - } - }); - - if (shouldActivateLanguageServer) { - await LanguageServer.activate(); - } else if (isIntelliSenseEngineDisabled) { - await LanguageServer.registerCommands(false); - // The check here for isIntelliSenseEngineDisabled avoids logging - // the message on old Macs that we've already displayed a warning for. - log(localize("intellisense.disabled", "intelliSenseEngine is disabled")); - } - - return cppTools; -} - -export async function deactivate(): Promise { - DebuggerExtension.dispose(); - void Telemetry.deactivate().catch(returns.undefined); - disposables.forEach(d => d.dispose()); - if (languageServiceDisabled) { - return; - } - await LanguageServer.deactivate(); - disposeOutputChannels(); -} - -async function makeBinariesExecutable(): Promise { - const promises: Thenable[] = []; - if (process.platform !== 'win32') { - const commonBinaries: string[] = [ - "./bin/cpptools", - "./bin/cpptools-srv", - "./bin/cpptools-wordexp", - "./LLVM/bin/clang-format", - "./LLVM/bin/clang-tidy", - "./debugAdapters/bin/OpenDebugAD7" - ]; - commonBinaries.forEach(binary => promises.push(util.allowExecution(util.getExtensionFilePath(binary)))); - if (process.platform === "darwin") { - const macBinaries: string[] = [ - "./debugAdapters/lldb-mi/bin/lldb-mi" - ]; - macBinaries.forEach(binary => promises.push(util.allowExecution(util.getExtensionFilePath(binary)))); - if (os.arch() === "x64") { - const oldMacBinaries: string[] = [ - "./debugAdapters/lldb/bin/debugserver", - "./debugAdapters/lldb/bin/lldb-mi", - "./debugAdapters/lldb/bin/lldb-argdumper", - "./debugAdapters/lldb/bin/lldb-launcher" - ]; - oldMacBinaries.forEach(binary => promises.push(util.allowExecution(util.getExtensionFilePath(binary)))); - } - } else if (os.arch() === "x64") { - promises.push(util.allowExecution(util.getExtensionFilePath("./bin/libc.so"))); - } - } - await Promise.all(promises); -} - -function sendTelemetry(info: PlatformInformation): void { - const telemetryProperties: { [key: string]: string } = {}; - if (info.distribution) { - telemetryProperties['linuxDistroName'] = info.distribution.name; - telemetryProperties['linuxDistroVersion'] = info.distribution.version; - } - telemetryProperties['osArchitecture'] = os.arch(); - telemetryProperties['infoArchitecture'] = info.architecture; - const targetPopulation: TargetPopulation = util.getCppToolsTargetPopulation(); - switch (targetPopulation) { - case TargetPopulation.Public: - telemetryProperties['targetPopulation'] = "Public"; - break; - case TargetPopulation.Internal: - telemetryProperties['targetPopulation'] = "Internal"; - break; - case TargetPopulation.Insiders: - telemetryProperties['targetPopulation'] = "Insiders"; - break; - default: - break; - } - Telemetry.logDebuggerEvent("acquisition", telemetryProperties); - logMachineIdMappings().catch(logAndReturn.undefined); -} - -async function checkVsixCompatibility(): Promise { - const ignoreMismatchedCompatibleVsix: PersistentState = new PersistentState("CPP." + util.packageJson.version + ".ignoreMismatchedCompatibleVsix", false); - let resetIgnoreMismatchedCompatibleVsix: boolean = true; - - // Check to ensure the correct platform-specific VSIX was installed. - const vsixManifestPath: string = path.join(util.extensionPath, ".vsixmanifest"); - // Skip the check if the file does not exist, such as when debugging cpptools. - if (await util.checkFileExists(vsixManifestPath)) { - const content: string = await util.readFileText(vsixManifestPath); - const matches: RegExpMatchArray | null = content.match(/TargetPlatform="(?[^"]*)"/); - if (matches && matches.length > 0 && matches.groups) { - const vsixTargetPlatform: string = matches.groups['platform']; - const platformInfo: PlatformInformation = await PlatformInformation.GetPlatformInformation(); - let isPlatformCompatible: boolean = true; - let isPlatformMatching: boolean = true; - switch (vsixTargetPlatform) { - case "win32-x64": - isPlatformMatching = platformInfo.platform === "win32" && platformInfo.architecture === "x64"; - // x64 binaries can also be run on arm64 Windows 11. - isPlatformCompatible = platformInfo.platform === "win32" && (platformInfo.architecture === "x64" || (platformInfo.architecture === "arm64" && semver.gte(os.release(), "10.0.22000"))); - break; - case "win32-ia32": - isPlatformMatching = platformInfo.platform === "win32" && platformInfo.architecture === "x86"; - // x86 binaries can also be run on x64 and arm64 Windows. - isPlatformCompatible = platformInfo.platform === "win32" && (platformInfo.architecture === "x86" || platformInfo.architecture === "x64" || platformInfo.architecture === "arm64"); - break; - case "win32-arm64": - isPlatformMatching = platformInfo.platform === "win32" && platformInfo.architecture === "arm64"; - isPlatformCompatible = isPlatformMatching; - break; - case "linux-x64": - isPlatformMatching = platformInfo.platform === "linux" && platformInfo.architecture === "x64" && platformInfo.distribution?.name !== "alpine"; - isPlatformCompatible = isPlatformMatching; - break; - case "linux-arm64": - isPlatformMatching = platformInfo.platform === "linux" && platformInfo.architecture === "arm64" && platformInfo.distribution?.name !== "alpine"; - isPlatformCompatible = isPlatformMatching; - break; - case "linux-armhf": - isPlatformMatching = platformInfo.platform === "linux" && platformInfo.architecture === "arm" && platformInfo.distribution?.name !== "alpine"; - // armhf binaries can also be run on aarch64 linux. - isPlatformCompatible = platformInfo.platform === "linux" && (platformInfo.architecture === "arm" || platformInfo.architecture === "arm64") && platformInfo.distribution?.name !== "alpine"; - break; - case "alpine-x64": - isPlatformMatching = platformInfo.platform === "linux" && platformInfo.architecture === "x64" && platformInfo.distribution?.name === "alpine"; - isPlatformCompatible = isPlatformMatching; - break; - case "alpine-arm64": - isPlatformMatching = platformInfo.platform === "linux" && platformInfo.architecture === "arm64" && platformInfo.distribution?.name === "alpine"; - isPlatformCompatible = isPlatformMatching; - break; - case "darwin-x64": - isPlatformMatching = platformInfo.platform === "darwin" && platformInfo.architecture === "x64"; - isPlatformCompatible = isPlatformMatching; - break; - case "darwin-arm64": - isPlatformMatching = platformInfo.platform === "darwin" && platformInfo.architecture === "arm64"; - // x64 binaries can also be run on arm64 macOS. - isPlatformCompatible = platformInfo.platform === "darwin" && (platformInfo.architecture === "x64" || platformInfo.architecture === "arm64"); - break; - default: - console.log("Unrecognized TargetPlatform in .vsixmanifest"); - break; - } - const moreInfoButton: string = localize("more.info.button", "More Info"); - const ignoreButton: string = localize("ignore.button", "Ignore"); - let promise: Thenable | undefined; - if (!isPlatformCompatible) { - promise = vscode.window.showErrorMessage(localize("vsix.platform.incompatible", "The C/C++ extension installed does not match your system.", vsixTargetPlatform), moreInfoButton); - } else if (!isPlatformMatching) { - if (!ignoreMismatchedCompatibleVsix.Value) { - resetIgnoreMismatchedCompatibleVsix = false; - promise = vscode.window.showWarningMessage(localize("vsix.platform.mismatching", "The C/C++ extension installed is compatible with but does not match your system.", vsixTargetPlatform), moreInfoButton, ignoreButton); - } - } - - void promise?.then((value) => { - if (value === moreInfoButton) { - void vscode.commands.executeCommand("markdown.showPreview", vscode.Uri.file(getLocalizedHtmlPath("Reinstalling the Extension.md"))); - } else if (value === ignoreButton) { - ignoreMismatchedCompatibleVsix.Value = true; - } - }, logAndReturn.undefined); - } else { - console.log("Unable to find TargetPlatform in .vsixmanifest"); - } - } - if (resetIgnoreMismatchedCompatibleVsix) { - ignoreMismatchedCompatibleVsix.Value = false; - } -} +/* -------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All Rights Reserved. + * See 'LICENSE' in the project root for license information. + * ------------------------------------------------------------------------------------------ */ +'use strict'; + +import * as os from 'os'; +import * as path from 'path'; +import * as vscode from 'vscode'; +import * as DebuggerExtension from './Debugger/extension'; +import * as LanguageServer from './LanguageServer/extension'; +import * as util from './common'; +import * as Telemetry from './telemetry'; + +import * as semver from 'semver'; +import { CppToolsApi, CppToolsExtension } from 'vscode-cpptools'; +import * as nls from 'vscode-nls'; +import { TargetPopulation } from 'vscode-tas-client'; +import { CppBuildTaskProvider, cppBuildTaskProvider } from './LanguageServer/cppBuildTaskProvider'; +import { getLocaleId, getLocalizedHtmlPath } from './LanguageServer/localization'; +import { PersistentState } from './LanguageServer/persistentState'; +import { CppSettings } from './LanguageServer/settings'; +import { logAndReturn, returns } from './Utility/Async/returns'; +import { CppTools1 } from './cppTools1'; +import { logMachineIdMappings } from './id'; +import { sendInstrumentation } from './instrumentation'; +import { disposeOutputChannels, log } from './logger'; +import { PlatformInformation } from './platform'; + +nls.config({ messageFormat: nls.MessageFormat.bundle, bundleFormat: nls.BundleFormat.standalone })(); +const localize: nls.LocalizeFunc = nls.loadMessageBundle(); + +const cppTools: CppTools1 = new CppTools1(); +let languageServiceDisabled: boolean = false; +let reloadMessageShown: boolean = false; +const disposables: vscode.Disposable[] = []; + +export async function activate(context: vscode.ExtensionContext): Promise { + sendInstrumentation({ + name: "activate", + context: { cpptools: '', start: '' } + }); + + util.setExtensionContext(context); + Telemetry.activate(); + util.setProgress(0); + + // Register a protocol handler to serve localized versions of the schema for c_cpp_properties.json + class SchemaProvider implements vscode.TextDocumentContentProvider { + public async provideTextDocumentContent(uri: vscode.Uri): Promise { + console.assert(uri.path[0] === '/', "A preceding slash is expected on schema uri path"); + const fileName: string = uri.path.substring(1); + const locale: string = getLocaleId(); + let localizedFilePath: string = util.getExtensionFilePath(path.join("dist/schema/", locale, fileName)); + const fileExists: boolean = await util.checkFileExists(localizedFilePath); + if (!fileExists) { + localizedFilePath = util.getExtensionFilePath(fileName); + } + return util.readFileText(localizedFilePath); + } + } + + vscode.workspace.registerTextDocumentContentProvider('cpptools-schema', new SchemaProvider()); + + // Initialize the DebuggerExtension and register the related commands and providers. + await DebuggerExtension.initialize(context); + + const info: PlatformInformation = await PlatformInformation.GetPlatformInformation(); + sendTelemetry(info); + + // Always attempt to make the binaries executable, not just when installedVersion changes. + // The user may have uninstalled and reinstalled the same version. + await makeBinariesExecutable(); + + // Notify users if debugging may not be supported on their OS. + util.checkDistro(info); + await checkVsixCompatibility(); + LanguageServer.UpdateInsidersAccess(); + + const ignoreRecommendations: boolean | undefined = vscode.workspace.getConfiguration("extensions", null).get("ignoreRecommendations"); + if (ignoreRecommendations !== true) { + await LanguageServer.preReleaseCheck(); + } + + const settings: CppSettings = new CppSettings((vscode.workspace.workspaceFolders && vscode.workspace.workspaceFolders.length > 0) ? vscode.workspace.workspaceFolders[0]?.uri : undefined); + let isOldMacOs: boolean = false; + if (info.platform === 'darwin') { + const releaseParts: string[] = os.release().split("."); + if (releaseParts.length >= 1) { + isOldMacOs = parseInt(releaseParts[0]) < 16; + } + } + + // Read the setting and determine whether we should activate the language server prior to installing callbacks, + // to ensure there is no potential race condition. LanguageServer.activate() is called near the end of this + // function, to allow any further setup to occur here, prior to activation. + const isIntelliSenseEngineDisabled: boolean = settings.intelliSenseEngine === "disabled"; + const shouldActivateLanguageServer: boolean = !isIntelliSenseEngineDisabled && !isOldMacOs; + + if (isOldMacOs) { + languageServiceDisabled = true; + void vscode.window.showErrorMessage(localize("macos.version.deprecated", "Versions of the C/C++ extension more recent than {0} require at least macOS version {1}.", "1.9.8", "10.12")); + } else { + if (settings.intelliSenseEngine === "disabled") { + languageServiceDisabled = true; + } + let currentIntelliSenseEngineValue: string | undefined = settings.intelliSenseEngine; + disposables.push(vscode.workspace.onDidChangeConfiguration(() => { + const settings: CppSettings = new CppSettings((vscode.workspace.workspaceFolders && vscode.workspace.workspaceFolders.length > 0) ? vscode.workspace.workspaceFolders[0]?.uri : undefined); + if (!reloadMessageShown && settings.intelliSenseEngine !== currentIntelliSenseEngineValue) { + if (currentIntelliSenseEngineValue === "disabled") { + // If switching from disabled to enabled, we can continue activation. + currentIntelliSenseEngineValue = settings.intelliSenseEngine; + languageServiceDisabled = false; + return LanguageServer.activate(); + } else { + // We can't deactivate or change engines on the fly, so prompt for window reload. + reloadMessageShown = true; + void util.promptForReloadWindowDueToSettingsChange(); + } + } + })); + } + + if (vscode.workspace.workspaceFolders && vscode.workspace.workspaceFolders.length > 0) { + for (let i: number = 0; i < vscode.workspace.workspaceFolders.length; ++i) { + const config: string = path.join(vscode.workspace.workspaceFolders[i].uri.fsPath, ".vscode/c_cpp_properties.json"); + if (await util.checkFileExists(config)) { + const doc: vscode.TextDocument = await vscode.workspace.openTextDocument(config); + void vscode.languages.setTextDocumentLanguage(doc, "jsonc"); + util.setWorkspaceIsCpp(); + } + } + } + + disposables.push(vscode.tasks.registerTaskProvider(CppBuildTaskProvider.CppBuildScriptType, cppBuildTaskProvider)); + + vscode.tasks.onDidStartTask(event => { + if (event.execution.task.definition.type === CppBuildTaskProvider.CppBuildScriptType + || event.execution.task.name.startsWith(LanguageServer.configPrefix)) { + Telemetry.logLanguageServerEvent('buildTaskStarted'); + } + }); + + vscode.tasks.onDidEndTask(event => { + if (event.execution.task.definition.type === CppBuildTaskProvider.CppBuildScriptType + || event.execution.task.name.startsWith(LanguageServer.configPrefix)) { + Telemetry.logLanguageServerEvent('buildTaskFinished'); + } + }); + + if (shouldActivateLanguageServer) { + await LanguageServer.activate(); + } else if (isIntelliSenseEngineDisabled) { + await LanguageServer.registerCommands(false); + // The check here for isIntelliSenseEngineDisabled avoids logging + // the message on old Macs that we've already displayed a warning for. + log(localize("intellisense.disabled", "intelliSenseEngine is disabled")); + } + + sendInstrumentation({ + name: "activate", + context: { cpptools: '', end: '' } + }); + return cppTools; +} + +export async function deactivate(): Promise { + DebuggerExtension.dispose(); + void Telemetry.deactivate().catch(returns.undefined); + disposables.forEach(d => d.dispose()); + if (languageServiceDisabled) { + return; + } + await LanguageServer.deactivate(); + disposeOutputChannels(); +} + +async function makeBinariesExecutable(): Promise { + const promises: Thenable[] = []; + if (process.platform !== 'win32') { + const commonBinaries: string[] = [ + "./bin/cpptools", + "./bin/cpptools-srv", + "./bin/cpptools-wordexp", + "./LLVM/bin/clang-format", + "./LLVM/bin/clang-tidy", + "./debugAdapters/bin/OpenDebugAD7" + ]; + commonBinaries.forEach(binary => promises.push(util.allowExecution(util.getExtensionFilePath(binary)))); + if (process.platform === "darwin") { + const macBinaries: string[] = [ + "./debugAdapters/lldb-mi/bin/lldb-mi" + ]; + macBinaries.forEach(binary => promises.push(util.allowExecution(util.getExtensionFilePath(binary)))); + if (os.arch() === "x64") { + const oldMacBinaries: string[] = [ + "./debugAdapters/lldb/bin/debugserver", + "./debugAdapters/lldb/bin/lldb-mi", + "./debugAdapters/lldb/bin/lldb-argdumper", + "./debugAdapters/lldb/bin/lldb-launcher" + ]; + oldMacBinaries.forEach(binary => promises.push(util.allowExecution(util.getExtensionFilePath(binary)))); + } + } else if (os.arch() === "x64") { + promises.push(util.allowExecution(util.getExtensionFilePath("./bin/libc.so"))); + } + } + await Promise.all(promises); +} + +function sendTelemetry(info: PlatformInformation): void { + const telemetryProperties: { [key: string]: string } = {}; + if (info.distribution) { + telemetryProperties['linuxDistroName'] = info.distribution.name; + telemetryProperties['linuxDistroVersion'] = info.distribution.version; + } + telemetryProperties['osArchitecture'] = os.arch(); + telemetryProperties['infoArchitecture'] = info.architecture; + const targetPopulation: TargetPopulation = util.getCppToolsTargetPopulation(); + switch (targetPopulation) { + case TargetPopulation.Public: + telemetryProperties['targetPopulation'] = "Public"; + break; + case TargetPopulation.Internal: + telemetryProperties['targetPopulation'] = "Internal"; + break; + case TargetPopulation.Insiders: + telemetryProperties['targetPopulation'] = "Insiders"; + break; + default: + break; + } + Telemetry.logDebuggerEvent("acquisition", telemetryProperties); + logMachineIdMappings().catch(logAndReturn.undefined); +} + +async function checkVsixCompatibility(): Promise { + const ignoreMismatchedCompatibleVsix: PersistentState = new PersistentState("CPP." + util.packageJson.version + ".ignoreMismatchedCompatibleVsix", false); + let resetIgnoreMismatchedCompatibleVsix: boolean = true; + + // Check to ensure the correct platform-specific VSIX was installed. + const vsixManifestPath: string = path.join(util.extensionPath, ".vsixmanifest"); + // Skip the check if the file does not exist, such as when debugging cpptools. + if (await util.checkFileExists(vsixManifestPath)) { + const content: string = await util.readFileText(vsixManifestPath); + const matches: RegExpMatchArray | null = content.match(/TargetPlatform="(?[^"]*)"/); + if (matches && matches.length > 0 && matches.groups) { + const vsixTargetPlatform: string = matches.groups['platform']; + const platformInfo: PlatformInformation = await PlatformInformation.GetPlatformInformation(); + let isPlatformCompatible: boolean = true; + let isPlatformMatching: boolean = true; + switch (vsixTargetPlatform) { + case "win32-x64": + isPlatformMatching = platformInfo.platform === "win32" && platformInfo.architecture === "x64"; + // x64 binaries can also be run on arm64 Windows 11. + isPlatformCompatible = platformInfo.platform === "win32" && (platformInfo.architecture === "x64" || (platformInfo.architecture === "arm64" && semver.gte(os.release(), "10.0.22000"))); + break; + case "win32-ia32": + isPlatformMatching = platformInfo.platform === "win32" && platformInfo.architecture === "x86"; + // x86 binaries can also be run on x64 and arm64 Windows. + isPlatformCompatible = platformInfo.platform === "win32" && (platformInfo.architecture === "x86" || platformInfo.architecture === "x64" || platformInfo.architecture === "arm64"); + break; + case "win32-arm64": + isPlatformMatching = platformInfo.platform === "win32" && platformInfo.architecture === "arm64"; + isPlatformCompatible = isPlatformMatching; + break; + case "linux-x64": + isPlatformMatching = platformInfo.platform === "linux" && platformInfo.architecture === "x64" && platformInfo.distribution?.name !== "alpine"; + isPlatformCompatible = isPlatformMatching; + break; + case "linux-arm64": + isPlatformMatching = platformInfo.platform === "linux" && platformInfo.architecture === "arm64" && platformInfo.distribution?.name !== "alpine"; + isPlatformCompatible = isPlatformMatching; + break; + case "linux-armhf": + isPlatformMatching = platformInfo.platform === "linux" && platformInfo.architecture === "arm" && platformInfo.distribution?.name !== "alpine"; + // armhf binaries can also be run on aarch64 linux. + isPlatformCompatible = platformInfo.platform === "linux" && (platformInfo.architecture === "arm" || platformInfo.architecture === "arm64") && platformInfo.distribution?.name !== "alpine"; + break; + case "alpine-x64": + isPlatformMatching = platformInfo.platform === "linux" && platformInfo.architecture === "x64" && platformInfo.distribution?.name === "alpine"; + isPlatformCompatible = isPlatformMatching; + break; + case "alpine-arm64": + isPlatformMatching = platformInfo.platform === "linux" && platformInfo.architecture === "arm64" && platformInfo.distribution?.name === "alpine"; + isPlatformCompatible = isPlatformMatching; + break; + case "darwin-x64": + isPlatformMatching = platformInfo.platform === "darwin" && platformInfo.architecture === "x64"; + isPlatformCompatible = isPlatformMatching; + break; + case "darwin-arm64": + isPlatformMatching = platformInfo.platform === "darwin" && platformInfo.architecture === "arm64"; + // x64 binaries can also be run on arm64 macOS. + isPlatformCompatible = platformInfo.platform === "darwin" && (platformInfo.architecture === "x64" || platformInfo.architecture === "arm64"); + break; + default: + console.log("Unrecognized TargetPlatform in .vsixmanifest"); + break; + } + const moreInfoButton: string = localize("more.info.button", "More Info"); + const ignoreButton: string = localize("ignore.button", "Ignore"); + let promise: Thenable | undefined; + if (!isPlatformCompatible) { + promise = vscode.window.showErrorMessage(localize("vsix.platform.incompatible", "The C/C++ extension installed does not match your system.", vsixTargetPlatform), moreInfoButton); + } else if (!isPlatformMatching) { + if (!ignoreMismatchedCompatibleVsix.Value) { + resetIgnoreMismatchedCompatibleVsix = false; + promise = vscode.window.showWarningMessage(localize("vsix.platform.mismatching", "The C/C++ extension installed is compatible with but does not match your system.", vsixTargetPlatform), moreInfoButton, ignoreButton); + } + } + + void promise?.then((value) => { + if (value === moreInfoButton) { + void vscode.commands.executeCommand("markdown.showPreview", vscode.Uri.file(getLocalizedHtmlPath("Reinstalling the Extension.md"))); + } else if (value === ignoreButton) { + ignoreMismatchedCompatibleVsix.Value = true; + } + }, logAndReturn.undefined); + } else { + console.log("Unable to find TargetPlatform in .vsixmanifest"); + } + } + if (resetIgnoreMismatchedCompatibleVsix) { + ignoreMismatchedCompatibleVsix.Value = false; + } +} diff --git a/Extension/src/platform.ts b/Extension/src/platform.ts index 5b5e3c1ab8..686ff7b4c2 100644 --- a/Extension/src/platform.ts +++ b/Extension/src/platform.ts @@ -1,105 +1,105 @@ -/* -------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All Rights Reserved. - * See 'LICENSE' in the project root for license information. - * ------------------------------------------------------------------------------------------ */ - -import * as fs from 'fs'; -import * as os from 'os'; -import * as plist from 'plist'; -import * as nls from 'vscode-nls'; -import { LinuxDistribution } from './linuxDistribution'; -import * as logger from './logger'; -import { SessionState, SupportedWindowsVersions } from './sessionState'; - -nls.config({ messageFormat: nls.MessageFormat.bundle, bundleFormat: nls.BundleFormat.standalone })(); -const localize: nls.LocalizeFunc = nls.loadMessageBundle(); - -export function GetOSName(processPlatform: string | undefined): string | undefined { - switch (processPlatform) { - case "win32": return "Windows"; - case "darwin": return "macOS"; - case "linux": return "Linux"; - default: return undefined; - } -} - -export class PlatformInformation { - constructor(public platform: string, public architecture: string, public distribution?: LinuxDistribution, public version?: string) { } - - public static async GetPlatformInformation(): Promise { - const platform: string = os.platform(); - const architecture: string = PlatformInformation.GetArchitecture(); - let distribution: LinuxDistribution | undefined; - let version: string | undefined; - switch (platform) { - case "win32": - version = PlatformInformation.GetWindowsVersion(); - void SessionState.windowsVersion.set(version as SupportedWindowsVersions); - break; - case "linux": - distribution = await LinuxDistribution.GetDistroInformation(); - break; - case "darwin": - version = await PlatformInformation.GetDarwinVersion(); - break; - default: - throw new Error(localize("unknown.os.platform", "Unknown OS platform")); - } - - return new PlatformInformation(platform, architecture, distribution, version); - } - - public static GetArchitecture(): string { - const arch: string = os.arch(); - switch (arch) { - case "arm64": - case "arm": - return arch; - case "x32": - case "ia32": - return "x86"; - default: - return "x64"; - } - } - - private static GetDarwinVersion(): Promise { - const DARWIN_SYSTEM_VERSION_PLIST: string = "/System/Library/CoreServices/SystemVersion.plist"; - let productDarwinVersion: string = ""; - let errorMessage: string = ""; - - if (fs.existsSync(DARWIN_SYSTEM_VERSION_PLIST)) { - const systemVersionPListBuffer: Buffer = fs.readFileSync(DARWIN_SYSTEM_VERSION_PLIST); - const systemVersionData: any = plist.parse(systemVersionPListBuffer.toString()); - if (systemVersionData) { - productDarwinVersion = systemVersionData.ProductVersion; - } else { - errorMessage = localize("missing.plist.productversion", "Could not get ProduceVersion from SystemVersion.plist"); - } - } else { - errorMessage = localize("missing.darwin.systemversion.file", "Failed to find SystemVersion.plist in {0}.", DARWIN_SYSTEM_VERSION_PLIST); - } - - if (errorMessage) { - logger.getOutputChannel().appendLine(errorMessage); - logger.showOutputChannel(); - } - - return Promise.resolve(productDarwinVersion); - } - - private static GetWindowsVersion(): SupportedWindowsVersions { - const version = os.release().split('.'); - if (version.length > 0) { - if (version[0] === '10') { - if (version.length > 2 && version[2].startsWith('1')) { - // 10.0.10240 - 10.0.190## - return '10'; - } - // 10.0.22000+ - return '11'; - } - } - return ''; - } -} +/* -------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All Rights Reserved. + * See 'LICENSE' in the project root for license information. + * ------------------------------------------------------------------------------------------ */ + +import * as fs from 'fs'; +import * as os from 'os'; +import * as plist from 'plist'; +import * as nls from 'vscode-nls'; +import { LinuxDistribution } from './linuxDistribution'; +import { getOutputChannelLogger, showOutputChannel } from './logger'; +import { SessionState, SupportedWindowsVersions } from './sessionState'; + +nls.config({ messageFormat: nls.MessageFormat.bundle, bundleFormat: nls.BundleFormat.standalone })(); +const localize: nls.LocalizeFunc = nls.loadMessageBundle(); + +export function GetOSName(processPlatform: string | undefined): string | undefined { + switch (processPlatform) { + case "win32": return "Windows"; + case "darwin": return "macOS"; + case "linux": return "Linux"; + default: return undefined; + } +} + +export class PlatformInformation { + constructor(public platform: string, public architecture: string, public distribution?: LinuxDistribution, public version?: string) { } + + public static async GetPlatformInformation(): Promise { + const platform: string = os.platform(); + const architecture: string = PlatformInformation.GetArchitecture(); + let distribution: LinuxDistribution | undefined; + let version: string | undefined; + switch (platform) { + case "win32": + version = PlatformInformation.GetWindowsVersion(); + void SessionState.windowsVersion.set(version as SupportedWindowsVersions); + break; + case "linux": + distribution = await LinuxDistribution.GetDistroInformation(); + break; + case "darwin": + version = await PlatformInformation.GetDarwinVersion(); + break; + default: + throw new Error(localize("unknown.os.platform", "Unknown OS platform")); + } + + return new PlatformInformation(platform, architecture, distribution, version); + } + + public static GetArchitecture(): string { + const arch: string = os.arch(); + switch (arch) { + case "arm64": + case "arm": + return arch; + case "x32": + case "ia32": + return "x86"; + default: + return "x64"; + } + } + + private static GetDarwinVersion(): Promise { + const DARWIN_SYSTEM_VERSION_PLIST: string = "/System/Library/CoreServices/SystemVersion.plist"; + let productDarwinVersion: string = ""; + let errorMessage: string = ""; + + if (fs.existsSync(DARWIN_SYSTEM_VERSION_PLIST)) { + const systemVersionPListBuffer: Buffer = fs.readFileSync(DARWIN_SYSTEM_VERSION_PLIST); + const systemVersionData: any = plist.parse(systemVersionPListBuffer.toString()); + if (systemVersionData) { + productDarwinVersion = systemVersionData.ProductVersion; + } else { + errorMessage = localize("missing.plist.productversion", "Could not get ProduceVersion from SystemVersion.plist"); + } + } else { + errorMessage = localize("missing.darwin.systemversion.file", "Failed to find SystemVersion.plist in {0}.", DARWIN_SYSTEM_VERSION_PLIST); + } + + if (errorMessage) { + getOutputChannelLogger().appendLine(errorMessage); + showOutputChannel(); + } + + return Promise.resolve(productDarwinVersion); + } + + private static GetWindowsVersion(): SupportedWindowsVersions { + const version = os.release().split('.'); + if (version.length > 0) { + if (version[0] === '10') { + if (version.length > 2 && version[2].startsWith('1')) { + // 10.0.10240 - 10.0.190## + return '10'; + } + // 10.0.22000+ + return '11'; + } + } + return ''; + } +}