diff --git a/Extension/c_cpp_properties.schema.json b/Extension/c_cpp_properties.schema.json index a07d242f0c..cd7e174d90 100644 --- a/Extension/c_cpp_properties.schema.json +++ b/Extension/c_cpp_properties.schema.json @@ -70,9 +70,20 @@ ] }, "compileCommands": { - "markdownDescription": "Full path to `compile_commands.json` file for the workspace.", - "descriptionHint": "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.", - "type": "string" + "oneOf": [ + { + "type": "string" + }, + { + "type": "array", + "items": { + "type": "string" + }, + "uniqueItems": true + } + ], + "markdownDescription": "Full path or a list of full paths to `compile_commands.json` files for the workspace.", + "descriptionHint": "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." }, "includePath": { "markdownDescription": "A list of paths for the IntelliSense engine to use while searching for included headers. Searching on these paths is not recursive. Specify `**` to indicate recursive search. For example, `${workspaceFolder}/**` will search through all subdirectories while `${workspaceFolder}` will not. Usually, this should not include system includes; instead, set `C_Cpp.default.compilerPath`.", @@ -239,7 +250,6 @@ }, "enableConfigurationSquiggles": { "type": "boolean", - "default": true, "markdownDescription": "Controls whether the extension will report errors detected in `c_cpp_properties.json`.", "descriptionHint": "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." } diff --git a/Extension/package.json b/Extension/package.json index cfdc2a73d7..2d36fdf323 100644 --- a/Extension/package.json +++ b/Extension/package.json @@ -693,7 +693,23 @@ "scope": "machine-overridable" }, "C_Cpp.default.compileCommands": { - "type": "string", + "oneOf": [ + { + "type": "string", + "default": "" + }, + { + "type": "array", + "items": { + "type": "string" + }, + "uniqueItems": true, + "default": [] + } + ], + "default": [ + "" + ], "markdownDescription": "%c_cpp.configuration.default.compileCommands.markdownDescription%", "scope": "machine-overridable" }, diff --git a/Extension/src/LanguageServer/configurations.ts b/Extension/src/LanguageServer/configurations.ts index 028cc7235c..62718852a0 100644 --- a/Extension/src/LanguageServer/configurations.ts +++ b/Extension/src/LanguageServer/configurations.ts @@ -78,8 +78,8 @@ export interface Configuration { defines?: string[]; intelliSenseMode?: string; intelliSenseModeIsExplicit?: boolean; - compileCommandsInCppPropertiesJson?: string; - compileCommands?: string; + compileCommandsInCppPropertiesJson?: string[]; + compileCommands?: string[]; forcedInclude?: string[]; configurationProviderInCppPropertiesJson?: string; configurationProvider?: string; @@ -136,7 +136,7 @@ export class CppProperties { private currentConfigurationIndex: PersistentFolderState | undefined; private configFileWatcher: vscode.FileSystemWatcher | null = null; private configFileWatcherFallbackTime: Date = new Date(); // Used when file watching fails. - private compileCommandsFile: vscode.Uri | undefined | null = undefined; + private compileCommandsFiles: Set = new Set(); private compileCommandsFileWatchers: fs.FSWatcher[] = []; private compileCommandsFileWatcherFallbackTime: Map = new Map(); // Used when file watching fails. private defaultCompilerPath: string | null = null; @@ -389,7 +389,8 @@ export class CppProperties { configuration.windowsSdkVersion = this.defaultWindowsSdkVersion; } if (isUnset(settings.defaultCompilerPath) && this.defaultCompilerPath && - (isUnset(settings.defaultCompileCommands) || settings.defaultCompileCommands === "") && !configuration.compileCommands) { + (isUnset(settings.defaultCompileCommands) || settings.defaultCompileCommands?.length === 0) && + (isUnset(configuration.compileCommands) || configuration.compileCommands?.length === 0)) { // 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. @@ -684,7 +685,7 @@ export class CppProperties { this.parsePropertiesFile(); // Clear out any modifications we may have made internally. const config: Configuration | undefined = this.CurrentConfiguration; if (config) { - config.compileCommands = path; + config.compileCommands = [path]; this.writeToJson(); } // Any time parsePropertiesFile is called, configurationJson gets @@ -938,7 +939,7 @@ export class CppProperties { configuration.macFrameworkPath = this.updateConfigurationStringArray(configuration.macFrameworkPath, settings.defaultMacFrameworkPath, env); configuration.windowsSdkVersion = this.updateConfigurationString(configuration.windowsSdkVersion, settings.defaultWindowsSdkVersion, env); configuration.forcedInclude = this.updateConfigurationPathsArray(configuration.forcedInclude, settings.defaultForcedInclude, env, false); - configuration.compileCommands = this.updateConfigurationString(configuration.compileCommands, settings.defaultCompileCommands, env); + configuration.compileCommands = this.updateConfigurationStringArray(configuration.compileCommands, settings.defaultCompileCommands, env); configuration.compilerArgs = this.updateConfigurationStringArray(configuration.compilerArgs, settings.defaultCompilerArgs, env); configuration.cStandard = this.updateConfigurationString(configuration.cStandard, settings.defaultCStandard, env); configuration.cppStandard = this.updateConfigurationString(configuration.cppStandard, settings.defaultCppStandard, env); @@ -1092,11 +1093,13 @@ export class CppProperties { } if (configuration.compileCommands) { - configuration.compileCommands = this.resolvePath(configuration.compileCommands); - if (!this.compileCommandsFileWatcherFallbackTime.has(configuration.compileCommands)) { - // Start tracking the fallback time for a new path. - this.compileCommandsFileWatcherFallbackTime.set(configuration.compileCommands, new Date()); - } + configuration.compileCommands = configuration.compileCommands.map((path: string) => this.resolvePath(path)); + configuration.compileCommands.forEach((path: string) => { + if (!this.compileCommandsFileWatcherFallbackTime.has(path)) { + // Start tracking the fallback time for a new path. + this.compileCommandsFileWatcherFallbackTime.set(path, new Date()); + } + }); } if (configuration.forcedInclude) { @@ -1120,10 +1123,12 @@ export class CppProperties { // Instead, we clear entries that are no longer relevant. const trackedCompileCommandsPaths: Set = new Set(); this.configurationJson?.configurations.forEach((config: Configuration) => { - const path = this.resolvePath(config.compileCommands); - if (path.length > 0) { - trackedCompileCommandsPaths.add(path); - } + config.compileCommands?.forEach((path: string) => { + const compileCommandsFile = this.resolvePath(path); + if (compileCommandsFile.length > 0) { + trackedCompileCommandsPaths.add(compileCommandsFile); + } + }); }); for (const path of this.compileCommandsFileWatcherFallbackTime.keys()) { @@ -1144,12 +1149,12 @@ export class CppProperties { this.compileCommandsFileWatchers = []; // reset it const filePaths: Set = new Set(); this.configurationJson.configurations.forEach(c => { - if (c.compileCommands) { - const fileSystemCompileCommandsPath: string = this.resolvePath(c.compileCommands); - if (fs.existsSync(fileSystemCompileCommandsPath)) { - filePaths.add(fileSystemCompileCommandsPath); + c.compileCommands?.forEach((path: string) => { + const compileCommandsFile: string = this.resolvePath(path); + if (fs.existsSync(compileCommandsFile)) { + filePaths.add(compileCommandsFile); } - } + }); }); try { filePaths.forEach((path: string) => { @@ -1412,6 +1417,18 @@ export class CppProperties { return; } + private forceCompileCommandsAsArray(compileCommandsInCppPropertiesJson: any): string[] | undefined { + if (util.isString(compileCommandsInCppPropertiesJson) && compileCommandsInCppPropertiesJson.length > 0) { + return [compileCommandsInCppPropertiesJson]; + } else if (util.isArrayOfString(compileCommandsInCppPropertiesJson)) { + const filteredArray: string[] = compileCommandsInCppPropertiesJson.filter(value => value.length > 0); + if (filteredArray.length > 0) { + return filteredArray; + } + } + return undefined; + } + private parsePropertiesFile(): boolean { if (!this.propertiesFile) { this.configurationJson = undefined; @@ -1441,6 +1458,13 @@ export class CppProperties { } } } + + // Configuration.compileCommands is allowed to be defined as a string in the schema, but we send an array to the language server. + // For having a predictable behavior, we convert it here to an array of strings. + for (let i: number = 0; i < newJson.configurations.length; i++) { + newJson.configurations[i].compileCommands = this.forceCompileCommandsAsArray(newJson.configurations[i].compileCommands); + } + this.configurationJson = newJson; if (this.CurrentConfigurationIndex < 0 || this.CurrentConfigurationIndex >= newJson.configurations.length) { const index: number | undefined = this.getConfigIndexForPlatform(newJson); @@ -1790,6 +1814,11 @@ export class CppProperties { const configurations: ConfigurationJson = jsonc.parse(configurationsText, undefined, true) as any; const currentConfiguration: Configuration = configurations.configurations[this.CurrentConfigurationIndex]; + // Configuration.compileCommands is allowed to be defined as a string in the schema, but we send an array to the language server. + // For having a predictable behavior, we convert it here to an array of strings. + // Squiggles are still handled for both cases. + currentConfiguration.compileCommands = this.forceCompileCommandsAsArray(currentConfiguration.compileCommands); + let curTextStartOffset: number = 0; if (!currentConfiguration.name) { return; @@ -1869,9 +1898,9 @@ export class CppProperties { curText = curText.substring(0, nextNameStart2); } if (this.prevSquiggleMetrics.get(currentConfiguration.name) === undefined) { - this.prevSquiggleMetrics.set(currentConfiguration.name, { PathNonExistent: 0, PathNotAFile: 0, PathNotADirectory: 0, CompilerPathMissingQuotes: 0, CompilerModeMismatch: 0, MultiplePathsNotAllowed: 0 }); + this.prevSquiggleMetrics.set(currentConfiguration.name, { PathNonExistent: 0, PathNotAFile: 0, PathNotADirectory: 0, CompilerPathMissingQuotes: 0, CompilerModeMismatch: 0, MultiplePathsNotAllowed: 0, MultiplePathsShouldBeSeparated: 0 }); } - const newSquiggleMetrics: { [key: string]: number } = { PathNonExistent: 0, PathNotAFile: 0, PathNotADirectory: 0, CompilerPathMissingQuotes: 0, CompilerModeMismatch: 0, MultiplePathsNotAllowed: 0 }; + const newSquiggleMetrics: { [key: string]: number } = { PathNonExistent: 0, PathNotAFile: 0, PathNotADirectory: 0, CompilerPathMissingQuotes: 0, CompilerModeMismatch: 0, MultiplePathsNotAllowed: 0, MultiplePathsShouldBeSeparated: 0 }; const isWindows: boolean = os.platform() === 'win32'; // TODO: Add other squiggles. @@ -1916,9 +1945,10 @@ export class CppProperties { } } } - if (currentConfiguration.compileCommands) { - paths.push(`${currentConfiguration.compileCommands}`); - } + + currentConfiguration.compileCommands?.forEach((file: string) => { + paths.push(`${file}`); + }); if (currentConfiguration.compilerPath) { // Unlike other cases, compilerPath may not start or end with " due to trimming of whitespace and the possibility of compiler args. @@ -1932,6 +1962,8 @@ export class CppProperties { const forcedeIncludeEnd: number = forcedIncludeStart === -1 ? -1 : curText.indexOf("]", forcedIncludeStart); const compileCommandsStart: number = curText.search(/\s*\"compileCommands\"\s*:\s*\"/); const compileCommandsEnd: number = compileCommandsStart === -1 ? -1 : curText.indexOf('"', curText.indexOf('"', curText.indexOf(":", compileCommandsStart)) + 1); + const compileCommandsArrayStart: number = curText.search(/\s*\"compileCommands\"\s*:\s*\[/); + const compileCommandsArrayEnd: number = compileCommandsArrayStart === -1 ? -1 : curText.indexOf("]", curText.indexOf("[", curText.indexOf(":", compileCommandsArrayStart)) + 1); const compilerPathStart: number = curText.search(/\s*\"compilerPath\"\s*:\s*\"/); const compilerPathValueStart: number = curText.indexOf('"', curText.indexOf(":", compilerPathStart)); const compilerPathEnd: number = compilerPathStart === -1 ? -1 : curText.indexOf('"', compilerPathValueStart + 1) + 1; @@ -2110,8 +2142,7 @@ export class CppProperties { newSquiggleMetrics.PathNonExistent++; } else { // Check for file versus path mismatches. - if ((curOffset >= forcedIncludeStart && curOffset <= forcedeIncludeEnd) || - (curOffset >= compileCommandsStart && curOffset <= compileCommandsEnd)) { + if (curOffset >= forcedIncludeStart && curOffset <= forcedeIncludeEnd) { if (expandedPaths.length > 1) { message = localize("multiple.paths.not.allowed", "Multiple paths are not allowed."); newSquiggleMetrics.MultiplePathsNotAllowed++; @@ -2121,6 +2152,20 @@ export class CppProperties { continue; } + message = localize("path.is.not.a.file", "Path is not a file: {0}", expandedPaths[0]); + newSquiggleMetrics.PathNotAFile++; + } + } else if ((curOffset >= compileCommandsStart && curOffset <= compileCommandsEnd) || + (curOffset >= compileCommandsArrayStart && curOffset <= compileCommandsArrayEnd)) { + if (expandedPaths.length > 1) { + message = localize("multiple.paths.should.be.separate.entries", "Multiple paths should be separate entries in an array."); + newSquiggleMetrics.MultiplePathsShouldBeSeparated++; + } else { + const resolvedPath = this.resolvePath(expandedPaths[0]); + if (util.checkFileExistsSync(resolvedPath)) { + continue; + } + message = localize("path.is.not.a.file", "Path is not a file: {0}", expandedPaths[0]); newSquiggleMetrics.PathNotAFile++; } @@ -2203,6 +2248,9 @@ export class CppProperties { if (newSquiggleMetrics.MultiplePathsNotAllowed !== this.prevSquiggleMetrics.get(currentConfiguration.name)?.MultiplePathsNotAllowed) { changedSquiggleMetrics.MultiplePathsNotAllowed = newSquiggleMetrics.MultiplePathsNotAllowed; } + if (newSquiggleMetrics.MultiplePathsShouldBeSeparated !== this.prevSquiggleMetrics.get(currentConfiguration.name)?.MultiplePathsShouldBeSeparated) { + changedSquiggleMetrics.MultiplePathsShouldBeSeparated = newSquiggleMetrics.MultiplePathsShouldBeSeparated; + } if (Object.keys(changedSquiggleMetrics).length > 0) { telemetry.logLanguageServerEvent("ConfigSquiggles", undefined, changedSquiggleMetrics); } @@ -2325,29 +2373,30 @@ export class CppProperties { public checkCompileCommands(): void { // Check for changes in case of file watcher failure. - const compileCommands: string | undefined = this.CurrentConfiguration?.compileCommands; + const compileCommands: string[] | undefined = this.CurrentConfiguration?.compileCommands; if (!compileCommands) { return; } - const compileCommandsFile: string | undefined = this.resolvePath(compileCommands); - fs.stat(compileCommandsFile, (err, stats) => { - if (err) { - if (err.code === "ENOENT" && this.compileCommandsFile) { - this.compileCommandsFileWatchers.forEach((watcher: fs.FSWatcher) => watcher.close()); - this.compileCommandsFileWatchers = []; // reset file watchers - this.onCompileCommandsChanged(compileCommandsFile); - this.compileCommandsFile = null; // File deleted - } - } else { - const compileCommandsLastChanged: Date | undefined = this.compileCommandsFileWatcherFallbackTime.get(compileCommandsFile); - if ((this.compileCommandsFile === undefined) || - (this.compileCommandsFile === null) || - (compileCommandsLastChanged !== undefined && stats.mtime > compileCommandsLastChanged)) { - this.compileCommandsFileWatcherFallbackTime.set(compileCommandsFile, new Date()); - this.onCompileCommandsChanged(compileCommandsFile); - this.compileCommandsFile = vscode.Uri.file(compileCommandsFile); // File created. + compileCommands.forEach((path: string) => { + const compileCommandsFile: string | undefined = this.resolvePath(path); + fs.stat(compileCommandsFile, (err, stats) => { + if (err) { + if (err.code === "ENOENT" && this.compileCommandsFiles.has(compileCommandsFile)) { + this.compileCommandsFileWatchers.forEach((watcher: fs.FSWatcher) => watcher.close()); + this.compileCommandsFileWatchers = []; // reset file watchers + this.onCompileCommandsChanged(compileCommandsFile); + this.compileCommandsFiles.delete(compileCommandsFile); // File deleted + } + } else { + const compileCommandsLastChanged: Date | undefined = this.compileCommandsFileWatcherFallbackTime.get(compileCommandsFile); + if (!this.compileCommandsFiles.has(compileCommandsFile) || + (compileCommandsLastChanged !== undefined && stats.mtime > compileCommandsLastChanged)) { + this.compileCommandsFileWatcherFallbackTime.set(compileCommandsFile, new Date()); + this.onCompileCommandsChanged(compileCommandsFile); + this.compileCommandsFiles.add(compileCommandsFile); // File created. + } } - } + }); }); } diff --git a/Extension/src/LanguageServer/settings.ts b/Extension/src/LanguageServer/settings.ts index fb96b2afab..8b682f4836 100644 --- a/Extension/src/LanguageServer/settings.ts +++ b/Extension/src/LanguageServer/settings.ts @@ -402,7 +402,17 @@ export class CppSettings extends Settings { public get defaultDotconfig(): string | undefined { return changeBlankStringToUndefined(this.getAsStringOrUndefined("default.dotConfig")); } public get defaultMacFrameworkPath(): string[] | undefined { return this.getArrayOfStringsWithUndefinedDefault("default.macFrameworkPath"); } public get defaultWindowsSdkVersion(): string | undefined { return changeBlankStringToUndefined(this.getAsStringOrUndefined("default.windowsSdkVersion")); } - public get defaultCompileCommands(): string | undefined { return changeBlankStringToUndefined(this.getAsStringOrUndefined("default.compileCommands")); } + public get defaultCompileCommands(): string[] | undefined { + const value: any = super.Section.get("default.compileCommands"); + if (isString(value)) { + return value.length > 0 ? [value] : undefined; + } + if (isArrayOfString(value)) { + const result = value.filter(x => x.length > 0); + return result.length > 0 ? result : undefined; + } + return undefined; + } public get defaultForcedInclude(): string[] | undefined { return this.getArrayOfStringsWithUndefinedDefault("default.forcedInclude"); } public get defaultIntelliSenseMode(): string | undefined { return this.getAsStringOrUndefined("default.intelliSenseMode"); } public get defaultCompilerPath(): string | null { return this.getAsString("default.compilerPath", true); } @@ -652,7 +662,7 @@ export class CppSettings extends Settings { } if (isArrayOfString(value)) { - if (setting.items.enum && !allowUndefinedEnums) { + if (setting.items?.enum !== undefined && !allowUndefinedEnums) { if (!value.every(x => this.isValidEnum(setting.items.enum, x))) { return setting.default; } diff --git a/Extension/src/LanguageServer/settingsPanel.ts b/Extension/src/LanguageServer/settingsPanel.ts index 4522091d02..a13ef8109c 100644 --- a/Extension/src/LanguageServer/settingsPanel.ts +++ b/Extension/src/LanguageServer/settingsPanel.ts @@ -337,7 +337,7 @@ export class SettingsPanel { this.configValues.macFrameworkPath = splitEntries(message.value); break; case elementId.compileCommands: - this.configValues.compileCommands = message.value || undefined; + this.configValues.compileCommands = splitEntries(message.value); break; case elementId.dotConfig: this.configValues.dotConfig = message.value || undefined; diff --git a/Extension/src/LanguageServer/settingsTracker.ts b/Extension/src/LanguageServer/settingsTracker.ts index 9cad273f50..fef9ae2fea 100644 --- a/Extension/src/LanguageServer/settingsTracker.ts +++ b/Extension/src/LanguageServer/settingsTracker.ts @@ -108,6 +108,11 @@ export class SettingsTracker { return ""; } return val; + } else if (curSetting["oneOf"]) { + // Currently only C_Cpp.default.compileCommands uses this case. + if (curSetting["oneOf"].some((x: any) => this.typeMatch(val, x.type))) { + return val; + } } else if (val === curSetting["default"]) { // C_Cpp.default.browse.path is a special case where the default value is null and doesn't match the type definition. return val; @@ -206,7 +211,7 @@ export class SettingsTracker { if (value && value.length > maxSettingLengthForTelemetry) { value = value.substring(0, maxSettingLengthForTelemetry) + "..."; } - return {key: key, value: value}; + return { key: key, value: value }; } return undefined; } diff --git a/Extension/ui/settings.html b/Extension/ui/settings.html index 01ea0d9e89..8abe8ef48c 100644 --- a/Extension/ui/settings.html +++ b/Extension/ui/settings.html @@ -672,11 +672,12 @@
Compile commands
- The full path to the compile_commands.json file for the workspace. The include paths and defines discovered in this file will be used instead of the values set for includePath and defines settings. If the compile commands database does not contain an entry for the translation unit that corresponds to the file you opened in the editor, then a warning message will appear and the extension will use the includePath and defines settings instead. + A list of paths to compile_commands.json files for the workspace. The include paths and defines discovered in these files will be used instead of the values set for includePath and defines settings. If the compile commands database does not contain an entry for the translation unit that corresponds to the file you opened in the editor, then a warning message will appear and the extension will use the includePath and defines settings instead.
- -
+
One compile commands path per line.
+ +
diff --git a/Extension/ui/settings.ts b/Extension/ui/settings.ts index e70516b27a..fc75f4cbdf 100644 --- a/Extension/ui/settings.ts +++ b/Extension/ui/settings.ts @@ -295,7 +295,7 @@ class SettingsApp { // Advanced settings (document.getElementById(elementId.windowsSdkVersion)).value = config.windowsSdkVersion ? config.windowsSdkVersion : ""; (document.getElementById(elementId.macFrameworkPath)).value = joinEntries(config.macFrameworkPath); - (document.getElementById(elementId.compileCommands)).value = config.compileCommands ? config.compileCommands : ""; + (document.getElementById(elementId.compileCommands)).value = joinEntries(config.compileCommands); (document.getElementById(elementId.mergeConfigurations)).checked = config.mergeConfigurations; (document.getElementById(elementId.configurationProvider)).value = config.configurationProvider ? config.configurationProvider : ""; (document.getElementById(elementId.forcedInclude)).value = joinEntries(config.forcedInclude);