diff --git a/Extension/src/LanguageServer/configurations.ts b/Extension/src/LanguageServer/configurations.ts index e1a41ba205..8ffb52a7f5 100644 --- a/Extension/src/LanguageServer/configurations.ts +++ b/Extension/src/LanguageServer/configurations.ts @@ -227,22 +227,6 @@ export class CppProperties { private onSelectionChanged(): void { this.selectionChanged.fire(this.CurrentConfigurationIndex); - if (this.settingsPanel) { - this.ensurePropertiesFile().then(() => { - if (this.propertiesFile) { - // Clear out any modifications we may have made internally by parsing the json file - if (this.parsePropertiesFile(false)) { - // Update the UI with new selected configuration - this.settingsPanel.updateConfigUI( - this.configurationJson.configurations[this.currentConfigurationIndex.Value], - this.getErrorsForConfigUI()); - } else { - // Parse failed, open json file - vscode.workspace.openTextDocument(this.propertiesFile); - } - } - }); - } this.handleSquiggles(); } @@ -273,53 +257,57 @@ export class CppProperties { private applyDefaultIncludePathsAndFrameworks(): void { if (this.configurationIncomplete && this.defaultIncludes && this.defaultFrameworks && this.vcpkgPathReady) { let configuration: Configuration = this.CurrentConfiguration; - let settings: CppSettings = new CppSettings(this.rootUri); - let isUnset: (input: any) => boolean = (input: any) => { + this.applyDefaultConfigurationValues(configuration); + this.configurationIncomplete = false; + } + } + + private applyDefaultConfigurationValues(configuration: Configuration): void { + let settings: CppSettings = new CppSettings(this.rootUri); + let isUnset: (input: any) => boolean = (input: any) => { // default values for "default" config settings is null. return input === null; - }; + }; - // Anything that has a vscode setting for it will be resolved in updateServerOnFolderSettingsChange. - // So if a property is currently unset, but has a vscode setting, don't set it yet, otherwise the linkage - // to the setting will be lost if this configuration is saved into a c_cpp_properties.json file. + // Anything that has a vscode setting for it will be resolved in updateServerOnFolderSettingsChange. + // So if a property is currently unset, but has a vscode setting, don't set it yet, otherwise the linkage + // to the setting will be lost if this configuration is saved into a c_cpp_properties.json file. - // Only add settings from the default compiler if user hasn't explicitly set the corresponding VS Code setting. + // Only add settings from the default compiler if user hasn't explicitly set the corresponding VS Code setting. - if (isUnset(settings.defaultIncludePath)) { - // We don't add system includes to the includePath anymore. The language server has this information. - let abTestSettings: ABTestSettings = getABTestSettings(); - let rootFolder: string = abTestSettings.UseRecursiveIncludes ? "${workspaceFolder}/**" : "${workspaceFolder}"; - configuration.includePath = [rootFolder].concat(this.vcpkgIncludes); - } - // browse.path is not set by default anymore. When it is not set, the includePath will be used instead. - if (isUnset(settings.defaultDefines)) { - configuration.defines = (process.platform === 'win32') ? ["_DEBUG", "UNICODE", "_UNICODE"] : []; - } - if (isUnset(settings.defaultMacFrameworkPath) && process.platform === 'darwin') { - configuration.macFrameworkPath = this.defaultFrameworks; - } - if (isUnset(settings.defaultWindowsSdkVersion) && this.defaultWindowsSdkVersion && process.platform === 'win32') { - configuration.windowsSdkVersion = this.defaultWindowsSdkVersion; - } - if (isUnset(settings.defaultCompilerPath) && this.defaultCompilerPath && - isUnset(settings.defaultCompileCommands) && !configuration.compileCommands) { - // compile_commands.json already specifies a compiler. compilerPath overrides the compile_commands.json compiler so - // don't set a default when compileCommands is in use. - configuration.compilerPath = this.defaultCompilerPath; - } - if (this.knownCompilers) { - configuration.knownCompilers = this.knownCompilers; - } - if (isUnset(settings.defaultCStandard) && this.defaultCStandard) { - configuration.cStandard = this.defaultCStandard; - } - if (isUnset(settings.defaultCppStandard) && this.defaultCppStandard) { - configuration.cppStandard = this.defaultCppStandard; - } - if (isUnset(settings.defaultIntelliSenseMode)) { - configuration.intelliSenseMode = this.defaultIntelliSenseMode; - } - this.configurationIncomplete = false; + if (isUnset(settings.defaultIncludePath)) { + // We don't add system includes to the includePath anymore. The language server has this information. + let abTestSettings: ABTestSettings = getABTestSettings(); + let rootFolder: string = abTestSettings.UseRecursiveIncludes ? "${workspaceFolder}/**" : "${workspaceFolder}"; + configuration.includePath = [rootFolder].concat(this.vcpkgIncludes); + } + // browse.path is not set by default anymore. When it is not set, the includePath will be used instead. + if (isUnset(settings.defaultDefines)) { + configuration.defines = (process.platform === 'win32') ? ["_DEBUG", "UNICODE", "_UNICODE"] : []; + } + if (isUnset(settings.defaultMacFrameworkPath) && process.platform === 'darwin') { + configuration.macFrameworkPath = this.defaultFrameworks; + } + if (isUnset(settings.defaultWindowsSdkVersion) && this.defaultWindowsSdkVersion && process.platform === 'win32') { + configuration.windowsSdkVersion = this.defaultWindowsSdkVersion; + } + if (isUnset(settings.defaultCompilerPath) && this.defaultCompilerPath && + isUnset(settings.defaultCompileCommands) && !configuration.compileCommands) { + // compile_commands.json already specifies a compiler. compilerPath overrides the compile_commands.json compiler so + // don't set a default when compileCommands is in use. + configuration.compilerPath = this.defaultCompilerPath; + } + if (this.knownCompilers) { + configuration.knownCompilers = this.knownCompilers; + } + if (isUnset(settings.defaultCStandard) && this.defaultCStandard) { + configuration.cStandard = this.defaultCStandard; + } + if (isUnset(settings.defaultCppStandard) && this.defaultCppStandard) { + configuration.cppStandard = this.defaultCppStandard; + } + if (isUnset(settings.defaultIntelliSenseMode)) { + configuration.intelliSenseMode = this.defaultIntelliSenseMode; } } @@ -397,20 +385,20 @@ export class CppProperties { } } - private isCompilerIntelliSenseModeCompatible(): boolean { + private isCompilerIntelliSenseModeCompatible(configuration: Configuration): boolean { // Check if intelliSenseMode and compilerPath are compatible // cl.exe and msvc mode should be used together // Ignore if compiler path is not set or intelliSenseMode is not set - if (this.CurrentConfiguration.compilerPath === undefined || - this.CurrentConfiguration.compilerPath === "" || - this.CurrentConfiguration.compilerPath === "${default}" || - this.CurrentConfiguration.intelliSenseMode === undefined || - this.CurrentConfiguration.intelliSenseMode === "" || - this.CurrentConfiguration.intelliSenseMode === "${default}") { + if (configuration.compilerPath === undefined || + configuration.compilerPath === "" || + configuration.compilerPath === "${default}" || + configuration.intelliSenseMode === undefined || + configuration.intelliSenseMode === "" || + configuration.intelliSenseMode === "${default}") { return true; } - let compilerPathAndArgs: util.CompilerPathAndArgs = util.extractCompilerPathAndArgs(this.CurrentConfiguration.compilerPath); - return compilerPathAndArgs.compilerPath.endsWith("cl.exe") === (this.CurrentConfiguration.intelliSenseMode === "msvc-x64"); + let compilerPathAndArgs: util.CompilerPathAndArgs = util.extractCompilerPathAndArgs(configuration.compilerPath); + return compilerPathAndArgs.compilerPath.endsWith("cl.exe") === (configuration.intelliSenseMode === "msvc-x64"); } public addToIncludePathCommand(path: string): void { @@ -635,6 +623,19 @@ export class CppProperties { }); } + private ensureSettingsPanelInitlialized(): void { + if (this.settingsPanel === undefined) { + let settings: CppSettings = new CppSettings(this.rootUri); + this.settingsPanel = new SettingsPanel(); + this.settingsPanel.setKnownCompilers(this.knownCompilers, settings.preferredPathSeparator); + this.settingsPanel.SettingsPanelActivated(() => this.onSettingsPanelActivated()); + this.settingsPanel.ConfigValuesChanged(() => this.saveConfigurationUI()); + this.settingsPanel.ConfigSelectionChanged(() => this.onConfigSelectionChanged()); + this.settingsPanel.AddConfigRequested((e) => this.onAddConfigRequested(e)); + this.disposables.push(this.settingsPanel); + } + } + public handleConfigurationEditUICommand(onCreation: () => void, showDocument: (document: vscode.TextDocument) => void): void { this.ensurePropertiesFile().then(() => { if (this.propertiesFile) { @@ -642,18 +643,13 @@ export class CppProperties { onCreation(); } if (this.parsePropertiesFile(false)) { - // Parse successful, show UI - if (this.settingsPanel === undefined) { - let settings: CppSettings = new CppSettings(this.rootUri); - this.settingsPanel = new SettingsPanel(); - this.settingsPanel.setKnownCompilers(this.knownCompilers, settings.preferredPathSeparator); - this.settingsPanel.SettingsPanelActivated(() => this.onSettingsPanelActivated()); - this.settingsPanel.ConfigValuesChanged(() => this.saveConfigurationUI()); - this.disposables.push(this.settingsPanel); - } - this.settingsPanel.createOrShow( - this.configurationJson.configurations[this.currentConfigurationIndex.Value], - this.getErrorsForConfigUI()); + this.ensureSettingsPanelInitlialized(); + + // Use the active configuration as the default selected configuration to load on UI editor + this.settingsPanel.selectedConfigIndex = this.currentConfigurationIndex.Value; + this.settingsPanel.createOrShow(this.ConfigurationNames, + this.configurationJson.configurations[this.settingsPanel.selectedConfigIndex], + this.getErrorsForConfigUI(this.settingsPanel.selectedConfigIndex)); } else { // Parse failed, open json file vscode.workspace.openTextDocument(this.propertiesFile).then((document: vscode.TextDocument) => { @@ -673,9 +669,12 @@ export class CppProperties { if (this.parsePropertiesFile(false)) { // The settings UI became visible or active. // Ensure settingsPanel has copy of latest current configuration - this.settingsPanel.updateConfigUI( - this.configurationJson.configurations[this.currentConfigurationIndex.Value], - this.getErrorsForConfigUI()); + if (this.settingsPanel.selectedConfigIndex >= this.configurationJson.configurations.length) { + this.settingsPanel.selectedConfigIndex = this.currentConfigurationIndex.Value; + } + this.settingsPanel.updateConfigUI(this.ConfigurationNames, + this.configurationJson.configurations[this.settingsPanel.selectedConfigIndex], + this.getErrorsForConfigUI(this.settingsPanel.selectedConfigIndex)); } else { // Parse failed, open json file vscode.workspace.openTextDocument(this.propertiesFile); @@ -688,8 +687,33 @@ export class CppProperties { private saveConfigurationUI(): void { this.parsePropertiesFile(false); // Clear out any modifications we may have made internally. let config: Configuration = this.settingsPanel.getLastValuesFromConfigUI(); - this.configurationJson.configurations[this.currentConfigurationIndex.Value] = config; - this.settingsPanel.updateErrors(this.getErrorsForConfigUI()); + this.configurationJson.configurations[this.settingsPanel.selectedConfigIndex] = config; + this.settingsPanel.updateErrors(this.getErrorsForConfigUI(this.settingsPanel.selectedConfigIndex)); + this.writeToJson(); + } + + private onConfigSelectionChanged(): void { + this.settingsPanel.updateConfigUI(this.ConfigurationNames, + this.configurationJson.configurations[this.settingsPanel.selectedConfigIndex], + this.getErrorsForConfigUI(this.settingsPanel.selectedConfigIndex)); + } + + private onAddConfigRequested(configName: string): void { + this.parsePropertiesFile(false); // Clear out any modifications we may have made internally. + + // Create default config and add to list of configurations + let newConfig: Configuration = { name: configName }; + this.applyDefaultConfigurationValues(newConfig); + delete newConfig.knownCompilers; + this.configurationJson.configurations.push(newConfig); + + // Update UI + this.settingsPanel.selectedConfigIndex = this.configurationJson.configurations.length - 1; + this.settingsPanel.updateConfigUI(this.ConfigurationNames, + this.configurationJson.configurations[this.settingsPanel.selectedConfigIndex], + null); + + // Save new config to file this.writeToJson(); } @@ -885,12 +909,14 @@ export class CppProperties { return result; } - private getErrorsForConfigUI(): ConfigurationErrors { + private getErrorsForConfigUI(configIndex: number): ConfigurationErrors { let errors: ConfigurationErrors = {}; const isWindows: boolean = os.platform() === 'win32'; + let config: Configuration = this.configurationJson.configurations[configIndex]; + // Validate compilerPath - let resolvedCompilerPath: string = this.resolvePath(this.CurrentConfiguration.compilerPath, isWindows); + let resolvedCompilerPath: string = this.resolvePath(config.compilerPath, isWindows); let compilerPathAndArgs: util.CompilerPathAndArgs = util.extractCompilerPathAndArgs(resolvedCompilerPath); if (resolvedCompilerPath && // Don't error cl.exe paths because it could be for an older preview build. @@ -949,8 +975,8 @@ export class CppProperties { // Validate includePath let includePathErrors: string[] = []; - if (this.CurrentConfiguration.includePath) { - for (let includePath of this.CurrentConfiguration.includePath) { + if (config.includePath) { + for (let includePath of config.includePath) { let pathExists: boolean = true; let resolvedIncludePath: string = this.resolvePath(includePath, isWindows); if (!resolvedIncludePath) { @@ -987,8 +1013,8 @@ export class CppProperties { } // Validate intelliSenseMode - if (isWindows && !this.isCompilerIntelliSenseModeCompatible()) { - errors.intelliSenseMode = `IntelliSense mode ${this.CurrentConfiguration.intelliSenseMode} is incompatible with compiler path.`; + if (isWindows && !this.isCompilerIntelliSenseModeCompatible(config)) { + errors.intelliSenseMode = `IntelliSense mode ${config.intelliSenseMode} is incompatible with compiler path.`; } return errors; @@ -1055,7 +1081,7 @@ export class CppProperties { const intelliSenseModeValueStart: number = curText.indexOf('"', curText.indexOf(":", intelliSenseModeStart)); const intelliSenseModeValueEnd: number = intelliSenseModeStart === -1 ? -1 : curText.indexOf('"', intelliSenseModeValueStart + 1) + 1; - if (!this.isCompilerIntelliSenseModeCompatible()) { + if (!this.isCompilerIntelliSenseModeCompatible(this.CurrentConfiguration)) { let message: string = `intelliSenseMode ${this.CurrentConfiguration.intelliSenseMode} is incompatible with compilerPath.`; let diagnostic: vscode.Diagnostic = new vscode.Diagnostic( new vscode.Range(document.positionAt(curTextStartOffset + intelliSenseModeValueStart), diff --git a/Extension/src/LanguageServer/settingsPanel.ts b/Extension/src/LanguageServer/settingsPanel.ts index 5cd9be9de5..761b27876a 100644 --- a/Extension/src/LanguageServer/settingsPanel.ts +++ b/Extension/src/LanguageServer/settingsPanel.ts @@ -13,37 +13,75 @@ import * as telemetry from '../telemetry'; // TODO: share ElementId between SettingsPanel and SettingsApp. Investigate why SettingsApp cannot import/export const elementId: { [key: string]: string } = { + // Basic settings configName: "configName", + configSelection: "configSelection", + addConfigBtn: "addConfigBtn", + addConfigOk: "addConfigOk", + addConfigCancel: "addConfigCancel", + addConfigName: "addConfigName", + compilerPath: "compilerPath", - intelliSenseMode: "intelliSenseMode", + compilerPathInvalid: "compilerPathInvalid", + knownCompilers: "knownCompilers", + + intelliSenseMode: "intelliSenseMode", + intelliSenseModeInvalid: "intelliSenseModeInvalid", includePath: "includePath", + includePathInvalid: "includePathInvalid", defines: "defines", cStandard: "cStandard", - cppStandard: "cppStandard" + cppStandard: "cppStandard", + + // Advanced settings + windowsSdkVersion: "windowsSdkVersion", + macFrameworkPath: "macFrameworkPath", + compileCommands: "compileCommands", + configurationProvider: "configurationProvider", + forcedInclude: "forcedInclude", + + // Browse properties + browsePath: "browsePath", + limitSymbolsToIncludedHeaders: "limitSymbolsToIncludedHeaders", + databaseFilename: "databaseFilename", + + // Other + showAdvancedBtn: "showAdvancedBtn" }; export class SettingsPanel { - private configValues: config.Configuration; - private compilerPaths: string[] = []; - private isIntelliSenseModeDefined: boolean = false; + private telemetry: { [key: string]: number } = {}; + private disposable: vscode.Disposable = undefined; + + // Events private settingsPanelActivated = new vscode.EventEmitter(); private configValuesChanged = new vscode.EventEmitter(); + private configSelectionChanged = new vscode.EventEmitter(); + private addConfigRequested = new vscode.EventEmitter(); + + // Configuration data + private configValues: config.Configuration; + private isIntelliSenseModeDefined: boolean = false; + private configIndexSelected: number = 0; + private compilerPaths: string[] = []; + + // WebviewPanel objects private panel: vscode.WebviewPanel; - private disposable: vscode.Disposable = undefined; private disposablesPanel: vscode.Disposable = undefined; private static readonly viewType: string = 'settingsPanel'; private static readonly title: string = 'C/C++ Configurations'; - private telemetry: { [key: string]: number } = {}; constructor() { this.configValues = { name: undefined }; this.disposable = vscode.Disposable.from( this.settingsPanelActivated, - this.configValuesChanged + this.configValuesChanged, + this.configSelectionChanged, + this.addConfigRequested ); } - public createOrShow(activeConfiguration: config.Configuration, errors: config.ConfigurationErrors): void { + public createOrShow(configSelection: string[], activeConfiguration: config.Configuration, errors: config.ConfigurationErrors): void { const column: vscode.ViewColumn = vscode.window.activeTextEditor ? vscode.window.activeTextEditor.viewColumn : undefined; @@ -83,7 +121,7 @@ export class SettingsPanel { this.panel.webview.html = this.getHtml(); - this.updateWebview(activeConfiguration, errors); + this.updateWebview(configSelection, activeConfiguration, errors); } public get SettingsPanelActivated(): vscode.Event { @@ -94,13 +132,29 @@ export class SettingsPanel { return this.configValuesChanged.event; } + public get ConfigSelectionChanged(): vscode.Event { + return this.configSelectionChanged.event; + } + + public get AddConfigRequested(): vscode.Event { + return this.addConfigRequested.event; + } + + public get selectedConfigIndex(): number { + return this.configIndexSelected; + } + + public set selectedConfigIndex(index: number) { + this.configIndexSelected = index; + } + public getLastValuesFromConfigUI(): config.Configuration { return this.configValues; } - public updateConfigUI(configuration: config.Configuration, errors: config.ConfigurationErrors): void { + public updateConfigUI(configSelection: string[], configuration: config.Configuration, errors: config.ConfigurationErrors|null): void { if (this.panel) { - this.updateWebview(configuration, errors); + this.updateWebview(configSelection, configuration, errors); } } @@ -129,7 +183,7 @@ export class SettingsPanel { } public dispose(): void { - // Log any telementry + // Log any telemetry if (Object.keys(this.telemetry).length > 0) { telemetry.logLanguageServerEvent("ConfigUI", null, this.telemetry); } @@ -153,13 +207,16 @@ export class SettingsPanel { } } - private updateWebview(configuration: config.Configuration, errors: config.ConfigurationErrors): void { + private updateWebview(configSelection: string[], configuration: config.Configuration, errors: config.ConfigurationErrors|null): void { this.configValues = Object.assign({}, configuration); // Copy configuration values this.isIntelliSenseModeDefined = (this.configValues.intelliSenseMode !== undefined); if (this.panel) { this.panel.webview.postMessage({ command: 'setKnownCompilers', compilers: this.compilerPaths}); + this.panel.webview.postMessage({ command: 'updateConfigSelection', selections: configSelection, selectedIndex: this.configIndexSelected}); this.panel.webview.postMessage({ command: 'updateConfig', config: this.configValues}); - this.panel.webview.postMessage({ command: 'updateErrors', errors: errors}); + if (errors !== null) { + this.panel.webview.postMessage({ command: 'updateErrors', errors: errors}); + } } } @@ -182,30 +239,55 @@ export class SettingsPanel { switch (message.command) { case 'change': this.updateConfig(message); + break; + case 'configSelect': + this.configSelect(message.index); + break; + case 'addConfig': + this.addConfig(message.name); + break; + case 'knownCompilerSelect': + this.knownCompilerSelect(); + break; + } + } + + private addConfig(name: string): void { + this.addConfigRequested.fire(name); + this.logTelemetryForElement(elementId.addConfigName); + } + + private configSelect(index: number): void { + this.configIndexSelected = index; + this.configSelectionChanged.fire(); + this.logTelemetryForElement(elementId.configSelection); + } + + private knownCompilerSelect(): void { + this.logTelemetryForElement(elementId.knownCompilers); + // Remove one count from compilerPath because selecting a different compiler causes a change on the compiler path + if (this.telemetry[elementId.compilerPath]) { + this.telemetry[elementId.compilerPath]--; } } private updateConfig(message: any): void { - let entries: string[]; + let splitEntries: (input: any) => string[] = (input: any) => { + return input.split("\n").filter((e: string) => e); + }; switch (message.key) { case elementId.configName: this.configValues.name = message.value; - this.logTelementryForElement(elementId.configName); break; case elementId.compilerPath: this.configValues.compilerPath = message.value; - this.logTelementryForElement(elementId.compilerPath); break; case elementId.includePath: - entries = message.value.split("\n"); - this.configValues.includePath = entries.filter(e => e); - this.logTelementryForElement(elementId.includePath); + this.configValues.includePath = splitEntries(message.value); break; case elementId.defines: - entries = message.value.split("\n"); - this.configValues.defines = entries.filter(e => e); - this.logTelementryForElement(elementId.defines); + this.configValues.defines = splitEntries(message.value); break; case elementId.intelliSenseMode: if (message.value !== "${default}" || this.isIntelliSenseModeDefined) { @@ -213,28 +295,59 @@ export class SettingsPanel { } else { this.configValues.intelliSenseMode = undefined; } - this.logTelementryForElement(elementId.intelliSenseMode); break; case elementId.cStandard: this.configValues.cStandard = message.value; - this.logTelementryForElement(elementId.cStandard); break; case elementId.cppStandard: this.configValues.cppStandard = message.value; - this.logTelementryForElement(elementId.cppStandard); + break; + case elementId.windowsSdkVersion: + this.configValues.windowsSdkVersion = message.value; + break; + case elementId.macFrameworkPath: + this.configValues.macFrameworkPath = splitEntries(message.value); + break; + case elementId.compileCommands: + this.configValues.compileCommands = message.value; + break; + case elementId.configurationProvider: + this.configValues.configurationProvider = message.value; + break; + case elementId.forcedInclude: + this.configValues.forcedInclude = splitEntries(message.value); + break; + case elementId.browsePath: + this.initializeBrowseProperties(); + this.configValues.browse.path = splitEntries(message.value); + break; + case elementId.limitSymbolsToIncludedHeaders: + this.initializeBrowseProperties(); + this.configValues.browse.limitSymbolsToIncludedHeaders = message.value; + break; + case elementId.databaseFilename: + this.initializeBrowseProperties(); + this.configValues.browse.databaseFilename = message.value; break; } this.configValuesChanged.fire(); + this.logTelemetryForElement(message.key); } - private logTelementryForElement(elementId: string): void { + private logTelemetryForElement(elementId: string): void { if (this.telemetry[elementId] === undefined) { this.telemetry[elementId] = 0; } this.telemetry[elementId]++; } + private initializeBrowseProperties(): void { + if (this.configValues.browse === undefined) { + this.configValues.browse = {}; + } + } + private getHtml(): string { let content: string | undefined; content = fs.readFileSync(util.getExtensionFilePath("ui/settings.html")).toString(); diff --git a/Extension/ui/settings.html b/Extension/ui/settings.html index 65bae3edac..ef0d39f95d 100644 --- a/Extension/ui/settings.html +++ b/Extension/ui/settings.html @@ -36,10 +36,44 @@ background: var(--vscode-inputValidation-errorBackground); border: solid 1px var(--vscode-inputValidation-errorBorder); white-space: pre-wrap; - visibility: hidden; + display: none; padding: 1px 4px 4px 4px; } + .headerBtn, + .headerBtn:hover, + .headerBtn:active { + cursor: pointer; + color: var(--vscode-foreground); + background: var(--vscode-background); + border: 0px; + padding: 0px; + outline: none; + } + + .headerBtn:focus { + outline: 1px solid -webkit-focus-ring-color; + outline-offset: 3px; + } + + .expand:after { + font-weight: bold; + float: right; + margin-right: 6px; + margin-top: -4px; + content: '\276E'; + transform: rotate(90deg); + } + + .collapse:after { + font-weight: bold; + float: right; + margin-right: 6px; + margin-top: -4px; + content: '\276E'; + transform: rotate(270deg); + } + .main { max-width: 800px; margin-left: 320px; @@ -90,10 +124,20 @@ line-height: 19px } + body:not(.tabbing) button:focus { + outline: none; + } + a { text-decoration: none } + checkbox { + color: var(--vscode-settings-checkboxForeground); + background: var(--vscode-settings-checkboxForeground); + border: var(--vscode-settings-checkboxBorder); + } + a:focus, input:focus, select:focus, @@ -122,6 +166,10 @@ text-decoration: underline } + ::placeholder { + color: var(--vscode-input-placeholderForeground); + } + input { height: 17px; padding: 6px; @@ -143,6 +191,26 @@ border: 1px solid var(--vscode-settings-textInputBorder); } + button { + color: var(--vscode-button-foreground); + background-color: var(--vscode-button-background); + border: solid 1px var(--vscode-contrastBorder); + padding: 6px 14px; + } + + button:hover { + background-color: var(--vscode-button-hoverBackground); + } + + button:focus { + outline: 1px solid -webkit-focus-ring-color; + outline-offset: 2px + } + + button:active { + outline: none; + } + .select-default { width: 300px; height: 27px; @@ -157,7 +225,7 @@ position: relative; background-color: var(--vscode-settings-textInputBackground); width: 800px; - height: 30px; + height: 31px; } .select-editable select { @@ -165,7 +233,7 @@ font-size: 13px; font-family: sans-serif; border: 1px solid var(--vscode-settings-textInputBorder); - height: 30px; + height: 31px; margin: auto; color: var(--vscode-settings-textInputForeground); background: var(--vscode-settings-textInputBackground); @@ -177,7 +245,7 @@ left: 1px; right: 1px; bottom: 1px; - height: 16px; + height: 17px; font-size: 13px; border: none; color: var(--vscode-settings-textInputForeground); @@ -367,8 +435,8 @@
IntelliSense Configurations
Use this editor to edit basic IntelliSense settings defined in the underlying c_cpp_properties.json file. - Changes made in this editor only apply to the active configuration. To modify other configurations change the active configuration. - To edit multiple configurations at once, or additional settings not shown here, go to c_cpp_properties.json. + Changes made in this editor only apply to the selected configuration. + To edit multiple configurations at once go to c_cpp_properties.json.
@@ -380,11 +448,32 @@
Configuration name
A friendly name that identifies a configuration. Mac, Linux, and Win32 are special identifiers for configurations that will be auto-selected on those platforms, but the name of the identifier can be anything. - To edit the name of the current active configuration, go to the c_cpp_properties.json file and edit the name property.
- +
+
Select a configuration set to edit.
+ + + + + + + +
+
+ + +
+
+
+ +
+ +
+
@@ -395,10 +484,10 @@
Specify a compiler path or select a detected compiler path from the drop-down list.
-
- - -
+
+ + +
@@ -411,7 +500,7 @@ Select a specific IntelliSense mode to override the ${default} mode.
- @@ -428,7 +517,7 @@
One include path per line.
- +
@@ -440,15 +529,15 @@
One definition per line.
- +
-
C Standard
+
C standard
The version of the C language standard to use for IntelliSense.
- @@ -457,20 +546,116 @@
-
C++ Standard
+
C++ standard
The version of the C++ language standard to use for IntelliSense.
- -
- +
+
+ +
+ +
+ +
+
Configuration provider
+
+ The ID of a VS Code extension that can provide IntelliSense configuration information for source files. +
+
+ +
+
+ +
+
Windows SDK version
+
+ Version of the Windows SDK include path to use on Windows, e.g. 10.0.17134.0. +
+
+ +
+
+ +
+
Mac framework path
+
+ A list of paths for the Intellisense engine to use while searching for included headers from Mac frameworks. Only supported on Mac configuration. +
+
+
One path per line.
+ +
+
+ +
+
Forced include
+
+ A list of files that should be included before any other characters in the source file are processed. Files are included in the order listed. +
+
+
One file per line.
+ +
+
+ +
+
Compile commands
+
+ Full path to compile_commands.json file for the workspace. +
+
+ +
+
+ +
+
Browse: path
+
+ A list of paths for the tag parser to use while searching for included headers. + Searching on these paths is recursive by default. Specify * to indicate non-recursive search. + For example: /usr/include will search through all subdirectories while /usr/include/* will not. +
+
+
One browse path per line.
+ +
+
+ +
+
Browse: limit symbols to included headers
+
+ + Process only those files directly or indirectly included as headers. If not set, process all files under the specified include paths. + +
+
+ +
+
Browse: database filename
+
+ Path to the generated symbol database. If a relative path is specified, + it will be made relative to the workspace's default storage location. +
+
+ +
+
+ +
+ + + diff --git a/Extension/ui/settings.ts b/Extension/ui/settings.ts index bc689e43ae..c1b8f2db51 100644 --- a/Extension/ui/settings.ts +++ b/Extension/ui/settings.ts @@ -5,17 +5,43 @@ 'use strict'; const elementId: { [key: string]: string } = { + // Basic settings configName: "configName", + configSelection: "configSelection", + addConfigDiv: "addConfigDiv", + addConfigBtn: "addConfigBtn", + addConfigInputDiv: "addConfigInputDiv", + addConfigOk: "addConfigOk", + addConfigCancel: "addConfigCancel", + addConfigName: "addConfigName", + compilerPath: "compilerPath", - intelliSenseMode: "intelliSenseMode", + compilerPathInvalid: "compilerPathInvalid", + knownCompilers: "knownCompilers", + + intelliSenseMode: "intelliSenseMode", + intelliSenseModeInvalid: "intelliSenseModeInvalid", includePath: "includePath", + includePathInvalid: "includePathInvalid", defines: "defines", cStandard: "cStandard", cppStandard: "cppStandard", - compilerPathInvalid: "compilerPathInvalid", - intelliSenseModeInvalid: "intelliSenseModeInvalid", - includePathInvalid: "includePathInvalid", - knownCompilers: "knownCompilers" + + // Advanced settings + windowsSdkVersion: "windowsSdkVersion", + macFrameworkPath: "macFrameworkPath", + compileCommands: "compileCommands", + configurationProvider: "configurationProvider", + forcedInclude: "forcedInclude", + + // Browse properties + browsePath: "browsePath", + limitSymbolsToIncludedHeaders: "limitSymbolsToIncludedHeaders", + databaseFilename: "databaseFilename", + + // Other + showAdvanced: "showAdvanced", + advancedSection: "advancedSection" }; interface VsCodeApi { @@ -33,26 +59,146 @@ class SettingsApp { constructor() { this.vsCodeApi = acquireVsCodeApi(); - window.addEventListener('message', this.onMessageReceived.bind(this)); + window.addEventListener("keydown", this.handleTab.bind(this)); + window.addEventListener("message", this.onMessageReceived.bind(this)); - document.getElementById(elementId.configName).addEventListener("change", this.onChanged.bind(this, elementId.configName)); - - document.getElementById(elementId.compilerPath).addEventListener("change", this.onChanged.bind(this, elementId.compilerPath)); - document.getElementById(elementId.intelliSenseMode).addEventListener("change", this.onChanged.bind(this, elementId.intelliSenseMode)); + // Add event listeners to UI elements + this.addEventsToConfigNameChanges(); + this.addEventsToInputValues(); + document.getElementById(elementId.knownCompilers).addEventListener("change", this.onKnownCompilerSelect.bind(this)); - document.getElementById(elementId.includePath).addEventListener("change", this.onChanged.bind(this, elementId.includePath)); - document.getElementById(elementId.defines).addEventListener("change", this.onChanged.bind(this, elementId.defines)); + // Set view state of advanced settings and add event + const oldState: any = this.vsCodeApi.getState(); + const advancedShown: boolean = (oldState && oldState.advancedShown); + document.getElementById(elementId.advancedSection).style.display = advancedShown ? "block" : "none"; + document.getElementById(elementId.showAdvanced).classList.toggle(advancedShown ? "collapse" : "expand", true); + document.getElementById(elementId.showAdvanced).addEventListener("click", this.onShowAdvanced.bind(this)); + } - document.getElementById(elementId.cStandard).addEventListener("change", this.onChanged.bind(this, elementId.cStandard)); - document.getElementById(elementId.cppStandard).addEventListener("change", this.onChanged.bind(this, elementId.cppStandard)); + private addEventsToInputValues(): void { + const elements: NodeListOf = document.getElementsByName("inputValue"); + elements.forEach(el => { + el.addEventListener("change", this.onChanged.bind(this, el.id)); + }); - document.getElementById(elementId.knownCompilers).addEventListener("change", this.onKnownCompilerSelect.bind(this)); + // Special case for checkbox element + document.getElementById(elementId.limitSymbolsToIncludedHeaders).addEventListener("change", this.onChangedCheckbox.bind(this, elementId.limitSymbolsToIncludedHeaders)); + } + + private addEventsToConfigNameChanges(): void { + document.getElementById(elementId.configName).addEventListener("change", this.onConfigNameChanged.bind(this)); + document.getElementById(elementId.configSelection).addEventListener("change", this.onConfigSelect.bind(this)); + document.getElementById(elementId.addConfigBtn).addEventListener("click", this.onAddConfigBtn.bind(this)); + document.getElementById(elementId.addConfigOk).addEventListener("click", this.OnAddConfigConfirm.bind(this, true)); + document.getElementById(elementId.addConfigCancel).addEventListener("click", this.OnAddConfigConfirm.bind(this, false)); + } + + private handleTab(e: any): void { + if (e.keyCode === 9) { + document.body.classList.add("tabbing"); + window.removeEventListener("keydown", this.handleTab); + window.addEventListener("mousedown", this.handleMouseDown.bind(this)); + } + } + + private handleMouseDown(): void { + document.body.classList.remove("tabbing"); + window.removeEventListener("mousedown", this.handleMouseDown); + window.addEventListener("keydown", this.handleTab.bind(this)); + } + + private onShowAdvanced(): void { + const isShown: boolean = (document.getElementById(elementId.advancedSection).style.display === "block"); + document.getElementById(elementId.advancedSection).style.display = isShown ? "none" : "block"; + + // Save view state + this.vsCodeApi.setState({ advancedShown: !isShown }); + + // Update chevron on button + const element: HTMLElement = document.getElementById(elementId.showAdvanced); + element.classList.toggle("collapse"); + element.classList.toggle("expand"); + } + + private onAddConfigBtn(): void { + this.showElement(elementId.addConfigDiv, false); + this.showElement(elementId.addConfigInputDiv, true); + } + + private OnAddConfigConfirm(request: boolean): void { + this.showElement(elementId.addConfigInputDiv, false); + this.showElement(elementId.addConfigDiv, true); + + // If request is yes, send message to create new config + if (request) { + const x: HTMLInputElement = document.getElementById(elementId.addConfigName); + if (x.value !== undefined && x.value !== "") { + this.vsCodeApi.postMessage({ + command: "addConfig", + name: x.value + }); + } + } + } + + private onConfigNameChanged(): void { + if (this.updating) { + return; + } + + const configName: HTMLInputElement = document.getElementById(elementId.configName); + let list: HTMLSelectElement = document.getElementById(elementId.configSelection); + + if (configName.value === "") { + (document.getElementById(elementId.configName)).value = list.options[list.selectedIndex].value; + return; + } + + // Update name on selection + list.options[list.selectedIndex].value = configName.value; + list.options[list.selectedIndex].text = configName.value; + + this.onChanged(elementId.configName); + } + + private onConfigSelect(): void { + if (this.updating) { + return; + } + + const x: HTMLSelectElement = document.getElementById(elementId.configSelection); + (document.getElementById(elementId.configName)).value = x.value; + + this.vsCodeApi.postMessage({ + command: "configSelect", + index: x.selectedIndex + }); } private onKnownCompilerSelect(): void { + if (this.updating) { + return; + } const x: HTMLInputElement = document.getElementById(elementId.knownCompilers); (document.getElementById(elementId.compilerPath)).value = x.value; this.onChanged(elementId.compilerPath); + + // Post message that this control was used for telemetry + this.vsCodeApi.postMessage({ + command: "knownCompilerSelect" + }); + } + private onChangedCheckbox(id: string): void { + if (this.updating) { + return; + } + + const el: HTMLInputElement = document.getElementById(id); + this.vsCodeApi.postMessage({ + command: "change", + key: id, + value: el.checked + }); } private onChanged(id: string): void { @@ -60,11 +206,11 @@ class SettingsApp { return; } - const x: HTMLInputElement = document.getElementById(id); + const el: HTMLInputElement = document.getElementById(id); this.vsCodeApi.postMessage({ command: "change", key: id, - value: x.value + value: el.value }); } @@ -80,14 +226,17 @@ class SettingsApp { case 'setKnownCompilers': this.setKnownCompilers(message.compilers); break; + case 'updateConfigSelection': + this.updateConfigSelection(message); + break; } } private updateConfig(config: any): void { this.updating = true; try { + // Basic settings (document.getElementById(elementId.configName)).value = config.name; - (document.getElementById(elementId.compilerPath)).value = config.compilerPath ? config.compilerPath : ""; (document.getElementById(elementId.intelliSenseMode)).value = config.intelliSenseMode ? config.intelliSenseMode : "${default}"; @@ -99,6 +248,30 @@ class SettingsApp { (document.getElementById(elementId.cStandard)).value = config.cStandard; (document.getElementById(elementId.cppStandard)).value = config.cppStandard; + + // Advanced settings + (document.getElementById(elementId.windowsSdkVersion)).value = config.windowsSdkVersion ? config.windowsSdkVersion : ""; + + (document.getElementById(elementId.macFrameworkPath)).value = + (config.macFrameworkPath && config.macFrameworkPath.length > 0) ? config.macFrameworkPath.join("\n") : ""; + + (document.getElementById(elementId.compileCommands)).value = config.compileCommands ? config.compileCommands : ""; + (document.getElementById(elementId.configurationProvider)).value = config.configurationProvider ? config.configurationProvider : ""; + + (document.getElementById(elementId.forcedInclude)).value = + (config.forcedInclude && config.forcedInclude.length > 0) ? config.forcedInclude.join("\n") : ""; + + if (config.browse) { + (document.getElementById(elementId.browsePath)).value = + (config.browse.path && config.browse.path.length > 0) ? config.browse.path.join("\n") : ""; + (document.getElementById(elementId.limitSymbolsToIncludedHeaders)).checked = + (config.browse.limitSymbolsToIncludedHeaders && config.browse.limitSymbolsToIncludedHeaders); + (document.getElementById(elementId.databaseFilename)).value = config.browse.databaseFilename ? config.browse.databaseFilename : ""; + } else { + (document.getElementById(elementId.browsePath)).value = ""; + (document.getElementById(elementId.limitSymbolsToIncludedHeaders)).checked = false; + (document.getElementById(elementId.databaseFilename)).value = ""; + } } finally { this.updating = false; } @@ -116,40 +289,71 @@ class SettingsApp { } private showErrorWithInfo(elementID: string, errorInfo: string): void { - document.getElementById(elementID).style.visibility = errorInfo ? "visible" : "hidden"; - document.getElementById(elementID).innerHTML = errorInfo ? errorInfo : ""; + this.showElement(elementID, errorInfo ? true : false); + document.getElementById(elementID).innerHTML = errorInfo ? errorInfo : ""; } - private setKnownCompilers(compilers: string[]): void { - let list: HTMLElement = document.getElementById(elementId.knownCompilers); + private updateConfigSelection(message: any): void { + this.updating = true; + try { + let list: HTMLSelectElement = document.getElementById(elementId.configSelection); - // No need to add items unless webview is reloaded, in which case it will not have any elements. - // Otherwise, add items again. - if (list.firstChild) { - return; - } + // Clear list before updating + list.options.length = 0; - if (compilers.length === 0) { - const noCompilers: string = "(No compiler paths detected)"; - let option: HTMLOptionElement = document.createElement("option"); - option.text = noCompilers; - option.value = noCompilers; - list.append(option); - - // Set the selection to this one item so that no selection change event will be fired - (list).value = noCompilers; - return; + // Update list + for (let name of message.selections) { + let option: HTMLOptionElement = document.createElement("option"); + option.text = name; + option.value = name; + list.append(option); + } + + list.selectedIndex = message.selectedIndex; + } finally { + this.updating = false; } + } + + private setKnownCompilers(compilers: string[]): void { + this.updating = true; + try { + let list: HTMLSelectElement = document.getElementById(elementId.knownCompilers); - for (let path of compilers) { - let option: HTMLOptionElement = document.createElement("option"); - option.text = path; - option.value = path; - list.append(option); + // No need to add items unless webview is reloaded, in which case it will not have any elements. + // Otherwise, add items again. + if (list.firstChild) { + return; + } + + if (compilers.length === 0) { + const noCompilers: string = "(No compiler paths detected)"; + let option: HTMLOptionElement = document.createElement("option"); + option.text = noCompilers; + option.value = noCompilers; + list.append(option); + + // Set the selection to this one item so that no selection change event will be fired + list.value = noCompilers; + return; + } + + for (let path of compilers) { + let option: HTMLOptionElement = document.createElement("option"); + option.text = path; + option.value = path; + list.append(option); + } + + // Initialize list with no selected item + list.value = ""; + } finally { + this.updating = false; } + } - // Initialize list with no selected item - (list).value = ""; + private showElement(elementID: string, show: boolean): void { + document.getElementById(elementID).style.display = show ? "block" : "none"; } }