diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 0000000..3e29176 --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,7 @@ +{ + "recommendations": [ + "dbaeumer.vscode-eslint", + "esbenp.prettier-vscode", + "redhat.vscode-yaml" + ] +} diff --git a/.vscode/launch.json b/.vscode/launch.json index 4f765fa..f2c34b8 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -1,55 +1,51 @@ // A launch configuration that compiles the extension and then opens it inside a new window { - "version": "0.1.0", - "configurations": [ - { - "name": "Launch Extension", - "type": "extensionHost", - "request": "launch", - "runtimeExecutable": "${execPath}", - "args": [ - "--extensionDevelopmentPath=${workspaceFolder}", - ], - "sourceMaps": true, - "outFiles": [ - "${workspaceFolder}/**/*.js" - ], - "env": { - "MAKEFILE_TOOLS_TESTING": "1", - "WindowsSDKVersion": "12.3.45678.9\\", - "CMT_DEVRUN": "1" - }, - "preLaunchTask": "build-with-webpack-watch", - }, - { - "name": "Launch Tests", - "type": "extensionHost", - "request": "launch", - "runtimeExecutable": "${execPath}", - "args": [ - "${workspaceFolder}/src/test/fakeSuite/Repros", - "--disable-workspace-trust", - "--disable-extensions", - "--extensionDevelopmentPath=${workspaceFolder}", - "--extensionTestsPath=${workspaceFolder}/out/src/test/fakeSuite/index" - ], - "sourceMaps": true, - "outFiles": [ - "${workspaceFolder}/out/*", - "${workspaceFolder}/out/src/*", - "${workspaceFolder}/out/src/test/**/*" - ], - "env": { - "MAKEFILE_TOOLS_TESTING": "1", - "WindowsSDKVersion": "12.3.45678.9\\" - }, - "preLaunchTask": "Pretest", - }, - { - "name": "Node Attach", - "type": "node", - "request": "attach", - "port": 5858 - } - ] -} \ No newline at end of file + "version": "0.1.0", + "configurations": [ + { + "name": "Launch Extension", + "type": "extensionHost", + "request": "launch", + "runtimeExecutable": "${execPath}", + "args": ["--extensionDevelopmentPath=${workspaceFolder}"], + "sourceMaps": true, + "outFiles": ["${workspaceFolder}/**/*.js"], + "env": { + "MAKEFILE_TOOLS_TESTING": "1", + "WindowsSDKVersion": "12.3.45678.9\\", + "CMT_DEVRUN": "1" + }, + "preLaunchTask": "build-with-webpack-watch" + }, + { + "name": "Launch Tests", + "type": "extensionHost", + "request": "launch", + "runtimeExecutable": "${execPath}", + "args": [ + "${workspaceFolder}/src/test/fakeSuite/Repros", + "--disable-workspace-trust", + "--disable-extensions", + "--extensionDevelopmentPath=${workspaceFolder}", + "--extensionTestsPath=${workspaceFolder}/out/src/test/fakeSuite/index" + ], + "sourceMaps": true, + "outFiles": [ + "${workspaceFolder}/out/*", + "${workspaceFolder}/out/src/*", + "${workspaceFolder}/out/src/test/**/*" + ], + "env": { + "MAKEFILE_TOOLS_TESTING": "1", + "WindowsSDKVersion": "12.3.45678.9\\" + }, + "preLaunchTask": "Pretest" + }, + { + "name": "Node Attach", + "type": "node", + "request": "attach", + "port": 5858 + } + ] +} diff --git a/.vscode/settings.json b/.vscode/settings.json index be67315..137e001 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,12 +1,37 @@ // Place your settings in this file to overwrite default and user settings. { - "files.exclude": { - "out": false // set this to true to hide the "out" folder with the compiled JS files - }, - "search.exclude": { - "out": true // set this to false to include "out" folder in search results - }, - "typescript.tsdk": "./node_modules/typescript/lib", - "editor.detectIndentation": false, - "cmake.configureOnOpen": false // we want to use the TS server from our node_modules folder to control its version -} \ No newline at end of file + "files.exclude": { + "out": false // set this to true to hide the "out" folder with the compiled JS files + }, + "search.exclude": { + "out": true // set this to false to include "out" folder in search results + }, + "typescript.tsdk": "./node_modules/typescript/lib", + "editor.detectIndentation": false, + "cmake.configureOnOpen": false, // we want to use the TS server from our node_modules folder to control its version + "[typescript]": { + "editor.defaultFormatter": "esbenp.prettier-vscode", + "editor.formatOnSave": true, + "editor.tabSize": 2 + }, + "[javascript]": { + "editor.defaultFormatter": "esbenp.prettier-vscode", + "editor.formatOnSave": true, + "editor.tabSize": 2 + }, + "[yaml]": { + "editor.defaultFormatter": "esbenp.prettier-vscode", + "editor.formatOnSave": true, + "editor.tabSize": 2 + }, + "[json]": { + "editor.defaultFormatter": "esbenp.prettier-vscode", + "editor.formatOnSave": true, + "editor.tabSize": 2 + }, + "[jsonc]": { + "editor.defaultFormatter": "esbenp.prettier-vscode", + "editor.formatOnSave": true, + "editor.tabSize": 2 + } +} diff --git a/.vscode/tasks.json b/.vscode/tasks.json index 6088953..9da7003 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -1,66 +1,58 @@ { - "version": "2.0.0", - "tasks": [ + "version": "2.0.0", + "tasks": [ + { + "label": "build-with-webpack-watch", + "type": "npm", + "script": "compile", + "problemMatcher": [ { - "label": "build-with-webpack-watch", - "type": "npm", - "script": "compile", - "problemMatcher": [ - { - "owner": "typescript", - "source": "ts", - "applyTo": "closedDocuments", - "fileLocation": "absolute", - "severity": "error", - "pattern": [ - { - "regexp": "\\[tsl\\] ERROR in (.*)?\\((\\d+),(\\d+)\\)", - "file": 1, - "line": 2, - "column": 3 - }, - { - "regexp": "\\s*TS\\d+:\\s*(.*)", - "message": 1 - } - ], - "background": { - "activeOnStart": true, - "beginsPattern": { - "regexp": "asset" - }, - "endsPattern": { - "regexp": "webpack (.*?) compiled (.*?) ms" - } - } - } - ], - "isBackground": true - }, - { - "label": "Pretest", - "group": "build", - "isBackground": false, - "type": "shell", - "command": "yarn", - "args": [ - "run", - "pretest" - ], - "dependsOn": [ - "Compile" - ] - }, - { - "label": "Compile", - "group": "build", - "isBackground": false, - "type": "shell", - "command": "yarn", - "args": [ - "run", - "compile" - ] + "owner": "typescript", + "source": "ts", + "applyTo": "closedDocuments", + "fileLocation": "absolute", + "severity": "error", + "pattern": [ + { + "regexp": "\\[tsl\\] ERROR in (.*)?\\((\\d+),(\\d+)\\)", + "file": 1, + "line": 2, + "column": 3 + }, + { + "regexp": "\\s*TS\\d+:\\s*(.*)", + "message": 1 + } + ], + "background": { + "activeOnStart": true, + "beginsPattern": { + "regexp": "asset" + }, + "endsPattern": { + "regexp": "webpack (.*?) compiled (.*?) ms" + } + } } - ] -} \ No newline at end of file + ], + "isBackground": true + }, + { + "label": "Pretest", + "group": "build", + "isBackground": false, + "type": "shell", + "command": "yarn", + "args": ["run", "pretest"], + "dependsOn": ["Compile"] + }, + { + "label": "Compile", + "group": "build", + "isBackground": false, + "type": "shell", + "command": "yarn", + "args": ["run", "compile"] + } + ] +} diff --git a/gulpfile.js b/gulpfile.js index d119bda..04d7129 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -3,149 +3,158 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -'use strict'; - -const gulp = require('gulp'); -const eslint = require('gulp-eslint'); -const fs = require('fs'); -const nls = require('vscode-nls-dev'); -const path = require('path'); -const minimist = require('minimist'); -const es = require('event-stream'); -const sourcemaps = require('gulp-sourcemaps'); -const ts = require('gulp-typescript'); -const typescript = require('typescript'); -const tsProject = ts.createProject('./tsconfig.json', { typescript }); -const filter = require('gulp-filter'); -const vinyl = require('vinyl'); -const jsonc = require('jsonc-parser'); +"use strict"; +const gulp = require("gulp"); +const eslint = require("gulp-eslint"); +const fs = require("fs"); +const nls = require("vscode-nls-dev"); +const path = require("path"); +const minimist = require("minimist"); +const es = require("event-stream"); +const sourcemaps = require("gulp-sourcemaps"); +const ts = require("gulp-typescript"); +const typescript = require("typescript"); +const tsProject = ts.createProject("./tsconfig.json", { typescript }); +const filter = require("gulp-filter"); +const vinyl = require("vinyl"); +const jsonc = require("jsonc-parser"); // Patterns to find schema files -const jsonSchemaFilesPatterns = [ - "*/*-schema.json" -]; +const jsonSchemaFilesPatterns = ["*/*-schema.json"]; const languages = [ - { id: "zh-TW", folderName: "cht", transifexId: "zh-hant" }, - { id: "zh-CN", folderName: "chs", transifexId: "zh-hans" }, - { id: "fr", folderName: "fra" }, - { id: "de", folderName: "deu" }, - { id: "it", folderName: "ita" }, - { id: "es", folderName: "esn" }, - { id: "ja", folderName: "jpn" }, - { id: "ko", folderName: "kor" }, - { id: "ru", folderName: "rus" }, - //{ id: "bg", folderName: "bul" }, // VS Code supports Bulgarian, but loc team is not currently generating it - //{ id: "hu", folderName: "hun" }, // VS Code supports Hungarian, but loc team is not currently generating it - { id: "pt-br", folderName: "ptb", transifexId: "pt-BR" }, - { id: "tr", folderName: "trk" }, - { id: "cs", folderName: "csy" }, - { id: "pl", folderName: "plk" } + { id: "zh-TW", folderName: "cht", transifexId: "zh-hant" }, + { id: "zh-CN", folderName: "chs", transifexId: "zh-hans" }, + { id: "fr", folderName: "fra" }, + { id: "de", folderName: "deu" }, + { id: "it", folderName: "ita" }, + { id: "es", folderName: "esn" }, + { id: "ja", folderName: "jpn" }, + { id: "ko", folderName: "kor" }, + { id: "ru", folderName: "rus" }, + //{ id: "bg", folderName: "bul" }, // VS Code supports Bulgarian, but loc team is not currently generating it + //{ id: "hu", folderName: "hun" }, // VS Code supports Hungarian, but loc team is not currently generating it + { id: "pt-br", folderName: "ptb", transifexId: "pt-BR" }, + { id: "tr", folderName: "trk" }, + { id: "cs", folderName: "csy" }, + { id: "pl", folderName: "plk" }, ]; - // **************************** // Command: translations-export // The following is used to export and XLF file containing english strings for translations. // The result will be written to: ./vscode-extensions-localization-export/ms-vscode/ // **************************** -const translationProjectName = "vscode-extensions"; -const translationExtensionName = "vscode-makefile-tools"; +const translationProjectName = "vscode-extensions"; +const translationExtensionName = "vscode-makefile-tools"; function removePathPrefix(path, prefix) { - if (!prefix) { - return path; - } - if (!path.startsWith(prefix)) { - return path; - } - if (path === prefix) { - return ""; - } - let ch = prefix.charAt(prefix.length - 1); - if (ch === '/' || ch === '\\') { - return path.substr(prefix.length); - } - ch = path.charAt(prefix.length); - if (ch === '/' || ch === '\\') { - return path.substr(prefix.length + 1); - } + if (!prefix) { return path; + } + if (!path.startsWith(prefix)) { + return path; + } + if (path === prefix) { + return ""; + } + let ch = prefix.charAt(prefix.length - 1); + if (ch === "/" || ch === "\\") { + return path.substr(prefix.length); + } + ch = path.charAt(prefix.length); + if (ch === "/" || ch === "\\") { + return path.substr(prefix.length + 1); + } + return path; } // descriptionCallback(path, value, parent) is invoked for attribtues const traverseJson = (jsonTree, descriptionCallback, prefixPath) => { - for (let fieldName in jsonTree) { - if (jsonTree[fieldName] !== null) { - if (typeof(jsonTree[fieldName]) == "string" && fieldName === "description") { - descriptionCallback(prefixPath, jsonTree[fieldName], jsonTree); - } else if (typeof(jsonTree[fieldName]) == "object") { - let path = prefixPath; - if (path !== "") - path = path + "."; - path = path + fieldName; - traverseJson(jsonTree[fieldName], descriptionCallback, path); - } - } + for (let fieldName in jsonTree) { + if (jsonTree[fieldName] !== null) { + if ( + typeof jsonTree[fieldName] == "string" && + fieldName === "description" + ) { + descriptionCallback(prefixPath, jsonTree[fieldName], jsonTree); + } else if (typeof jsonTree[fieldName] == "object") { + let path = prefixPath; + if (path !== "") path = path + "."; + path = path + fieldName; + traverseJson(jsonTree[fieldName], descriptionCallback, path); + } } + } }; // Traverses schema json files looking for "description" fields to localized. // The path to the "description" field is used to create a localization key. const processJsonSchemaFiles = () => { - return es.through(function (file) { - let jsonTree = JSON.parse(file.contents.toString()); - let localizationJsonContents = {}; - let filePath = removePathPrefix(file.path, file.cwd); - filePath = filePath.replace(/\\/g, '/') - let localizationMetadataContents = { - messages: [], - keys: [], - filePath: filePath - }; - let descriptionCallback = (path, value, parent) => { - let locId = filePath + "." + path; - localizationJsonContents[locId] = value; - localizationMetadataContents.keys.push(locId); - localizationMetadataContents.messages.push(value); - }; - traverseJson(jsonTree, descriptionCallback, ""); - this.queue(new vinyl({ - path: path.join(file.path + '.nls.json'), - contents: Buffer.from(JSON.stringify(localizationJsonContents, null, '\t'), 'utf8') - })); - this.queue(new vinyl({ - path: path.join(file.path + '.nls.metadata.json'), - contents: Buffer.from(JSON.stringify(localizationMetadataContents, null, '\t'), 'utf8') - })); - }); + return es.through(function (file) { + let jsonTree = JSON.parse(file.contents.toString()); + let localizationJsonContents = {}; + let filePath = removePathPrefix(file.path, file.cwd); + filePath = filePath.replace(/\\/g, "/"); + let localizationMetadataContents = { + messages: [], + keys: [], + filePath: filePath, + }; + let descriptionCallback = (path, value, parent) => { + let locId = filePath + "." + path; + localizationJsonContents[locId] = value; + localizationMetadataContents.keys.push(locId); + localizationMetadataContents.messages.push(value); + }; + traverseJson(jsonTree, descriptionCallback, ""); + this.queue( + new vinyl({ + path: path.join(file.path + ".nls.json"), + contents: Buffer.from( + JSON.stringify(localizationJsonContents, null, "\t"), + "utf8" + ), + }) + ); + this.queue( + new vinyl({ + path: path.join(file.path + ".nls.metadata.json"), + contents: Buffer.from( + JSON.stringify(localizationMetadataContents, null, "\t"), + "utf8" + ), + }) + ); + }); }; gulp.task("translations-export", (done) => { + // Transpile the TS to JS, and let vscode-nls-dev scan the files for calls to localize. + let jsStream = tsProject + .src() + .pipe(sourcemaps.init()) + .pipe(tsProject()) + .js.pipe(nls.createMetaDataFiles()); - // Transpile the TS to JS, and let vscode-nls-dev scan the files for calls to localize. - let jsStream = tsProject.src() - .pipe(sourcemaps.init()) - .pipe(tsProject()).js - .pipe(nls.createMetaDataFiles()); + // Scan schema files + let jsonSchemaStream = gulp + .src(jsonSchemaFilesPatterns) + .pipe(processJsonSchemaFiles()); - // Scan schema files - let jsonSchemaStream = gulp.src(jsonSchemaFilesPatterns) - .pipe(processJsonSchemaFiles()); - - // Merge files from all source streams - es.merge(jsStream, jsonSchemaStream) + // Merge files from all source streams + es.merge(jsStream, jsonSchemaStream) // Filter down to only the files we need - .pipe(filter(['**/*.nls.json', '**/*.nls.metadata.json'])) + .pipe(filter(["**/*.nls.json", "**/*.nls.metadata.json"])) // Consoldate them into nls.metadata.json, which the xlf is built from. - .pipe(nls.bundleMetaDataFiles('ms-vscode.makefile-tools', '.')) + .pipe(nls.bundleMetaDataFiles("ms-vscode.makefile-tools", ".")) // filter down to just the resulting metadata files - .pipe(filter(['**/nls.metadata.header.json', '**/nls.metadata.json'])) + .pipe(filter(["**/nls.metadata.header.json", "**/nls.metadata.json"])) // Add package.nls.json, used to localized package.json .pipe(gulp.src(["package.nls.json"])) @@ -154,12 +163,13 @@ gulp.task("translations-export", (done) => { // Does not re-queue any files to the stream. Outputs only the XLF file .pipe(nls.createXlfFiles(translationProjectName, translationExtensionName)) .pipe(gulp.dest(path.join(`${translationProjectName}-localization-export`))) - .pipe(es.wait(() => { + .pipe( + es.wait(() => { done(); - })); + }) + ); }); - // **************************** // Command: translations-import // The following is used to import an XLF file containing all language strings. @@ -168,24 +178,34 @@ gulp.task("translations-export", (done) => { // Imports translations from raw localized MLCP strings to VS Code .i18n.json files gulp.task("translations-import", (done) => { - let options = minimist(process.argv.slice(2), { - string: "location", - default: { - location: "./vscode-translations-import" - } - }); - es.merge(languages.map((language) => { - let id = language.transifexId || language.id; - return gulp.src(path.join(options.location, id, translationProjectName, `${translationExtensionName}.xlf`)) - .pipe(nls.prepareJsonFiles()) - .pipe(gulp.dest(path.join("./i18n", language.folderName))); - })) - .pipe(es.wait(() => { - done(); - })); + let options = minimist(process.argv.slice(2), { + string: "location", + default: { + location: "./vscode-translations-import", + }, + }); + es.merge( + languages.map((language) => { + let id = language.transifexId || language.id; + return gulp + .src( + path.join( + options.location, + id, + translationProjectName, + `${translationExtensionName}.xlf` + ) + ) + .pipe(nls.prepareJsonFiles()) + .pipe(gulp.dest(path.join("./i18n", language.folderName))); + }) + ).pipe( + es.wait(() => { + done(); + }) + ); }); - // **************************** // Command: translations-generate // The following is used to import an i18n directory structure and generate files used at runtime. @@ -194,122 +214,156 @@ gulp.task("translations-import", (done) => { // Generate package.nls.*.json files from: ./i18n/*/package.i18n.json // Outputs to root path, as these nls files need to be along side package.json const generatedAdditionalLocFiles = () => { - return gulp.src(['package.nls.json']) - .pipe(nls.createAdditionalLanguageFiles(languages, 'i18n')) - .pipe(gulp.dest('.')); + return gulp + .src(["package.nls.json"]) + .pipe(nls.createAdditionalLanguageFiles(languages, "i18n")) + .pipe(gulp.dest(".")); }; // Generates ./dist/nls.bundle..json from files in ./i18n/** *///.i18n.json // Localized strings are read from these files at runtime. const generatedSrcLocBundle = () => { - // Transpile the TS to JS, and let vscode-nls-dev scan the files for calls to localize. - return tsProject.src() - .pipe(sourcemaps.init()) - .pipe(tsProject()).js - .pipe(nls.rewriteLocalizeCalls()) - .pipe(nls.createAdditionalLanguageFiles(languages, "i18n")) - .pipe(nls.bundleMetaDataFiles('ms-vscode.makefile-tools', 'dist')) - .pipe(nls.bundleLanguageFiles()) - .pipe(filter(['**/nls.bundle.*.json', '**/nls.metadata.header.json', '**/nls.metadata.json'])) - .pipe(gulp.dest('dist')); + // Transpile the TS to JS, and let vscode-nls-dev scan the files for calls to localize. + return tsProject + .src() + .pipe(sourcemaps.init()) + .pipe(tsProject()) + .js.pipe(nls.rewriteLocalizeCalls()) + .pipe(nls.createAdditionalLanguageFiles(languages, "i18n")) + .pipe(nls.bundleMetaDataFiles("ms-vscode.makefile-tools", "dist")) + .pipe(nls.bundleLanguageFiles()) + .pipe( + filter([ + "**/nls.bundle.*.json", + "**/nls.metadata.header.json", + "**/nls.metadata.json", + ]) + ) + .pipe(gulp.dest("dist")); }; const generateLocalizedJsonSchemaFiles = () => { - return es.through(function (file) { - let jsonTree = JSON.parse(file.contents.toString()); - languages.map((language) => { - let stringTable = {}; - // Try to open i18n file for this file - let relativePath = removePathPrefix(file.path, file.cwd); - let locFile = path.join("./i18n", language.folderName, relativePath + ".i18n.json"); - if (fs.existsSync(locFile)) { - stringTable = jsonc.parse(fs.readFileSync(locFile).toString()); - } - // Entire file is scanned and modified, then serialized for that language. - // Even if no translations are available, we still write new files to dist/schema/... - let keyPrefix = relativePath + "."; - keyPrefix = keyPrefix.replace(/\\/g, "/"); - let descriptionCallback = (path, value, parent) => { - if (stringTable[keyPrefix + path]) { - parent.description = stringTable[keyPrefix + path]; - } - }; - traverseJson(jsonTree, descriptionCallback, ""); - let newContent = JSON.stringify(jsonTree, null, '\t'); - this.queue(new vinyl({ - path: path.join("schema", language.id, relativePath), - contents: Buffer.from(newContent, 'utf8') - })); - }); + return es.through(function (file) { + let jsonTree = JSON.parse(file.contents.toString()); + languages.map((language) => { + let stringTable = {}; + // Try to open i18n file for this file + let relativePath = removePathPrefix(file.path, file.cwd); + let locFile = path.join( + "./i18n", + language.folderName, + relativePath + ".i18n.json" + ); + if (fs.existsSync(locFile)) { + stringTable = jsonc.parse(fs.readFileSync(locFile).toString()); + } + // Entire file is scanned and modified, then serialized for that language. + // Even if no translations are available, we still write new files to dist/schema/... + let keyPrefix = relativePath + "."; + keyPrefix = keyPrefix.replace(/\\/g, "/"); + let descriptionCallback = (path, value, parent) => { + if (stringTable[keyPrefix + path]) { + parent.description = stringTable[keyPrefix + path]; + } + }; + traverseJson(jsonTree, descriptionCallback, ""); + let newContent = JSON.stringify(jsonTree, null, "\t"); + this.queue( + new vinyl({ + path: path.join("schema", language.id, relativePath), + contents: Buffer.from(newContent, "utf8"), + }) + ); }); + }); }; // Generate localized versions of JSON schema files // Check for cooresponding localized json file in i18n // Generate new version of the JSON schema file in dist/schema// const generateJsonSchemaLoc = () => { - return gulp.src(jsonSchemaFilesPatterns) - .pipe(generateLocalizedJsonSchemaFiles()) - .pipe(gulp.dest('dist')); + return gulp + .src(jsonSchemaFilesPatterns) + .pipe(generateLocalizedJsonSchemaFiles()) + .pipe(gulp.dest("dist")); }; -const bundleScriptsPatterns = [ - "assets/*" -]; +const bundleScriptsPatterns = ["assets/*"]; const bundleScripts = () => { - return gulp.src(bundleScriptsPatterns).pipe(gulp.dest("assets")); -} + return gulp.src(bundleScriptsPatterns).pipe(gulp.dest("assets")); +}; -gulp.task('bundle-assets', bundleScripts); -gulp.task('translations-generate', gulp.series(generatedSrcLocBundle, generatedAdditionalLocFiles, generateJsonSchemaLoc)); +gulp.task("bundle-assets", bundleScripts); +gulp.task( + "translations-generate", + gulp.series( + generatedSrcLocBundle, + generatedAdditionalLocFiles, + generateJsonSchemaLoc + ) +); const allTypeScript = [ - 'src/**/*.ts', - 'test/**/*.ts', - '!**/*.d.ts', - '!**/typings**' + "src/**/*.ts", + "test/**/*.ts", + "!**/*.d.ts", + "!**/typings**", ]; // Prints file path and line number in the same line. Easier to ctrl + left click in VS Code. -const lintReporter = results => { +const lintReporter = (results) => { const messages = []; let errorCount = 0; let warningCount = 0; let fixableErrorCount = 0; let fixableWarningCount = 0; - results.forEach(result => { + results.forEach((result) => { if (result.errorCount || result.warningCount) { - const filePath = result.filePath.replace(/\\/g, '/'); - errorCount += result.errorCount; - warningCount += result.warningCount; - fixableErrorCount += result.fixableErrorCount; - fixableWarningCount += result.fixableWarningCount; + const filePath = result.filePath.replace(/\\/g, "/"); + errorCount += result.errorCount; + warningCount += result.warningCount; + fixableErrorCount += result.fixableErrorCount; + fixableWarningCount += result.fixableWarningCount; - result.messages.forEach(message => { - messages.push(`[lint] ${filePath}:${message.line}:${message.column}: ${message.message} [${message.ruleId}]`); - }); + result.messages.forEach((message) => { + messages.push( + `[lint] ${filePath}:${message.line}:${message.column}: ${message.message} [${message.ruleId}]` + ); + }); - messages.push(''); + messages.push(""); } }); if (errorCount || warningCount) { - messages.push('\x1b[31m' + ` ${errorCount + warningCount} Problems (${errorCount} Errors, ${warningCount} Warnings)` + '\x1b[39m'); - messages.push('\x1b[31m' + ` ${fixableErrorCount} Errors, ${fixableWarningCount} Warnings potentially fixable with \`--fix\` option.` + '\x1b[39m'); - messages.push('', ''); + messages.push( + "\x1b[31m" + + ` ${ + errorCount + warningCount + } Problems (${errorCount} Errors, ${warningCount} Warnings)` + + "\x1b[39m" + ); + messages.push( + "\x1b[31m" + + ` ${fixableErrorCount} Errors, ${fixableWarningCount} Warnings potentially fixable with \`--fix\` option.` + + "\x1b[39m" + ); + messages.push("", ""); } - return messages.join('\n'); + return messages.join("\n"); }; -gulp.task('lint', function () { - // Un-comment these parts for applying auto-fix. - return gulp.src(allTypeScript) - .pipe(eslint({ configFile: ".eslintrc.js" /*, fix: true */})) - .pipe(eslint.format(lintReporter, process.stderr)) - //.pipe(gulp.dest(file => file.base)) - .pipe(eslint.failAfterError()); +gulp.task("lint", function () { + // Un-comment these parts for applying auto-fix. + return ( + gulp + .src(allTypeScript) + .pipe(eslint({ configFile: ".eslintrc.js" /*, fix: true */ })) + .pipe(eslint.format(lintReporter, process.stderr)) + //.pipe(gulp.dest(file => file.base)) + .pipe(eslint.failAfterError()) + ); }); - diff --git a/jobs/cg.yml b/jobs/cg.yml index ef06ab4..1c79860 100644 --- a/jobs/cg.yml +++ b/jobs/cg.yml @@ -4,27 +4,27 @@ trigger: branches: include: - - refs/heads/main + - refs/heads/main resources: repositories: - - repository: self - type: git - ref: refs/heads/main - - repository: MicroBuildTemplate - type: git - name: 1ESPipelineTemplates/MicroBuildTemplate - ref: refs/tags/release + - repository: self + type: git + ref: refs/heads/main + - repository: MicroBuildTemplate + type: git + name: 1ESPipelineTemplates/MicroBuildTemplate + ref: refs/tags/release name: $(Date:yyyyMMdd).$(Rev:r) variables: -- name: Codeql.Enabled - value: true -- name: Codeql.TSAEnabled - value: true -- name: TeamName - value: C++ Cross Platform and Cloud + - name: Codeql.Enabled + value: true + - name: Codeql.TSAEnabled + value: true + - name: TeamName + value: C++ Cross Platform and Cloud extends: template: azure-pipelines/MicroBuild.1ES.Official.yml@MicroBuildTemplate @@ -42,51 +42,51 @@ extends: ignoreDirectories: node_modules,dist,i18n alertWarningLevel: Medium customBuildTags: - - ES365AIMigrationTooling + - ES365AIMigrationTooling stages: - - stage: stage - jobs: - - job: Job_1 - displayName: Agent job 1 - steps: - - checkout: self - fetchTags: false - - - task: NodeTool@0 - displayName: Use Node 16.x - inputs: - versionSpec: 16.x - - - task: geeklearningio.gl-vsts-tasks-yarn.yarn-installer-task.YarnInstaller@3 - displayName: Use Yarn 1.x - - - task: geeklearningio.gl-vsts-tasks-yarn.yarn-task.Yarn@3 - displayName: Yarn install - inputs: - arguments: install - - - task: Npm@0 - displayName: npm install vsce - inputs: - arguments: -g vsce - - - task: CmdLine@2 - displayName: Run VSCE to package vsix - inputs: - script: |- - echo Building VSIX - vsce package --yarn -o $(Build.StagingDirectory)\makefile-tools.vsix - - - task: Npm@0 - displayName: npm uninstall vsce - inputs: - command: uninstall - arguments: -g vsce - - - task: DeleteFiles@1 - displayName: Remove code that should not be scanned - inputs: - Contents: |- - node_modules - dist - i18n \ No newline at end of file + - stage: stage + jobs: + - job: Job_1 + displayName: Agent job 1 + steps: + - checkout: self + fetchTags: false + + - task: NodeTool@0 + displayName: Use Node 16.x + inputs: + versionSpec: 16.x + + - task: geeklearningio.gl-vsts-tasks-yarn.yarn-installer-task.YarnInstaller@3 + displayName: Use Yarn 1.x + + - task: geeklearningio.gl-vsts-tasks-yarn.yarn-task.Yarn@3 + displayName: Yarn install + inputs: + arguments: install + + - task: Npm@0 + displayName: npm install vsce + inputs: + arguments: -g vsce + + - task: CmdLine@2 + displayName: Run VSCE to package vsix + inputs: + script: |- + echo Building VSIX + vsce package --yarn -o $(Build.StagingDirectory)\makefile-tools.vsix + + - task: Npm@0 + displayName: npm uninstall vsce + inputs: + command: uninstall + arguments: -g vsce + + - task: DeleteFiles@1 + displayName: Remove code that should not be scanned + inputs: + Contents: |- + node_modules + dist + i18n diff --git a/jobs/loc/LocProject.json b/jobs/loc/LocProject.json index 4f663dd..ba2f866 100644 --- a/jobs/loc/LocProject.json +++ b/jobs/loc/LocProject.json @@ -1,14 +1,14 @@ { - "Projects": [ + "Projects": [ + { + "LanguangeSet": "VS_Main_Languages", + "LocItems": [ { - "LanguangeSet": "VS_Main_Languages", - "LocItems": [ - { - "SourceFile": "vscode-extensions-localization-export\\vscode-extensions\\vscode-makefile-tools.xlf", - "Languages": "cs;de;es;fr;it;ja;ko;pl;pt-BR;ru;tr;zh-Hans;zh-Hant", - "LclFile": "jobs\\loc\\LCL\\{Lang}\\vscode-makefile-tools.xlf.lcl" - } - ] + "SourceFile": "vscode-extensions-localization-export\\vscode-extensions\\vscode-makefile-tools.xlf", + "Languages": "cs;de;es;fr;it;ja;ko;pl;pt-BR;ru;tr;zh-Hans;zh-Hant", + "LclFile": "jobs\\loc\\LCL\\{Lang}\\vscode-makefile-tools.xlf.lcl" } - ] -} \ No newline at end of file + ] + } + ] +} diff --git a/jobs/loc/TranslationsImportExport.yml b/jobs/loc/TranslationsImportExport.yml index 882fd9c..0a4b1c2 100644 --- a/jobs/loc/TranslationsImportExport.yml +++ b/jobs/loc/TranslationsImportExport.yml @@ -4,26 +4,26 @@ # ================================================================================== resources: repositories: - - repository: self - clean: true - - repository: MicroBuildTemplate - type: git - name: 1ESPipelineTemplates/MicroBuildTemplate - ref: refs/tags/release + - repository: self + clean: true + - repository: MicroBuildTemplate + type: git + name: 1ESPipelineTemplates/MicroBuildTemplate + ref: refs/tags/release trigger: none pr: none schedules: -- cron: "0 7 * * *" - displayName: Daily 7 AM - branches: - include: - - main - always: true + - cron: "0 7 * * *" + displayName: Daily 7 AM + branches: + include: + - main + always: true variables: -- name: TeamName - value: C++ Cross Platform and Cloud + - name: TeamName + value: C++ Cross Platform and Cloud extends: template: azure-pipelines/MicroBuild.1ES.Official.yml@MicroBuildTemplate @@ -33,50 +33,50 @@ extends: image: AzurePipelinesWindows2022compliantGPT os: windows sdl: - sourceAnalysisPool: + sourceAnalysisPool: name: AzurePipelines-EO image: AzurePipelinesWindows2022compliantGPT os: windows customBuildTags: - - ES365AIMigrationTooling + - ES365AIMigrationTooling stages: - - stage: stage - jobs: - - job: job - templateContext: - outputs: - - output: pipelineArtifact - targetPath: '$(Build.ArtifactStagingDirectory)' - artifactName: 'drop' - publishLocation: 'Container' - steps: - - task: NodeTool@0 - inputs: - versionSpec: '16.x' - displayName: 'Install Node.js' - - - task: CmdLine@2 - inputs: - script: 'yarn install' - - - task: CmdLine@2 - inputs: - script: 'yarn run translations-export' - - - task: OneLocBuild@2 - env: - SYSTEM_ACCESSTOKEN: $(System.AccessToken) - inputs: - locProj: 'jobs/loc/LocProject.json' - outDir: '$(Build.ArtifactStagingDirectory)' - isCreatePrSelected: false - prSourceBranchPrefix: 'locfiles' - packageSourceAuth: 'patAuth' - patVariable: '$(OneLocBuildPat)' - LclSource: lclFilesfromPackage - LclPackageId: 'LCL-JUNO-PROD-VMAKEFILE' - lsBuildXLocPackageVersion: '7.0.30510' - - - task: CmdLine@2 - inputs: - script: 'node ./translations_auto_pr.js microsoft vscode-makefile-tools csigs $(csigsPat) csigs csigs@users.noreply.github.com "$(Build.ArtifactStagingDirectory)/loc" vscode-extensions-localization-export/vscode-extensions' \ No newline at end of file + - stage: stage + jobs: + - job: job + templateContext: + outputs: + - output: pipelineArtifact + targetPath: "$(Build.ArtifactStagingDirectory)" + artifactName: "drop" + publishLocation: "Container" + steps: + - task: NodeTool@0 + inputs: + versionSpec: "16.x" + displayName: "Install Node.js" + + - task: CmdLine@2 + inputs: + script: "yarn install" + + - task: CmdLine@2 + inputs: + script: "yarn run translations-export" + + - task: OneLocBuild@2 + env: + SYSTEM_ACCESSTOKEN: $(System.AccessToken) + inputs: + locProj: "jobs/loc/LocProject.json" + outDir: "$(Build.ArtifactStagingDirectory)" + isCreatePrSelected: false + prSourceBranchPrefix: "locfiles" + packageSourceAuth: "patAuth" + patVariable: "$(OneLocBuildPat)" + LclSource: lclFilesfromPackage + LclPackageId: "LCL-JUNO-PROD-VMAKEFILE" + lsBuildXLocPackageVersion: "7.0.30510" + + - task: CmdLine@2 + inputs: + script: 'node ./translations_auto_pr.js microsoft vscode-makefile-tools csigs $(csigsPat) csigs csigs@users.noreply.github.com "$(Build.ArtifactStagingDirectory)/loc" vscode-extensions-localization-export/vscode-extensions' diff --git a/jobs/prerelease.yml b/jobs/prerelease.yml index 7c39c50..235b886 100644 --- a/jobs/prerelease.yml +++ b/jobs/prerelease.yml @@ -4,32 +4,32 @@ trigger: none schedules: -- cron: 0 4 * * 1,2,3,4,5 - branches: - include: - - refs/heads/main + - cron: 0 4 * * 1,2,3,4,5 + branches: + include: + - refs/heads/main resources: repositories: - - repository: self - type: git - ref: refs/heads/main - - repository: MicroBuildTemplate - type: git - name: 1ESPipelineTemplates/MicroBuildTemplate - ref: refs/tags/release + - repository: self + type: git + ref: refs/heads/main + - repository: MicroBuildTemplate + type: git + name: 1ESPipelineTemplates/MicroBuildTemplate + ref: refs/tags/release name: $(Date:yyyyMMdd).$(Rev:r) variables: -- name: IsPreRelease - value: 1 -- name: ReleaseVersion - value: unset -- name: TeamName - value: C++ Cross Platform and Cloud -- name: SignType - value: real + - name: IsPreRelease + value: 1 + - name: ReleaseVersion + value: unset + - name: TeamName + value: C++ Cross Platform and Cloud + - name: SignType + value: real extends: template: azure-pipelines/MicroBuild.1ES.Official.yml@MicroBuildTemplate @@ -40,22 +40,22 @@ extends: sourceAnalysisPool: name: VSEngSS-MicroBuild2022-1ES customBuildTags: - - ES365AIMigrationTooling + - ES365AIMigrationTooling stages: - - stage: stage - jobs: - - job: Job_1 - displayName: Build pre-release - templateContext: - outputs: - - output: pipelineArtifact - displayName: 'Publish VSIX' - targetPath: $(Build.ArtifactStagingDirectory)/vsix - artifactName: vsix - sbomBuildDropPath: $(Build.ArtifactStagingDirectory) - # No need for SBOM, it's now located in the vsix artifact - steps: - - checkout: self - clean: true - fetchTags: false - - template: /jobs/shared/build.yml@self \ No newline at end of file + - stage: stage + jobs: + - job: Job_1 + displayName: Build pre-release + templateContext: + outputs: + - output: pipelineArtifact + displayName: "Publish VSIX" + targetPath: $(Build.ArtifactStagingDirectory)/vsix + artifactName: vsix + sbomBuildDropPath: $(Build.ArtifactStagingDirectory) + # No need for SBOM, it's now located in the vsix artifact + steps: + - checkout: self + clean: true + fetchTags: false + - template: /jobs/shared/build.yml@self diff --git a/jobs/release.yml b/jobs/release.yml index 0be267a..5f3e767 100644 --- a/jobs/release.yml +++ b/jobs/release.yml @@ -2,32 +2,32 @@ # Licensed under the MIT License. parameters: -- name: SignTypeOverride - displayName: Signing Type Override - type: string - default: default - values: - - default - - test - - real + - name: SignTypeOverride + displayName: Signing Type Override + type: string + default: default + values: + - default + - test + - real trigger: none resources: repositories: - - repository: self - type: git - ref: refs/heads/main - - repository: MicroBuildTemplate - type: git - name: 1ESPipelineTemplates/MicroBuildTemplate - ref: refs/tags/release + - repository: self + type: git + ref: refs/heads/main + - repository: MicroBuildTemplate + type: git + name: 1ESPipelineTemplates/MicroBuildTemplate + ref: refs/tags/release name: $(Date:yyyyMMdd).$(Rev:r) variables: IsPreRelease: 0 -# ReleaseVersion is set in the versions tab so it can be edited. + # ReleaseVersion is set in the versions tab so it can be edited. TeamName: C++ Cross Platform and Cloud # If the user didn't override the signing type, then only real-sign on main. ${{ if ne(parameters.SignTypeOverride, 'default') }}: @@ -46,32 +46,32 @@ extends: sourceAnalysisPool: name: VSEngSS-MicroBuild2022-1ES customBuildTags: - - ES365AIMigrationTooling + - ES365AIMigrationTooling stages: - - stage: stage - jobs: - - job: Job_1 - displayName: Build release - templateContext: - outputs: - - output: pipelineArtifact - displayName: 'Publish VSIX' - targetPath: $(Build.ArtifactStagingDirectory)/vsix - artifactName: vsix - sbomBuildDropPath: $(Build.ArtifactStagingDirectory) - # No need for the SBOM, it's now located in the vsix artifact - steps: - - checkout: self - clean: true - fetchTags: false - - task: CmdLine@2 - displayName: Check ReleaseVersion - inputs: - script: |- - @echo off - if "$(ReleaseVersion)"=="unset" ( - echo ^"ReleaseVersion^" was not set - exit /B 1 - ) - exit /b 0 - - template: /jobs/shared/build.yml@self \ No newline at end of file + - stage: stage + jobs: + - job: Job_1 + displayName: Build release + templateContext: + outputs: + - output: pipelineArtifact + displayName: "Publish VSIX" + targetPath: $(Build.ArtifactStagingDirectory)/vsix + artifactName: vsix + sbomBuildDropPath: $(Build.ArtifactStagingDirectory) + # No need for the SBOM, it's now located in the vsix artifact + steps: + - checkout: self + clean: true + fetchTags: false + - task: CmdLine@2 + displayName: Check ReleaseVersion + inputs: + script: |- + @echo off + if "$(ReleaseVersion)"=="unset" ( + echo ^"ReleaseVersion^" was not set + exit /B 1 + ) + exit /b 0 + - template: /jobs/shared/build.yml@self diff --git a/jobs/shared/build.yml b/jobs/shared/build.yml index 4b1dec6..c6e9b92 100644 --- a/jobs/shared/build.yml +++ b/jobs/shared/build.yml @@ -2,137 +2,137 @@ # Licensed under the MIT License. parameters: -- name: IsPreRelease - type: string - default: $(IsPreRelease) -- name: ReleaseVersion - type: string - default: $(ReleaseVersion) + - name: IsPreRelease + type: string + default: $(IsPreRelease) + - name: ReleaseVersion + type: string + default: $(ReleaseVersion) steps: -- task: MicroBuildSigningPlugin@3 - displayName: Install MicroBuild Signing - inputs: - signType: $(SignType) - zipSources: false + - task: MicroBuildSigningPlugin@3 + displayName: Install MicroBuild Signing + inputs: + signType: $(SignType) + zipSources: false -- task: NodeTool@0 - displayName: Use Node 16.x - inputs: - versionSpec: 16.x + - task: NodeTool@0 + displayName: Use Node 16.x + inputs: + versionSpec: 16.x -- task: geeklearningio.gl-vsts-tasks-yarn.yarn-installer-task.YarnInstaller@3 - displayName: Use Yarn 1.x + - task: geeklearningio.gl-vsts-tasks-yarn.yarn-installer-task.YarnInstaller@3 + displayName: Use Yarn 1.x -- task: CmdLine@2 - displayName: IF EXIST %SYSTEMDRIVE%\Users\%USERNAME%\.npmrc del %SYSTEMDRIVE%\Users\%USERNAME%\.npmrc - inputs: - script: IF EXIST %SYSTEMDRIVE%\Users\%USERNAME%\.npmrc del %SYSTEMDRIVE%\Users\%USERNAME%\.npmrc + - task: CmdLine@2 + displayName: IF EXIST %SYSTEMDRIVE%\Users\%USERNAME%\.npmrc del %SYSTEMDRIVE%\Users\%USERNAME%\.npmrc + inputs: + script: IF EXIST %SYSTEMDRIVE%\Users\%USERNAME%\.npmrc del %SYSTEMDRIVE%\Users\%USERNAME%\.npmrc -- task: Npm@0 - displayName: npm install VSCE - inputs: - arguments: -g vsce + - task: Npm@0 + displayName: npm install VSCE + inputs: + arguments: -g vsce -- task: PowerShell@2 - displayName: Update ReleaseVersion for pre-release builds - condition: eq(variables.IsPreRelease, '1') - inputs: - targetType: inline - script: | - # - # Query the Marketplace for the latest version of Makefile Tools - # https://github.com/microsoft/vscode/blob/main/src/vs/platform/extensionManagement/common/extensionGalleryService.ts - # - $uri = 'https://marketplace.visualstudio.com/_apis/public/gallery/extensionquery' - $contentType = 'application/json' - $headers = @{ - "Accept" = "application/json; api-version=3.0-preview" - } - $data = '{"filters": [{"criteria": [{"filterType": 7, "value": "ms-vscode.makefile-tools"}]}], "flags": 529}' - $response = Invoke-WebRequest -Method 'POST' -Uri $uri -UseBasicParsing -ContentType $contentType -Headers $headers -Body $data - $newVersion = '' - if ($response.StatusCode.Equals(200)) { - $result = $response.Content | ConvertFrom-Json - # Check the $result.results[0].extensions.versions.properties for the 'Microsoft.VisualStudio.Code.PreRelease' key to make sure it's true, - # if not, we need to bump the .Minor version and reset .Build to 0 - $index = $result.results[0].extensions.versions.properties.key.IndexOf("Microsoft.VisualStudio.Code.PreRelease") - $isPreRelease = "false" - if ($index -ge 0) { - $isPreRelease = $result.results[0].extensions.versions.properties.value[$index] - } - $v = [System.Version]::Parse($result.results[0].extensions.versions.version) - if ($isPreRelease.Equals('true')) { - $newVersion = [System.Version]::new($v.Major, $v.Minor, $v.Build + 1).ToString() - } else { - $newVersion = [System.Version]::new($v.Major, $v.Minor + 1, 0).ToString() - } - } - Write-Host "New version is: $newVersion" - Write-Host "##vso[task.setvariable variable=ReleaseVersion]$newVersion" - ignoreLASTEXITCODE: true + - task: PowerShell@2 + displayName: Update ReleaseVersion for pre-release builds + condition: eq(variables.IsPreRelease, '1') + inputs: + targetType: inline + script: | + # + # Query the Marketplace for the latest version of Makefile Tools + # https://github.com/microsoft/vscode/blob/main/src/vs/platform/extensionManagement/common/extensionGalleryService.ts + # + $uri = 'https://marketplace.visualstudio.com/_apis/public/gallery/extensionquery' + $contentType = 'application/json' + $headers = @{ + "Accept" = "application/json; api-version=3.0-preview" + } + $data = '{"filters": [{"criteria": [{"filterType": 7, "value": "ms-vscode.makefile-tools"}]}], "flags": 529}' + $response = Invoke-WebRequest -Method 'POST' -Uri $uri -UseBasicParsing -ContentType $contentType -Headers $headers -Body $data + $newVersion = '' + if ($response.StatusCode.Equals(200)) { + $result = $response.Content | ConvertFrom-Json + # Check the $result.results[0].extensions.versions.properties for the 'Microsoft.VisualStudio.Code.PreRelease' key to make sure it's true, + # if not, we need to bump the .Minor version and reset .Build to 0 + $index = $result.results[0].extensions.versions.properties.key.IndexOf("Microsoft.VisualStudio.Code.PreRelease") + $isPreRelease = "false" + if ($index -ge 0) { + $isPreRelease = $result.results[0].extensions.versions.properties.value[$index] + } + $v = [System.Version]::Parse($result.results[0].extensions.versions.version) + if ($isPreRelease.Equals('true')) { + $newVersion = [System.Version]::new($v.Major, $v.Minor, $v.Build + 1).ToString() + } else { + $newVersion = [System.Version]::new($v.Major, $v.Minor + 1, 0).ToString() + } + } + Write-Host "New version is: $newVersion" + Write-Host "##vso[task.setvariable variable=ReleaseVersion]$newVersion" + ignoreLASTEXITCODE: true -- task: PowerShell@2 - displayName: Set build name - inputs: - targetType: inline - script: |- - $str = Get-Date -Format "yyMMdd-HHmm" - Write-Host "##vso[build.updatebuildnumber]${{parameters.ReleaseVersion}} - $str" + - task: PowerShell@2 + displayName: Set build name + inputs: + targetType: inline + script: |- + $str = Get-Date -Format "yyMMdd-HHmm" + Write-Host "##vso[build.updatebuildnumber]${{parameters.ReleaseVersion}} - $str" -- task: PowerShell@2 - displayName: Update version in package.json - inputs: - targetType: inline - script: |- - $JsonData = Get-Content -Path .\package.json -raw | ConvertFrom-Json - $oldVersion = $JsonData.version - $newVersion = "${{parameters.ReleaseVersion}}" - $JsonData.version = $newVersion - $JsonData | ConvertTo-Json -Depth 20 | Set-Content -Path .\package.json - Write-Host "old version was: $oldVersion" - Write-Host "new version is: $newVersion" - Get-Content -Path .\package.json + - task: PowerShell@2 + displayName: Update version in package.json + inputs: + targetType: inline + script: |- + $JsonData = Get-Content -Path .\package.json -raw | ConvertFrom-Json + $oldVersion = $JsonData.version + $newVersion = "${{parameters.ReleaseVersion}}" + $JsonData.version = $newVersion + $JsonData | ConvertTo-Json -Depth 20 | Set-Content -Path .\package.json + Write-Host "old version was: $oldVersion" + Write-Host "new version is: $newVersion" + Get-Content -Path .\package.json -- task: CmdLine@2 - displayName: Create release.flag/insiders.flag - inputs: - script: | - if "${{parameters.IsPreRelease}}"=="1" (type nul > "insiders.flag") else (type nul > "release.flag") + - task: CmdLine@2 + displayName: Create release.flag/insiders.flag + inputs: + script: | + if "${{parameters.IsPreRelease}}"=="1" (type nul > "insiders.flag") else (type nul > "release.flag") -- template: /jobs/shared/install-nuget.yml@self + - template: /jobs/shared/install-nuget.yml@self -- script: nuget restore $(Build.SourcesDirectory)\build\SignFiles.proj -PackagesDirectory $(Build.SourcesDirectory)\build\packages - displayName: Restore MicroBuild Core + - script: nuget restore $(Build.SourcesDirectory)\build\SignFiles.proj -PackagesDirectory $(Build.SourcesDirectory)\build\packages + displayName: Restore MicroBuild Core -- task: CmdLine@2 - displayName: Build files - inputs: - script: | - npm run vscode:prepublish + - task: CmdLine@2 + displayName: Build files + inputs: + script: | + npm run vscode:prepublish -- task: MSBuild@1 - displayName: Sign files - inputs: - solution: $(Build.SourcesDirectory)\build\SignFiles.proj - msbuildArguments: /p:SignType=$(SignType) + - task: MSBuild@1 + displayName: Sign files + inputs: + solution: $(Build.SourcesDirectory)\build\SignFiles.proj + msbuildArguments: /p:SignType=$(SignType) -- task: CmdLine@2 - displayName: vsce package - inputs: - script: | - mkdir $(Build.ArtifactStagingDirectory)\vsix - if "${{parameters.IsPreRelease}}"=="1" (vsce package --yarn -o $(Build.ArtifactStagingDirectory)\vsix\makefile-tools.vsix --pre-release) else (vsce package --yarn -o $(Build.ArtifactStagingDirectory)\vsix\makefile-tools.vsix) + - task: CmdLine@2 + displayName: vsce package + inputs: + script: | + mkdir $(Build.ArtifactStagingDirectory)\vsix + if "${{parameters.IsPreRelease}}"=="1" (vsce package --yarn -o $(Build.ArtifactStagingDirectory)\vsix\makefile-tools.vsix --pre-release) else (vsce package --yarn -o $(Build.ArtifactStagingDirectory)\vsix\makefile-tools.vsix) -- task: MSBuild@1 - displayName: Sign VSIX - inputs: - solution: $(Build.SourcesDirectory)\build\SignVsix.proj - msbuildArguments: /p:SignType=$(SignType) + - task: MSBuild@1 + displayName: Sign VSIX + inputs: + solution: $(Build.SourcesDirectory)\build\SignVsix.proj + msbuildArguments: /p:SignType=$(SignType) -- task: CmdLine@2 - displayName: Write the version to version.txt - inputs: - script: | - echo ${{parameters.ReleaseVersion}} >> version.txt - move version.txt $(Build.ArtifactStagingDirectory)\vsix\version.txt \ No newline at end of file + - task: CmdLine@2 + displayName: Write the version to version.txt + inputs: + script: | + echo ${{parameters.ReleaseVersion}} >> version.txt + move version.txt $(Build.ArtifactStagingDirectory)\vsix\version.txt diff --git a/package.json b/package.json index b084956..a18b63b 100644 --- a/package.json +++ b/package.json @@ -1,920 +1,922 @@ { - "name": "makefile-tools", - "displayName": "Makefile Tools", - "description": "Provide makefile support in VS Code: C/C++ IntelliSense, build, debug/run.", - "version": "0.7.0", - "publisher": "ms-vscode", - "preview": true, - "icon": "res/makefile-logo.png", - "readme": "README.md", - "author": { - "name": "Microsoft Corporation" - }, - "license": "SEE LICENSE IN LICENSE.txt", - "engines": { - "vscode": "^1.74.0" - }, - "bugs": { - "url": "https://github.com/Microsoft/vscode-makefile-tools/issues", - "email": "c_cpp_support@microsoft.com" - }, - "repository": { - "type": "git", - "url": "https://github.com/Microsoft/vscode-makefile-tools.git" - }, - "homepage": "https://github.com/Microsoft/vscode-makefile-tools", - "qna": "https://github.com/Microsoft/vscode-makefile-tools/issues", - "keywords": [ - "C", - "C++", - "IntelliSense", - "Microsoft", - "Makefile" - ], - "categories": [ - "Programming Languages", - "Debuggers", - "Other" - ], - "activationEvents": [ - "onCommand:makefile.setBuildConfiguration", - "onCommand:makefile.getConfiguration", - "onCommand:makefile.setBuildTarget", - "onCommand:makefile.getBuildTarget", - "onCommand:makefile.buildTarget", - "onCommand:makefile.buildCleanTarget", - "onCommand:makefile.buildAll", - "onCommand:makefile.buildCleanAll", - "onCommand:makefile.setLaunchConfiguration", - "onCommand:makefile.launchDebug", - "onCommand:makefile.launchRun", - "onCommand:makefile.launchTargetPath", - "onCommand:makefile.getLaunchTargetPath", - "onCommand:makefile.launchTargetFileName", - "onCommand:makefile.getLaunchTargetFileName", - "onCommand:makefile.getLaunchTargetDirectory", - "onCommand:makefile.getLaunchTargetArgs", - "onCommand:makefile.getLaunchTargetArgsConcat", - "onCommand:makefile.makeBaseDirectory", - "onCommand:makefile.configure", - "onCommand:makefile.cleanConfigure", - "onCommand:makefile.preConfigure", - "onCommand:makefile.postConfigure", - "onCommand:makefile.outline.setBuildConfiguration", - "onCommand:makefile.outline.setBuildTarget", - "onCommand:makefile.outline.buildTarget", - "onCommand:makefile.outline.buildCleanTarget", - "onCommand:makefile.outline.setLaunchConfiguration", - "onCommand:makefile.outline.launchDebug", - "onCommand:makefile.outline.launchRun", - "onCommand:makefile.outline.configure", - "onCommand:makefile.outline.cleanConfigure", - "onCommand:makefile.outline.preConfigure", - "onCommand:makefile.outline.postConfigure", - "onCommand:makefile.resetState", - "workspaceContains:**/makefile", - "workspaceContains:**/Makefile", - "workspaceContains:**/GNUmakefile" - ], - "main": "./dist/main", - "contributes": { - "commands": [ - { - "command": "makefile.buildTarget", - "title": "%makefile-tools.command.makefile.buildTarget.title%" - }, - { - "command": "makefile.buildCleanTarget", - "title": "%makefile-tools.command.makefile.buildCleanTarget.title%" - }, - { - "command": "makefile.buildAll", - "title": "%makefile-tools.command.makefile.buildAll.title%" - }, - { - "command": "makefile.buildCleanAll", - "title": "%makefile-tools.command.makefile.buildCleanAll.title%" - }, - { - "command": "makefile.launchDebug", - "title": "%makefile-tools.command.makefile.launchDebug.title%" - }, - { - "command": "makefile.launchRun", - "title": "%makefile-tools.command.makefile.launchRun.title%" - }, - { - "command": "makefile.setBuildConfiguration", - "title": "%makefile-tools.command.makefile.setBuildConfiguration.title%" - }, - { - "command": "makefile.setBuildTarget", - "title": "%makefile-tools.command.makefile.setBuildTarget.title%" - }, - { - "command": "makefile.setLaunchConfiguration", - "title": "%makefile-tools.command.makefile.setLaunchConfiguration.title%" - }, - { - "command": "makefile.configure", - "title": "%makefile-tools.command.makefile.configure.title%" - }, - { - "command": "makefile.cleanConfigure", - "title": "%makefile-tools.command.makefile.cleanConfigure.title%" - }, - { - "command": "makefile.preConfigure", - "title": "%makefile-tools.command.makefile.preConfigure.title%" - }, - { - "command": "makefile.postConfigure", - "title": "%makefile-tools.command.makefile.postConfigure.title%" - }, - { - "command": "makefile.outline.buildTarget", - "title": "%makefile-tools.command.makefile.buildTarget.title%", - "icon": { - "light": "res/light/build.svg", - "dark": "res/dark/build.svg" - } - }, - { - "command": "makefile.outline.buildCleanTarget", - "title": "%makefile-tools.command.makefile.buildCleanTarget.title%" - }, - { - "command": "makefile.outline.launchDebug", - "title": "%makefile-tools.command.makefile.launchDebug.title%", - "icon": "$(debug)", - "enablement": "makefile:localDebugFeature" - }, - { - "command": "makefile.outline.launchRun", - "title": "%makefile-tools.command.makefile.launchRun.title%", - "icon": "$(run)", - "enablement": "makefile:localRunFeature" - }, - { - "command": "makefile.outline.setBuildConfiguration", - "title": "%makefile-tools.command.makefile.setBuildConfiguration.title%", - "icon": "$(notebook-edit)" - }, - { - "command": "makefile.outline.setBuildTarget", - "title": "%makefile-tools.command.makefile.setBuildTarget.title%", - "icon": "$(notebook-edit)" - }, - { - "command": "makefile.outline.setLaunchConfiguration", - "title": "%makefile-tools.command.makefile.setLaunchConfiguration.title%", - "icon": "$(notebook-edit)" - }, - { - "command": "makefile.outline.openMakefilePathSetting", - "title": "%makefile-tools.command.makefile.openMakefilePathSetting.title%", - "icon": "$(notebook-edit)" - }, - { - "command": "makefile.outline.openMakefileFile", - "title": "%makefile-tools.command.makefile.openMakefileFile.title%", - "icon": "$(open-preview)" - }, - { - "command": "makefile.outline.openMakePathSetting", - "title": "%makefile-tools.command.makefile.openMakePathSetting.title%", - "icon": "$(notebook-edit)" - }, - { - "command": "makefile.outline.openBuildLogSetting", - "title": "%makefile-tools.command.makefile.openBuildLogSetting.title%", - "icon": "$(notebook-edit)" - }, - { - "command": "makefile.outline.openBuildLogFile", - "title": "%makefile-tools.command.makefile.openBuildLogFile.title%", - "icon": "$(open-preview)" - }, - { - "command": "makefile.outline.configure", - "title": "%makefile-tools.command.makefile.configure.title%", - "icon": "$(settings)" - }, - { - "command": "makefile.outline.cleanConfigure", - "title": "%makefile-tools.command.makefile.cleanConfigure.title%" - }, - { - "command": "makefile.outline.preConfigure", - "title": "%makefile-tools.command.makefile.preConfigure.title%" - }, - { - "command": "makefile.outline.postConfigure", - "title": "%makefile-tools.command.makefile.postConfigure.title%" - }, - { - "command": "makefile.resetState", - "title": "%makefile-tools.command.makefile.resetState.title%" - } - ], - "problemMatchers": [ - { - "name": "gcc", - "source": "gcc", - "owner": "makefile-tools", - "fileLocation": [ - "autoDetect", - "${command:makefile.makeBaseDirectory}" - ], - "pattern": { - "regexp": "^(.*?):(\\d+):(\\d*):?\\s+(?:fatal\\s+)?(warning|error):\\s+(.*)$", - "file": 1, - "line": 2, - "column": 3, - "severity": 4, - "message": 5 - } - }, - { - "name": "msvc", - "source": "msvc", - "owner": "makefile-tools", - "base": "$msCompile", - "fileLocation": [ - "autoDetect", - "${command:makefile.makeBaseDirectory}" - ] - } - ], - "configuration": { - "type": "object", - "title": "Makefile Tools", - "properties": { - "makefile.configurations": { - "type": "array", - "default": [], - "description": "%makefile-tools.configuration.makefile.configurations.description%", - "items": { - "type": "object", - "default": null, - "properties": { - "name": { - "type": "string", - "description": "%makefile-tools.configuration.makefile.configurations.name.description%" - }, - "makefilePath": { - "type": "string", - "description": "%makefile-tools.configuration.makefile.configurations.makefilePath.description%" - }, - "makePath": { - "type": "string", - "description": "%makefile-tools.configuration.makefile.configurations.makePath.description%" - }, - "makeDirectory": { - "type": "string", - "description": "%makefile-tools.configuration.makefile.configurations.makeDirectory.description%" - }, - "makeArgs": { - "type": "array", - "description": "%makefile-tools.configuration.makefile.configurations.makeArgs.description%", - "items": { - "type": "string" - }, - "default": [] - }, - "problemMatchers": { - "type": "array", - "items": { - "type": "string" - }, - "default": ["$gcc", "$msvc"], - "description": "%makefile-tools.configuration.makefile.configurations.problemMatchers.description%" - }, - "buildLog": { - "type": "string", - "description": "%makefile-tools.configuration.makefile.configurations.buildLog.description%" - }, - "preConfigureArgs": { - "type": "array", - "items": { - "type": "string" - }, - "default": [], - "description": "%makefile-tools.configuration.makefile.preConfigureArgs.description%" - }, - "postConfigureArgs": { - "type": "array", - "items": { - "type": "string" - }, - "default": [], - "description": "%makefile-tools.configuration.makefile.postConfigureArgs.description%" - } - } - }, - "scope": "resource" - }, - "makefile.defaultLaunchConfiguration": { - "type": "object", - "default": null, - "description": "%makefile-tools.configuration.makefile.defaultLaunchConfiguration.description%", - "properties": { - "MIMode": { - "type": "string", - "description": "%makefile-tools.configuration.makefile.defaultLaunchConfiguration.MIMode.description%", - "enum": [ - "gdb", - "lldb" - ] - }, - "miDebuggerPath": { - "type": "string", - "description": "%makefile-tools.configuration.makefile.defaultLaunchConfiguration.miDebuggerPath.description%" - }, - "stopAtEntry": { - "type": "boolean", - "description": "%makefile-tools.configuration.makefile.defaultLaunchConfiguration.stopAtEntry.description%", - "default": false - }, - "symbolSearchPath": { - "type": "string", - "description": "%makefile-tools.configuration.makefile.defaultLaunchConfiguration.symbolSearchPath.description%" - } - }, - "scope": "resource" - }, - "makefile.launchConfigurations": { - "type": "array", - "default": [], - "description": "%makefile-tools.configuration.makefile.launchConfigurations.description%", - "items": { - "type": "object", - "default": null, - "properties": { - "binaryPath": { - "type": "string", - "description": "%makefile-tools.configuration.makefile.launchConfigurations.binaryPath.description%" - }, - "binaryArgs": { - "type": "array", - "description": "%makefile-tools.configuration.makefile.launchConfigurations.binaryArgs.description%", - "items": { - "type": "string" - }, - "default": [] - }, - "cwd": { - "type": "string", - "description": "%makefile-tools.configuration.makefile.launchConfigurations.cwd.description%" - }, - "MIMode": { - "type": "string", - "description": "%makefile-tools.configuration.makefile.launchConfigurations.MIMode.description%", - "enum": [ - "gdb", - "lldb" - ] - }, - "miDebuggerPath": { - "type": "string", - "description": "%makefile-tools.configuration.makefile.launchConfigurations.miDebuggerPath.description%" - }, - "stopAtEntry": { - "type": "boolean", - "description": "%makefile-tools.configuration.makefile.launchConfigurations.stopAtEntry.description%", - "default": false - }, - "symbolSearchPath": { - "type": "string", - "description": "%makefile-tools.configuration.makefile.launchConfigurations.symbolSearchPath.description%" - } - } - }, - "scope": "resource" - }, - "makefile.loggingLevel": { - "type": "string", - "enum": [ - "Normal", - "Verbose", - "Debug" - ], - "default": "Normal", - "description": "%makefile-tools.configuration.makefile.loggingLevel.description%", - "scope": "resource" - }, - "makefile.makePath": { - "type": "string", - "default": "make", - "description": "%makefile-tools.configuration.makefile.makePath.description%", - "scope": "resource" - }, - "makefile.makeDirectory": { - "type": "string", - "description": "%makefile-tools.configuration.makefile.makeDirectory.description%", - "scope": "resource" - }, - "makefile.makefilePath": { - "type": "string", - "description": "%makefile-tools.configuration.makefile.makefilePath.description%", - "scope": "resource" - }, - "makefile.buildLog": { - "type": "string", - "description": "%makefile-tools.configuration.makefile.buildLog.description%", - "default": null, - "scope": "resource" - }, - "makefile.extensionOutputFolder": { - "type": "string", - "description": "%makefile-tools.configuration.makefile.extensionOutputFolder.description%", - "default": "", - "scope": "resource" - }, - "makefile.extensionLog": { - "type": "string", - "description": "%makefile-tools.configuration.makefile.extensionLog.description%", - "default": "", - "scope": "resource" - }, - "makefile.configurationCachePath": { - "type": "string", - "description": "%makefile-tools.configuration.makefile.configurationCachePath.description%", - "default": "", - "scope": "resource" - }, - "makefile.dryrunSwitches": { - "type": "array", - "default": [ - "--always-make", - "--keep-going", - "--print-directory" - ], - "description": "%makefile-tools.configuration.makefile.dryrunSwitches.description%", - "items": { - "type": "string" - }, - "scope": "resource" - }, - "makefile.additionalCompilerNames": { - "type": "array", - "default": [], - "description": "%makefile-tools.configuration.makefile.additionalCompilerNames.description%", - "items": { - "type": "string" - }, - "scope": "resource" - }, - "makefile.excludeCompilerNames": { - "type": "array", - "default": [], - "description": "%makefile-tools.configuration.makefile.excludeCompilerNames.description%", - "items": { - "type": "string" - }, - "scope": "resource" - }, - "makefile.configureOnOpen": { - "type": "boolean", - "default": true, - "description": "%makefile-tools.configuration.makefile.configureOnOpen.description%", - "scope": "resource" - }, - "makefile.configureOnEdit": { - "type": "boolean", - "default": true, - "description": "%makefile-tools.configuration.makefile.configureOnEdit.description%", - "scope": "resource" - }, - "makefile.configureAfterCommand": { - "type": "boolean", - "default": true, - "description": "%makefile-tools.configuration.makefile.configureAfterCommand.description%", - "scope": "resource" - }, - "makefile.preConfigureScript": { - "type": "string", - "description": "%makefile-tools.configuration.makefile.preConfigureScript.description%", - "default": null, - "scope": "resource" - }, - "makefile.preConfigureArgs": { - "type": "array", - "description": "%makefile-tools.configuration.makefile.preConfigureArgs.description%", - "items": { - "type": "string" - }, - "default": [] - }, - "makefile.postConfigureScript": { - "type": "string", - "description": "%makefile-tools.configuration.makefile.postConfigureScript.description%", - "default": null, - "scope": "resource" - }, - "makefile.postConfigureArgs": { - "type": "array", - "description": "%makefile-tools.configuration.makefile.postConfigureArgs.description%", - "items": { - "type": "string" - }, - "default": [] - }, - "makefile.alwaysPreConfigure": { - "type": "boolean", - "description": "%makefile-tools.configuration.makefile.alwaysPreConfigure.description%", - "default": false, - "scope": "resource" - }, - "makefile.alwaysPostConfigure": { - "type": "boolean", - "description": "%makefile-tools.configuration.makefile.alwaysPostConfigure.description%", - "default": false, - "scope": "resource" - }, - "makefile.ignoreDirectoryCommands": { - "type": "boolean", - "description": "%makefile-tools.configuration.makefile.ignoreDirectoryCommands.description%", - "default": true, - "scope": "resource" - }, - "makefile.phonyOnlyTargets": { - "type": "boolean", - "default": false, - "description": "%makefile-tools.configuration.makefile.phonyOnlyTargets.description%", - "scope": "resource" - }, - "makefile.saveBeforeBuildOrConfigure": { - "type": "boolean", - "default": true, - "description": "%makefile-tools.configuration.makefile.saveBeforeBuildOrConfigure.description%", - "scope": "resource" - }, - "makefile.buildBeforeLaunch": { - "type": "boolean", - "default": true, - "description": "%makefile-tools.configuration.makefile.buildBeforeLaunch.description%", - "scope": "resource" - }, - "makefile.clearOutputBeforeBuild": { - "type": "boolean", - "default": true, - "description": "%makefile-tools.configuration.makefile.clearOutputBeforeBuild.description%", - "scope": "resource" - }, - "makefile.compileCommandsPath": { - "type": "string", - "default": null, - "description": "%makefile-tools.configuration.makefile.compileCommandsPath.description%", - "scope": "resource" - }, - "makefile.panel.visibility": { - "type": "object", - "default": null, - "properties": { - "debug": { - "type": "boolean", - "description": "%makefile-tools.configuration.makefile.panel.visibility.debug.description%", - "default": true - }, - "run": { - "type": "boolean", - "description": "%makefile-tools.configuration.makefile.panel.visibility.run.description%", - "default": true - } - } - } - } - }, - "viewsContainers": { - "activitybar": [ - { - "id": "makefile__viewContainer", - "title": "Makefile", - "when": "makefile:fullFeatureSet", - "icon": "res/viewcontainer.svg" - } - ] - }, - "views": { - "makefile__viewContainer": [ - { - "id": "makefile.outline", - "when": "isWorkspaceTrusted || makefile:testing", - "name": "%makefile-tools.configuration.views.makefile.outline.description%" - } - ] - }, - "menus": { - "commandPalette": [ - { - "command": "makefile.configure", - "when": "isWorkspaceTrusted || makefile:testing" - }, - { - "command": "makefile.cleanConfigure", - "when": "isWorkspaceTrusted || makefile:testing" - }, - { - "command": "makefile.outline.openMakefilePathSetting", - "when": "isWorkspaceTrusted || makefile:testing" - }, - { - "command": "makefile.outline.openMakefileFile", - "when": "isWorkspaceTrusted || makefile:testing" - }, - { - "command": "makefile.outline.openMakePathSetting", - "when": "isWorkspaceTrusted || makefile:testing" - }, - { - "command": "makefile.outline.openBuildLogSetting", - "when": "isWorkspaceTrusted || makefile:testing" - }, - { - "command": "makefile.outline.openBuildLogFile", - "when": "isWorkspaceTrusted || makefile:testing" - }, - { - "command": "makefile.preConfigure", - "when": "makefile:fullFeatureSet" - }, - { - "command": "makefile.postConfigure", - "when": "makefile:fullFeatureSet" - }, - { - "command": "makefile.buildTarget", - "when": "makefile:fullFeatureSet" - }, - { - "command": "makefile.buildCleanTarget", - "when": "makefile:fullFeatureSet" - }, - { - "command": "makefile.buildAll", - "when": "makefile:fullFeatureSet" - }, - { - "command": "makefile.buildCleanAll", - "when": "makefile:fullFeatureSet" - }, - { - "command": "makefile.launchDebug", - "when": "makefile:localDebugFeature" - }, - { - "command": "makefile.launchRun", - "when": "makefile:localRunFeature" - }, - { - "command": "makefile.setBuildConfiguration", - "when": "makefile:fullFeatureSet" - }, - { - "command": "makefile.setBuildTarget", - "when": "makefile:fullFeatureSet" - }, - { - "command": "makefile.setLaunchConfiguration", - "when": "makefile:localDebugFeature" - }, - { - "command": "makefile.outline.configure", - "when": "never" - }, - { - "command": "makefile.outline.cleanConfigure", - "when": "never" - }, - { - "command": "makefile.outline.preConfigure", - "when": "never" - }, - { - "command": "makefile.outline.postConfigure", - "when": "never" - }, - { - "command": "makefile.outline.buildTarget", - "when": "never" - }, - { - "command": "makefile.outline.buildCleanTarget", - "when": "never" - }, - { - "command": "makefile.outline.launchDebug", - "when": "never" - }, - { - "command": "makefile.outline.launchRun", - "when": "never" - }, - { - "command": "makefile.outline.setBuildConfiguration", - "when": "never" - }, - { - "command": "makefile.outline.setBuildTarget", - "when": "never" - }, - { - "command": "makefile.outline.setLaunchConfiguration", - "when": "never" - }, - { - "command": "makefile.resetState", - "when": "isWorkspaceTrusted || makefile:testing" - } - ], - "view/title": [ - { - "command": "makefile.outline.preConfigure", - "when": "view == makefile.outline", - "group": "1_makefileOutline@1" - }, - { - "command": "makefile.outline.postConfigure", - "when": "view == makefile.outline", - "group": "1_makefileOutline@2" - }, - { - "command": "makefile.outline.configure", - "when": "view == makefile.outline", - "group": "1_makefileOutline@3" - }, - { - "command": "makefile.outline.cleanConfigure", - "when": "view == makefile.outline", - "group": "1_makefileOutline@4" - }, - { - "command": "makefile.outline.buildTarget", - "when": "makefile:fullFeatureSet && view == makefile.outline", - "group": "navigation@1" - }, - { - "command": "makefile.outline.buildCleanTarget", - "when": "makefile:fullFeatureSet && view == makefile.outline", - "group": "1_makefileOutline@4" - }, - { - "command": "makefile.outline.launchDebug", - "when": "makefile:fullFeatureSet && view == makefile.outline && makefile:localDebugFeature", - "group": "navigation@2" - }, - { - "command": "makefile.outline.launchRun", - "when": "makefile:fullFeatureSet && view == makefile.outline && makefile:localRunFeature", - "group": "navigation@3" - } - ], - "view/item/context": [ - { - "command": "makefile.outline.configure", - "when": "view == makefile.outline && viewItem =~ /nodeType=configuration/", - "group": "1_stateActions@1" - }, - { - "command": "makefile.outline.cleanConfigure", - "when": "view == makefile.outline && viewItem =~ /nodeType=configuration/", - "group": "1_stateActions@2" - }, - { - "command": "makefile.outline.setBuildConfiguration", - "when": "makefile:fullFeatureSet && view == makefile.outline && viewItem =~ /nodeType=configuration/", - "group": "inline@1" - }, - { - "command": "makefile.outline.buildTarget", - "when": "makefile:fullFeatureSet && view == makefile.outline && viewItem =~ /nodeType=buildTarget/", - "group": "1_stateActions@1" - }, - { - "command": "makefile.outline.buildCleanTarget", - "when": "makefile:fullFeatureSet && view == makefile.outline && viewItem =~ /nodeType=buildTarget/", - "group": "1_stateActions@2" - }, - { - "command": "makefile.outline.setBuildTarget", - "when": "makefile:fullFeatureSet && view == makefile.outline && viewItem =~ /nodeType=buildTarget/", - "group": "inline@1" - }, - { - "command": "makefile.outline.launchDebug", - "when": "makefile:fullFeatureSet && view == makefile.outline && viewItem =~ /nodeType=launchTarget/", - "group": "1_stateActions@1" - }, - { - "command": "makefile.outline.launchRun", - "when": "makefile:fullFeatureSet && view == makefile.outline && viewItem =~ /nodeType=launchTarget/", - "group": "1_stateActions@2" - }, - { - "command": "makefile.outline.setLaunchConfiguration", - "when": "makefile:fullFeatureSet && view == makefile.outline && viewItem =~ /nodeType=launchTarget/", - "group": "inline@1" - }, - { - "command": "makefile.outline.openMakefilePathSetting", - "when": "view == makefile.outline && viewItem =~ /nodeType=makefilePathInfo/", - "group": "inline@2" - }, - { - "command": "makefile.outline.openMakefileFile", - "when": "view == makefile.outline && viewItem =~ /nodeType=makefilePathInfo/ && makefile.makefileFilePresent", - "group": "inline@1" - }, - { - "command": "makefile.outline.openMakePathSetting", - "when": "view == makefile.outline && viewItem =~ /nodeType=makePathInfo/", - "group": "inline@1" - }, - { - "command": "makefile.outline.openBuildLogSetting", - "when": "view == makefile.outline && viewItem =~ /nodeType=buildLogPathInfo/", - "group": "inline@1" - }, - { - "command": "makefile.outline.openBuildLogFile", - "when": "view == makefile.outline && viewItem =~ /nodeType=buildLogPathInfo/ && makefile.buildLogFilePresent", - "group": "inline@1" - } - ] + "name": "makefile-tools", + "displayName": "Makefile Tools", + "description": "Provide makefile support in VS Code: C/C++ IntelliSense, build, debug/run.", + "version": "0.7.0", + "publisher": "ms-vscode", + "preview": true, + "icon": "res/makefile-logo.png", + "readme": "README.md", + "author": { + "name": "Microsoft Corporation" + }, + "license": "SEE LICENSE IN LICENSE.txt", + "engines": { + "vscode": "^1.74.0" + }, + "bugs": { + "url": "https://github.com/Microsoft/vscode-makefile-tools/issues", + "email": "c_cpp_support@microsoft.com" + }, + "repository": { + "type": "git", + "url": "https://github.com/Microsoft/vscode-makefile-tools.git" + }, + "homepage": "https://github.com/Microsoft/vscode-makefile-tools", + "qna": "https://github.com/Microsoft/vscode-makefile-tools/issues", + "keywords": [ + "C", + "C++", + "IntelliSense", + "Microsoft", + "Makefile" + ], + "categories": [ + "Programming Languages", + "Debuggers", + "Other" + ], + "activationEvents": [ + "onCommand:makefile.setBuildConfiguration", + "onCommand:makefile.getConfiguration", + "onCommand:makefile.setBuildTarget", + "onCommand:makefile.getBuildTarget", + "onCommand:makefile.buildTarget", + "onCommand:makefile.buildCleanTarget", + "onCommand:makefile.buildAll", + "onCommand:makefile.buildCleanAll", + "onCommand:makefile.setLaunchConfiguration", + "onCommand:makefile.launchDebug", + "onCommand:makefile.launchRun", + "onCommand:makefile.launchTargetPath", + "onCommand:makefile.getLaunchTargetPath", + "onCommand:makefile.launchTargetFileName", + "onCommand:makefile.getLaunchTargetFileName", + "onCommand:makefile.getLaunchTargetDirectory", + "onCommand:makefile.getLaunchTargetArgs", + "onCommand:makefile.getLaunchTargetArgsConcat", + "onCommand:makefile.makeBaseDirectory", + "onCommand:makefile.configure", + "onCommand:makefile.cleanConfigure", + "onCommand:makefile.preConfigure", + "onCommand:makefile.postConfigure", + "onCommand:makefile.outline.setBuildConfiguration", + "onCommand:makefile.outline.setBuildTarget", + "onCommand:makefile.outline.buildTarget", + "onCommand:makefile.outline.buildCleanTarget", + "onCommand:makefile.outline.setLaunchConfiguration", + "onCommand:makefile.outline.launchDebug", + "onCommand:makefile.outline.launchRun", + "onCommand:makefile.outline.configure", + "onCommand:makefile.outline.cleanConfigure", + "onCommand:makefile.outline.preConfigure", + "onCommand:makefile.outline.postConfigure", + "onCommand:makefile.resetState", + "workspaceContains:**/makefile", + "workspaceContains:**/Makefile", + "workspaceContains:**/GNUmakefile" + ], + "main": "./dist/main", + "contributes": { + "commands": [ + { + "command": "makefile.buildTarget", + "title": "%makefile-tools.command.makefile.buildTarget.title%" + }, + { + "command": "makefile.buildCleanTarget", + "title": "%makefile-tools.command.makefile.buildCleanTarget.title%" + }, + { + "command": "makefile.buildAll", + "title": "%makefile-tools.command.makefile.buildAll.title%" + }, + { + "command": "makefile.buildCleanAll", + "title": "%makefile-tools.command.makefile.buildCleanAll.title%" + }, + { + "command": "makefile.launchDebug", + "title": "%makefile-tools.command.makefile.launchDebug.title%" + }, + { + "command": "makefile.launchRun", + "title": "%makefile-tools.command.makefile.launchRun.title%" + }, + { + "command": "makefile.setBuildConfiguration", + "title": "%makefile-tools.command.makefile.setBuildConfiguration.title%" + }, + { + "command": "makefile.setBuildTarget", + "title": "%makefile-tools.command.makefile.setBuildTarget.title%" + }, + { + "command": "makefile.setLaunchConfiguration", + "title": "%makefile-tools.command.makefile.setLaunchConfiguration.title%" + }, + { + "command": "makefile.configure", + "title": "%makefile-tools.command.makefile.configure.title%" + }, + { + "command": "makefile.cleanConfigure", + "title": "%makefile-tools.command.makefile.cleanConfigure.title%" + }, + { + "command": "makefile.preConfigure", + "title": "%makefile-tools.command.makefile.preConfigure.title%" + }, + { + "command": "makefile.postConfigure", + "title": "%makefile-tools.command.makefile.postConfigure.title%" + }, + { + "command": "makefile.outline.buildTarget", + "title": "%makefile-tools.command.makefile.buildTarget.title%", + "icon": { + "light": "res/light/build.svg", + "dark": "res/dark/build.svg" } - }, - "scripts": { - "vscode:prepublish": "yarn compile-production", - "compile": "yarn install && webpack --mode development", - "compile-watch": "yarn install && webpack --mode development --watch --progress", - "compile-production": "yarn install && yarn run translations-generate && yarn run bundle-assets && webpack --env BUILD_VSCODE_NLS=true --mode production", - "bundle-assets": "gulp bundle-assets", - "translations-export": "gulp translations-export", - "translations-generate": "gulp translations-generate", - "translations-import": "gulp translations-import", - "watch": "tsc -watch -p ./", - "package": "vsce package --yarn -o makefile-tools.vsix", - "pretest": "tsc -p test.tsconfig.json", - "test": "yarn run pretest && node ./out/src/test/runTest.js" - }, - "devDependencies": { - "@types/mocha": "^9.0.0", - "@types/node": "^10.17.17", - "@types/uuid": "^9.0.7", - "@types/vscode": "^1.74.0", - "tslint": "^5.20.1", - "tslint-microsoft-contrib": "^6.2.0", - "tslint-no-unused-expression-chai": "^0.1.4", - "@types/chai": "^4.2.15", - "@types/chai-as-promised": "^7.1.3", - "@types/chai-string": "^1.4.2", - "chai": "^4.3.0", - "chai-as-promised": "^7.1.1", - "chai-string": "^1.5.0", - "typescript": "^5.0.2", - "vsce": "^1.95.0", - "vrsource-tslint-rules": "^6.0.0", - "vscode-nls-dev": "^3.3.2", - "gulp": "^4.0.2", - "gulp-eslint": "^6.0.0", - "gulp-filter": "^6.0.0", - "gulp-mocha": "^8.0.0", - "gulp-sourcemaps": "^3.0.0", - "gulp-typescript": "^5.0.1", - "fs-extra": "^9.1.0", - "@octokit/rest": "^18.1.1", - "parse-git-config": "^3.0.0", - "jsonc-parser": "^3.0.0", - "@vscode/test-electron": "^2.3.8", - "ts-loader": "^8.0.17", - "webpack": "^5.76.0", - "webpack-cli": "^4.5.0" - }, - "dependencies": { - "@types/glob": "^7.1.1", - "glob": "^7.1.6", - "module-alias": "^2.2.2", - "uuid": "^9.0.1", - "vscode-cpptools": "^6.1.0", - "vscode-nls": "^5.0.0", - "@vscode/extension-telemetry": "^0.6.2", - "vscode-jsonrpc": "^3.6.2" - }, - "resolutions": { - "ansi-regex": "^5.0.1", - "glob-parent": "^5.1.2", - "markdown-it": "^12.3.2", - "minimist": "^1.2.6", - "nanoid": "^3.1.20", - "minimatch": "^3.0.5", - "xml2js": "^0.5.0" - }, - "capabilities": { - "untrustedWorkspaces": { - "supported": false, - "description": "" + }, + { + "command": "makefile.outline.buildCleanTarget", + "title": "%makefile-tools.command.makefile.buildCleanTarget.title%" + }, + { + "command": "makefile.outline.launchDebug", + "title": "%makefile-tools.command.makefile.launchDebug.title%", + "icon": "$(debug)", + "enablement": "makefile:localDebugFeature" + }, + { + "command": "makefile.outline.launchRun", + "title": "%makefile-tools.command.makefile.launchRun.title%", + "icon": "$(run)", + "enablement": "makefile:localRunFeature" + }, + { + "command": "makefile.outline.setBuildConfiguration", + "title": "%makefile-tools.command.makefile.setBuildConfiguration.title%", + "icon": "$(notebook-edit)" + }, + { + "command": "makefile.outline.setBuildTarget", + "title": "%makefile-tools.command.makefile.setBuildTarget.title%", + "icon": "$(notebook-edit)" + }, + { + "command": "makefile.outline.setLaunchConfiguration", + "title": "%makefile-tools.command.makefile.setLaunchConfiguration.title%", + "icon": "$(notebook-edit)" + }, + { + "command": "makefile.outline.openMakefilePathSetting", + "title": "%makefile-tools.command.makefile.openMakefilePathSetting.title%", + "icon": "$(notebook-edit)" + }, + { + "command": "makefile.outline.openMakefileFile", + "title": "%makefile-tools.command.makefile.openMakefileFile.title%", + "icon": "$(open-preview)" + }, + { + "command": "makefile.outline.openMakePathSetting", + "title": "%makefile-tools.command.makefile.openMakePathSetting.title%", + "icon": "$(notebook-edit)" + }, + { + "command": "makefile.outline.openBuildLogSetting", + "title": "%makefile-tools.command.makefile.openBuildLogSetting.title%", + "icon": "$(notebook-edit)" + }, + { + "command": "makefile.outline.openBuildLogFile", + "title": "%makefile-tools.command.makefile.openBuildLogFile.title%", + "icon": "$(open-preview)" + }, + { + "command": "makefile.outline.configure", + "title": "%makefile-tools.command.makefile.configure.title%", + "icon": "$(settings)" + }, + { + "command": "makefile.outline.cleanConfigure", + "title": "%makefile-tools.command.makefile.cleanConfigure.title%" + }, + { + "command": "makefile.outline.preConfigure", + "title": "%makefile-tools.command.makefile.preConfigure.title%" + }, + { + "command": "makefile.outline.postConfigure", + "title": "%makefile-tools.command.makefile.postConfigure.title%" + }, + { + "command": "makefile.resetState", + "title": "%makefile-tools.command.makefile.resetState.title%" } + ], + "problemMatchers": [ + { + "name": "gcc", + "source": "gcc", + "owner": "makefile-tools", + "fileLocation": [ + "autoDetect", + "${command:makefile.makeBaseDirectory}" + ], + "pattern": { + "regexp": "^(.*?):(\\d+):(\\d*):?\\s+(?:fatal\\s+)?(warning|error):\\s+(.*)$", + "file": 1, + "line": 2, + "column": 3, + "severity": 4, + "message": 5 + } + }, + { + "name": "msvc", + "source": "msvc", + "owner": "makefile-tools", + "base": "$msCompile", + "fileLocation": [ + "autoDetect", + "${command:makefile.makeBaseDirectory}" + ] + } + ], + "configuration": { + "type": "object", + "title": "Makefile Tools", + "properties": { + "makefile.configurations": { + "type": "array", + "default": [], + "description": "%makefile-tools.configuration.makefile.configurations.description%", + "items": { + "type": "object", + "default": null, + "properties": { + "name": { + "type": "string", + "description": "%makefile-tools.configuration.makefile.configurations.name.description%" + }, + "makefilePath": { + "type": "string", + "description": "%makefile-tools.configuration.makefile.configurations.makefilePath.description%" + }, + "makePath": { + "type": "string", + "description": "%makefile-tools.configuration.makefile.configurations.makePath.description%" + }, + "makeDirectory": { + "type": "string", + "description": "%makefile-tools.configuration.makefile.configurations.makeDirectory.description%" + }, + "makeArgs": { + "type": "array", + "description": "%makefile-tools.configuration.makefile.configurations.makeArgs.description%", + "items": { + "type": "string" + }, + "default": [] + }, + "problemMatchers": { + "type": "array", + "items": { + "type": "string" + }, + "default": [ + "$gcc", + "$msvc" + ], + "description": "%makefile-tools.configuration.makefile.configurations.problemMatchers.description%" + }, + "buildLog": { + "type": "string", + "description": "%makefile-tools.configuration.makefile.configurations.buildLog.description%" + }, + "preConfigureArgs": { + "type": "array", + "items": { + "type": "string" + }, + "default": [], + "description": "%makefile-tools.configuration.makefile.preConfigureArgs.description%" + }, + "postConfigureArgs": { + "type": "array", + "items": { + "type": "string" + }, + "default": [], + "description": "%makefile-tools.configuration.makefile.postConfigureArgs.description%" + } + } + }, + "scope": "resource" + }, + "makefile.defaultLaunchConfiguration": { + "type": "object", + "default": null, + "description": "%makefile-tools.configuration.makefile.defaultLaunchConfiguration.description%", + "properties": { + "MIMode": { + "type": "string", + "description": "%makefile-tools.configuration.makefile.defaultLaunchConfiguration.MIMode.description%", + "enum": [ + "gdb", + "lldb" + ] + }, + "miDebuggerPath": { + "type": "string", + "description": "%makefile-tools.configuration.makefile.defaultLaunchConfiguration.miDebuggerPath.description%" + }, + "stopAtEntry": { + "type": "boolean", + "description": "%makefile-tools.configuration.makefile.defaultLaunchConfiguration.stopAtEntry.description%", + "default": false + }, + "symbolSearchPath": { + "type": "string", + "description": "%makefile-tools.configuration.makefile.defaultLaunchConfiguration.symbolSearchPath.description%" + } + }, + "scope": "resource" + }, + "makefile.launchConfigurations": { + "type": "array", + "default": [], + "description": "%makefile-tools.configuration.makefile.launchConfigurations.description%", + "items": { + "type": "object", + "default": null, + "properties": { + "binaryPath": { + "type": "string", + "description": "%makefile-tools.configuration.makefile.launchConfigurations.binaryPath.description%" + }, + "binaryArgs": { + "type": "array", + "description": "%makefile-tools.configuration.makefile.launchConfigurations.binaryArgs.description%", + "items": { + "type": "string" + }, + "default": [] + }, + "cwd": { + "type": "string", + "description": "%makefile-tools.configuration.makefile.launchConfigurations.cwd.description%" + }, + "MIMode": { + "type": "string", + "description": "%makefile-tools.configuration.makefile.launchConfigurations.MIMode.description%", + "enum": [ + "gdb", + "lldb" + ] + }, + "miDebuggerPath": { + "type": "string", + "description": "%makefile-tools.configuration.makefile.launchConfigurations.miDebuggerPath.description%" + }, + "stopAtEntry": { + "type": "boolean", + "description": "%makefile-tools.configuration.makefile.launchConfigurations.stopAtEntry.description%", + "default": false + }, + "symbolSearchPath": { + "type": "string", + "description": "%makefile-tools.configuration.makefile.launchConfigurations.symbolSearchPath.description%" + } + } + }, + "scope": "resource" + }, + "makefile.loggingLevel": { + "type": "string", + "enum": [ + "Normal", + "Verbose", + "Debug" + ], + "default": "Normal", + "description": "%makefile-tools.configuration.makefile.loggingLevel.description%", + "scope": "resource" + }, + "makefile.makePath": { + "type": "string", + "default": "make", + "description": "%makefile-tools.configuration.makefile.makePath.description%", + "scope": "resource" + }, + "makefile.makeDirectory": { + "type": "string", + "description": "%makefile-tools.configuration.makefile.makeDirectory.description%", + "scope": "resource" + }, + "makefile.makefilePath": { + "type": "string", + "description": "%makefile-tools.configuration.makefile.makefilePath.description%", + "scope": "resource" + }, + "makefile.buildLog": { + "type": "string", + "description": "%makefile-tools.configuration.makefile.buildLog.description%", + "default": null, + "scope": "resource" + }, + "makefile.extensionOutputFolder": { + "type": "string", + "description": "%makefile-tools.configuration.makefile.extensionOutputFolder.description%", + "default": "", + "scope": "resource" + }, + "makefile.extensionLog": { + "type": "string", + "description": "%makefile-tools.configuration.makefile.extensionLog.description%", + "default": "", + "scope": "resource" + }, + "makefile.configurationCachePath": { + "type": "string", + "description": "%makefile-tools.configuration.makefile.configurationCachePath.description%", + "default": "", + "scope": "resource" + }, + "makefile.dryrunSwitches": { + "type": "array", + "default": [ + "--always-make", + "--keep-going", + "--print-directory" + ], + "description": "%makefile-tools.configuration.makefile.dryrunSwitches.description%", + "items": { + "type": "string" + }, + "scope": "resource" + }, + "makefile.additionalCompilerNames": { + "type": "array", + "default": [], + "description": "%makefile-tools.configuration.makefile.additionalCompilerNames.description%", + "items": { + "type": "string" + }, + "scope": "resource" + }, + "makefile.excludeCompilerNames": { + "type": "array", + "default": [], + "description": "%makefile-tools.configuration.makefile.excludeCompilerNames.description%", + "items": { + "type": "string" + }, + "scope": "resource" + }, + "makefile.configureOnOpen": { + "type": "boolean", + "default": true, + "description": "%makefile-tools.configuration.makefile.configureOnOpen.description%", + "scope": "resource" + }, + "makefile.configureOnEdit": { + "type": "boolean", + "default": true, + "description": "%makefile-tools.configuration.makefile.configureOnEdit.description%", + "scope": "resource" + }, + "makefile.configureAfterCommand": { + "type": "boolean", + "default": true, + "description": "%makefile-tools.configuration.makefile.configureAfterCommand.description%", + "scope": "resource" + }, + "makefile.preConfigureScript": { + "type": "string", + "description": "%makefile-tools.configuration.makefile.preConfigureScript.description%", + "default": null, + "scope": "resource" + }, + "makefile.preConfigureArgs": { + "type": "array", + "description": "%makefile-tools.configuration.makefile.preConfigureArgs.description%", + "items": { + "type": "string" + }, + "default": [] + }, + "makefile.postConfigureScript": { + "type": "string", + "description": "%makefile-tools.configuration.makefile.postConfigureScript.description%", + "default": null, + "scope": "resource" + }, + "makefile.postConfigureArgs": { + "type": "array", + "description": "%makefile-tools.configuration.makefile.postConfigureArgs.description%", + "items": { + "type": "string" + }, + "default": [] + }, + "makefile.alwaysPreConfigure": { + "type": "boolean", + "description": "%makefile-tools.configuration.makefile.alwaysPreConfigure.description%", + "default": false, + "scope": "resource" + }, + "makefile.alwaysPostConfigure": { + "type": "boolean", + "description": "%makefile-tools.configuration.makefile.alwaysPostConfigure.description%", + "default": false, + "scope": "resource" + }, + "makefile.ignoreDirectoryCommands": { + "type": "boolean", + "description": "%makefile-tools.configuration.makefile.ignoreDirectoryCommands.description%", + "default": true, + "scope": "resource" + }, + "makefile.phonyOnlyTargets": { + "type": "boolean", + "default": false, + "description": "%makefile-tools.configuration.makefile.phonyOnlyTargets.description%", + "scope": "resource" + }, + "makefile.saveBeforeBuildOrConfigure": { + "type": "boolean", + "default": true, + "description": "%makefile-tools.configuration.makefile.saveBeforeBuildOrConfigure.description%", + "scope": "resource" + }, + "makefile.buildBeforeLaunch": { + "type": "boolean", + "default": true, + "description": "%makefile-tools.configuration.makefile.buildBeforeLaunch.description%", + "scope": "resource" + }, + "makefile.clearOutputBeforeBuild": { + "type": "boolean", + "default": true, + "description": "%makefile-tools.configuration.makefile.clearOutputBeforeBuild.description%", + "scope": "resource" + }, + "makefile.compileCommandsPath": { + "type": "string", + "default": null, + "description": "%makefile-tools.configuration.makefile.compileCommandsPath.description%", + "scope": "resource" + }, + "makefile.panel.visibility": { + "type": "object", + "default": null, + "properties": { + "debug": { + "type": "boolean", + "description": "%makefile-tools.configuration.makefile.panel.visibility.debug.description%", + "default": true + }, + "run": { + "type": "boolean", + "description": "%makefile-tools.configuration.makefile.panel.visibility.run.description%", + "default": true + } + } + } + } + }, + "viewsContainers": { + "activitybar": [ + { + "id": "makefile__viewContainer", + "title": "Makefile", + "when": "makefile:fullFeatureSet", + "icon": "res/viewcontainer.svg" + } + ] + }, + "views": { + "makefile__viewContainer": [ + { + "id": "makefile.outline", + "when": "isWorkspaceTrusted || makefile:testing", + "name": "%makefile-tools.configuration.views.makefile.outline.description%" + } + ] + }, + "menus": { + "commandPalette": [ + { + "command": "makefile.configure", + "when": "isWorkspaceTrusted || makefile:testing" + }, + { + "command": "makefile.cleanConfigure", + "when": "isWorkspaceTrusted || makefile:testing" + }, + { + "command": "makefile.outline.openMakefilePathSetting", + "when": "isWorkspaceTrusted || makefile:testing" + }, + { + "command": "makefile.outline.openMakefileFile", + "when": "isWorkspaceTrusted || makefile:testing" + }, + { + "command": "makefile.outline.openMakePathSetting", + "when": "isWorkspaceTrusted || makefile:testing" + }, + { + "command": "makefile.outline.openBuildLogSetting", + "when": "isWorkspaceTrusted || makefile:testing" + }, + { + "command": "makefile.outline.openBuildLogFile", + "when": "isWorkspaceTrusted || makefile:testing" + }, + { + "command": "makefile.preConfigure", + "when": "makefile:fullFeatureSet" + }, + { + "command": "makefile.postConfigure", + "when": "makefile:fullFeatureSet" + }, + { + "command": "makefile.buildTarget", + "when": "makefile:fullFeatureSet" + }, + { + "command": "makefile.buildCleanTarget", + "when": "makefile:fullFeatureSet" + }, + { + "command": "makefile.buildAll", + "when": "makefile:fullFeatureSet" + }, + { + "command": "makefile.buildCleanAll", + "when": "makefile:fullFeatureSet" + }, + { + "command": "makefile.launchDebug", + "when": "makefile:localDebugFeature" + }, + { + "command": "makefile.launchRun", + "when": "makefile:localRunFeature" + }, + { + "command": "makefile.setBuildConfiguration", + "when": "makefile:fullFeatureSet" + }, + { + "command": "makefile.setBuildTarget", + "when": "makefile:fullFeatureSet" + }, + { + "command": "makefile.setLaunchConfiguration", + "when": "makefile:localDebugFeature" + }, + { + "command": "makefile.outline.configure", + "when": "never" + }, + { + "command": "makefile.outline.cleanConfigure", + "when": "never" + }, + { + "command": "makefile.outline.preConfigure", + "when": "never" + }, + { + "command": "makefile.outline.postConfigure", + "when": "never" + }, + { + "command": "makefile.outline.buildTarget", + "when": "never" + }, + { + "command": "makefile.outline.buildCleanTarget", + "when": "never" + }, + { + "command": "makefile.outline.launchDebug", + "when": "never" + }, + { + "command": "makefile.outline.launchRun", + "when": "never" + }, + { + "command": "makefile.outline.setBuildConfiguration", + "when": "never" + }, + { + "command": "makefile.outline.setBuildTarget", + "when": "never" + }, + { + "command": "makefile.outline.setLaunchConfiguration", + "when": "never" + }, + { + "command": "makefile.resetState", + "when": "isWorkspaceTrusted || makefile:testing" + } + ], + "view/title": [ + { + "command": "makefile.outline.preConfigure", + "when": "view == makefile.outline", + "group": "1_makefileOutline@1" + }, + { + "command": "makefile.outline.postConfigure", + "when": "view == makefile.outline", + "group": "1_makefileOutline@2" + }, + { + "command": "makefile.outline.configure", + "when": "view == makefile.outline", + "group": "1_makefileOutline@3" + }, + { + "command": "makefile.outline.cleanConfigure", + "when": "view == makefile.outline", + "group": "1_makefileOutline@4" + }, + { + "command": "makefile.outline.buildTarget", + "when": "makefile:fullFeatureSet && view == makefile.outline", + "group": "navigation@1" + }, + { + "command": "makefile.outline.buildCleanTarget", + "when": "makefile:fullFeatureSet && view == makefile.outline", + "group": "1_makefileOutline@4" + }, + { + "command": "makefile.outline.launchDebug", + "when": "makefile:fullFeatureSet && view == makefile.outline && makefile:localDebugFeature", + "group": "navigation@2" + }, + { + "command": "makefile.outline.launchRun", + "when": "makefile:fullFeatureSet && view == makefile.outline && makefile:localRunFeature", + "group": "navigation@3" + } + ], + "view/item/context": [ + { + "command": "makefile.outline.configure", + "when": "view == makefile.outline && viewItem =~ /nodeType=configuration/", + "group": "1_stateActions@1" + }, + { + "command": "makefile.outline.cleanConfigure", + "when": "view == makefile.outline && viewItem =~ /nodeType=configuration/", + "group": "1_stateActions@2" + }, + { + "command": "makefile.outline.setBuildConfiguration", + "when": "makefile:fullFeatureSet && view == makefile.outline && viewItem =~ /nodeType=configuration/", + "group": "inline@1" + }, + { + "command": "makefile.outline.buildTarget", + "when": "makefile:fullFeatureSet && view == makefile.outline && viewItem =~ /nodeType=buildTarget/", + "group": "1_stateActions@1" + }, + { + "command": "makefile.outline.buildCleanTarget", + "when": "makefile:fullFeatureSet && view == makefile.outline && viewItem =~ /nodeType=buildTarget/", + "group": "1_stateActions@2" + }, + { + "command": "makefile.outline.setBuildTarget", + "when": "makefile:fullFeatureSet && view == makefile.outline && viewItem =~ /nodeType=buildTarget/", + "group": "inline@1" + }, + { + "command": "makefile.outline.launchDebug", + "when": "makefile:fullFeatureSet && view == makefile.outline && viewItem =~ /nodeType=launchTarget/", + "group": "1_stateActions@1" + }, + { + "command": "makefile.outline.launchRun", + "when": "makefile:fullFeatureSet && view == makefile.outline && viewItem =~ /nodeType=launchTarget/", + "group": "1_stateActions@2" + }, + { + "command": "makefile.outline.setLaunchConfiguration", + "when": "makefile:fullFeatureSet && view == makefile.outline && viewItem =~ /nodeType=launchTarget/", + "group": "inline@1" + }, + { + "command": "makefile.outline.openMakefilePathSetting", + "when": "view == makefile.outline && viewItem =~ /nodeType=makefilePathInfo/", + "group": "inline@2" + }, + { + "command": "makefile.outline.openMakefileFile", + "when": "view == makefile.outline && viewItem =~ /nodeType=makefilePathInfo/ && makefile.makefileFilePresent", + "group": "inline@1" + }, + { + "command": "makefile.outline.openMakePathSetting", + "when": "view == makefile.outline && viewItem =~ /nodeType=makePathInfo/", + "group": "inline@1" + }, + { + "command": "makefile.outline.openBuildLogSetting", + "when": "view == makefile.outline && viewItem =~ /nodeType=buildLogPathInfo/", + "group": "inline@1" + }, + { + "command": "makefile.outline.openBuildLogFile", + "when": "view == makefile.outline && viewItem =~ /nodeType=buildLogPathInfo/ && makefile.buildLogFilePresent", + "group": "inline@1" + } + ] } - + }, + "scripts": { + "vscode:prepublish": "yarn compile-production", + "compile": "yarn install && webpack --mode development", + "compile-watch": "yarn install && webpack --mode development --watch --progress", + "compile-production": "yarn install && yarn run translations-generate && yarn run bundle-assets && webpack --env BUILD_VSCODE_NLS=true --mode production", + "bundle-assets": "gulp bundle-assets", + "translations-export": "gulp translations-export", + "translations-generate": "gulp translations-generate", + "translations-import": "gulp translations-import", + "watch": "tsc -watch -p ./", + "package": "vsce package --yarn -o makefile-tools.vsix", + "pretest": "tsc -p test.tsconfig.json", + "test": "yarn run pretest && node ./out/src/test/runTest.js" + }, + "devDependencies": { + "@types/mocha": "^9.0.0", + "@types/node": "^10.17.17", + "@types/uuid": "^9.0.7", + "@types/vscode": "^1.74.0", + "tslint": "^5.20.1", + "tslint-microsoft-contrib": "^6.2.0", + "tslint-no-unused-expression-chai": "^0.1.4", + "@types/chai": "^4.2.15", + "@types/chai-as-promised": "^7.1.3", + "@types/chai-string": "^1.4.2", + "chai": "^4.3.0", + "chai-as-promised": "^7.1.1", + "chai-string": "^1.5.0", + "typescript": "^5.0.2", + "vsce": "^1.95.0", + "vrsource-tslint-rules": "^6.0.0", + "vscode-nls-dev": "^3.3.2", + "gulp": "^4.0.2", + "gulp-eslint": "^6.0.0", + "gulp-filter": "^6.0.0", + "gulp-mocha": "^8.0.0", + "gulp-sourcemaps": "^3.0.0", + "gulp-typescript": "^5.0.1", + "fs-extra": "^9.1.0", + "@octokit/rest": "^18.1.1", + "parse-git-config": "^3.0.0", + "jsonc-parser": "^3.0.0", + "@vscode/test-electron": "^2.3.8", + "ts-loader": "^8.0.17", + "webpack": "^5.76.0", + "webpack-cli": "^4.5.0" + }, + "dependencies": { + "@types/glob": "^7.1.1", + "glob": "^7.1.6", + "module-alias": "^2.2.2", + "uuid": "^9.0.1", + "vscode-cpptools": "^6.1.0", + "vscode-nls": "^5.0.0", + "@vscode/extension-telemetry": "^0.6.2", + "vscode-jsonrpc": "^3.6.2" + }, + "resolutions": { + "ansi-regex": "^5.0.1", + "glob-parent": "^5.1.2", + "markdown-it": "^12.3.2", + "minimist": "^1.2.6", + "nanoid": "^3.1.20", + "minimatch": "^3.0.5", + "xml2js": "^0.5.0" + }, + "capabilities": { + "untrustedWorkspaces": { + "supported": false, + "description": "" + } + } } diff --git a/package.nls.json b/package.nls.json index 0b4f33b..f6079d8 100644 --- a/package.nls.json +++ b/package.nls.json @@ -1,72 +1,72 @@ { - "makefile-tools.command.makefile.buildTarget.title": "Makefile: Build the current target", - "makefile-tools.command.makefile.buildCleanTarget.title": "Makefile: Build clean the current target", - "makefile-tools.command.makefile.buildAll.title": "Makefile: Build the target ALL", - "makefile-tools.command.makefile.buildCleanAll.title": "Makefile: Build clean the target ALL", - "makefile-tools.command.makefile.launchDebug.title": "Makefile: Debug the selected binary target", - "makefile-tools.command.makefile.launchRun.title": "Makefile: Run the selected binary target in the terminal", - "makefile-tools.command.makefile.setBuildConfiguration.title": "Makefile: Set the current build configuration", - "makefile-tools.command.makefile.setBuildTarget.title": "Makefile: Set the target to be built by make", - "makefile-tools.command.makefile.setLaunchConfiguration.title": "Makefile: Set the make launch configuration", - "makefile-tools.command.makefile.openMakefilePathSetting.title": "Makefile: Open Makefile Path Setting", - "makefile-tools.command.makefile.openMakePathSetting.title": "Makefile: Open Make Path Setting", - "makefile-tools.command.makefile.openBuildLogSetting.title": "Makefile: Open Build Log Setting", - "makefile-tools.command.makefile.openBuildLogFile.title": "Makefile: Open Build Log File", - "makefile-tools.command.makefile.openMakefileFile.title": "Makefile: Open Makefile File", - "workbench.action.openWorkspaceSettings.title": "Preferences: Open Workspace Settings", - "makefile-tools.command.makefile.configure.title": "Makefile: Configure", - "makefile-tools.command.makefile.cleanConfigure.title": "Makefile: Clean Configure", - "makefile-tools.command.makefile.preConfigure.title": "Makefile: Pre-Configure", - "makefile-tools.command.makefile.postConfigure.title": "Makefile: Post-Configure", - "makefile-tools.command.makefile.resetState.title": "Makefile: Reset the Makefile Tools Extension workspace state (For troubleshooting)", - "makefile-tools.configuration.views.makefile.outline.description": "Project Outline", - "makefile-tools.configuration.makefile.makePath.description": "The path to the make tool", - "makefile-tools.configuration.makefile.configurations.description": "The user defined makefile configurations", - "makefile-tools.configuration.makefile.configurations.name.description": "The name of the makefile configuration", - "makefile-tools.configuration.makefile.configurations.makefilePath.description": "File path to the makefile", - "makefile-tools.configuration.makefile.configurations.makePath.description": "File path to the make command", - "makefile-tools.configuration.makefile.configurations.makeDirectory.description": "Folder path passed to make via the -C switch", - "makefile-tools.configuration.makefile.configurations.makeArgs.description": "Arguments to pass to the make command", - "makefile-tools.configuration.makefile.configurations.problemMatchers.description": "Problem matcher names to use when building the current target", - "makefile-tools.configuration.makefile.configurations.buildLog.description": "File path to the build log used instead of dry-run output", - "makefile-tools.configuration.makefile.defaultLaunchConfiguration.description": "Various global debugger settings", - "makefile-tools.configuration.makefile.defaultLaunchConfiguration.MIMode.description": "The non VS debugger type: gdb or lldb", - "makefile-tools.configuration.makefile.defaultLaunchConfiguration.miDebuggerPath.description": "Path to the non VS debugger (gdb or lldb)", - "makefile-tools.configuration.makefile.defaultLaunchConfiguration.stopAtEntry.description": "Stop at the entry point of the target", - "makefile-tools.configuration.makefile.defaultLaunchConfiguration.symbolSearchPath.description": "The path to the symbols", - "makefile-tools.configuration.makefile.launchConfigurations.description": "The user defined launch (debug/run) configurations", - "makefile-tools.configuration.makefile.launchConfigurations.binaryPath.description": "The full path to the binary to run or debug", - "makefile-tools.configuration.makefile.launchConfigurations.binaryArgs.description": "Arguments to pass to program command line", - "makefile-tools.configuration.makefile.launchConfigurations.cwd.description": "Set the working directory for the program", - "makefile-tools.configuration.makefile.launchConfigurations.MIMode.description": "The non VS debugger type: gdb or lldb", - "makefile-tools.configuration.makefile.launchConfigurations.miDebuggerPath.description": "Path to the non VS debugger (gdb or lldb)", - "makefile-tools.configuration.makefile.launchConfigurations.stopAtEntry.description": "Stop at the entry point of the target", - "makefile-tools.configuration.makefile.launchConfigurations.symbolSearchPath.description": "The path to the symbols", - "makefile-tools.configuration.makefile.loggingLevel.description": "The logging level for the makefile tools extension", - "makefile-tools.configuration.makefile.makeDirectory.description": "The folder path to be passed to make via the switch -C", - "makefile-tools.configuration.makefile.makefilePath.description": "The path to the makefile of the project", - "makefile-tools.configuration.makefile.buildLog.description": "The path to the build log that is read to bypass a dry-run", - "makefile-tools.configuration.makefile.extensionOutputFolder.description": "The path to various output files produced by the extension. Defaults to the VS Code workspace storage location.", - "makefile-tools.configuration.makefile.extensionLog.description": "The path to an output file storing all content from the Makefile output channel. Defaults to the value of the 'makefile.extensionOutputFolder' setting.", - "makefile-tools.configuration.makefile.configurationCachePath.description": "The path to a cache file storing the output of the last dry-run make command. When unset, a file named 'configurationCache.log' is stored at the path specified by the 'makefile.extensionOutputFolder' setting.", - "makefile-tools.configuration.makefile.dryrunSwitches.description": "Arguments to pass to the dry-run make invocation", - "makefile-tools.configuration.makefile.additionalCompilerNames.description": "Names of compiler tools to be added to the extension known list", - "makefile-tools.configuration.makefile.excludeCompilerNames.description": "Names of compiler tools to be excluded from the extension known list", - "makefile-tools.configuration.makefile.configureOnOpen.description": "Automatically configure Makefile project directories when they are opened", - "makefile-tools.configuration.makefile.configureOnEdit.description": "Automatically configure Makefile project directories when any relevant makefiles and/or settings are changed", - "makefile-tools.configuration.makefile.configureAfterCommand.description": "Automatically configure Makefile project directories after relevant operations, like change build configuration or makefile target", - "makefile-tools.configuration.makefile.preConfigureScript.description": "The path to the script that needs to be run at least once before configure", - "makefile-tools.configuration.makefile.preConfigureArgs.description": "Arguments to pass to the pre-configure script", - "makefile-tools.configuration.makefile.postConfigureScript.description": "The path to the script that needs to be run at least once after configure", - "makefile-tools.configuration.makefile.postConfigureArgs.description": "Arguments to pass to the post-configure script", - "makefile-tools.configuration.makefile.alwaysPreConfigure.description": "Always run the pre-configure script before configure", - "makefile-tools.configuration.makefile.alwaysPostConfigure.description": "Always run the post-configure script after configure", - "makefile-tools.configuration.makefile.ignoreDirectoryCommands.description": "Don't analyze directory changing commands like cd, push, pop.", - "makefile-tools.configuration.makefile.phonyOnlyTargets.description": "Display only the phony targets", - "makefile-tools.configuration.makefile.saveBeforeBuildOrConfigure.description": "Save opened files before building or configuring", - "makefile-tools.configuration.makefile.buildBeforeLaunch.description": "Build the current target before launch (debug/run)", - "makefile-tools.configuration.makefile.clearOutputBeforeBuild.description": "Clear the output channel at the beginning of a build", - "makefile-tools.configuration.makefile.compileCommandsPath.description": "The path to the compilation database file", - "makefile-tools.configuration.makefile.panel.visibility.debug.description": "Enable debugging locally (in this host) images built by this extension", - "makefile-tools.configuration.makefile.panel.visibility.run.description": "Enable running locally (in this host) images built by this extension" + "makefile-tools.command.makefile.buildTarget.title": "Makefile: Build the current target", + "makefile-tools.command.makefile.buildCleanTarget.title": "Makefile: Build clean the current target", + "makefile-tools.command.makefile.buildAll.title": "Makefile: Build the target ALL", + "makefile-tools.command.makefile.buildCleanAll.title": "Makefile: Build clean the target ALL", + "makefile-tools.command.makefile.launchDebug.title": "Makefile: Debug the selected binary target", + "makefile-tools.command.makefile.launchRun.title": "Makefile: Run the selected binary target in the terminal", + "makefile-tools.command.makefile.setBuildConfiguration.title": "Makefile: Set the current build configuration", + "makefile-tools.command.makefile.setBuildTarget.title": "Makefile: Set the target to be built by make", + "makefile-tools.command.makefile.setLaunchConfiguration.title": "Makefile: Set the make launch configuration", + "makefile-tools.command.makefile.openMakefilePathSetting.title": "Makefile: Open Makefile Path Setting", + "makefile-tools.command.makefile.openMakePathSetting.title": "Makefile: Open Make Path Setting", + "makefile-tools.command.makefile.openBuildLogSetting.title": "Makefile: Open Build Log Setting", + "makefile-tools.command.makefile.openBuildLogFile.title": "Makefile: Open Build Log File", + "makefile-tools.command.makefile.openMakefileFile.title": "Makefile: Open Makefile File", + "workbench.action.openWorkspaceSettings.title": "Preferences: Open Workspace Settings", + "makefile-tools.command.makefile.configure.title": "Makefile: Configure", + "makefile-tools.command.makefile.cleanConfigure.title": "Makefile: Clean Configure", + "makefile-tools.command.makefile.preConfigure.title": "Makefile: Pre-Configure", + "makefile-tools.command.makefile.postConfigure.title": "Makefile: Post-Configure", + "makefile-tools.command.makefile.resetState.title": "Makefile: Reset the Makefile Tools Extension workspace state (For troubleshooting)", + "makefile-tools.configuration.views.makefile.outline.description": "Project Outline", + "makefile-tools.configuration.makefile.makePath.description": "The path to the make tool", + "makefile-tools.configuration.makefile.configurations.description": "The user defined makefile configurations", + "makefile-tools.configuration.makefile.configurations.name.description": "The name of the makefile configuration", + "makefile-tools.configuration.makefile.configurations.makefilePath.description": "File path to the makefile", + "makefile-tools.configuration.makefile.configurations.makePath.description": "File path to the make command", + "makefile-tools.configuration.makefile.configurations.makeDirectory.description": "Folder path passed to make via the -C switch", + "makefile-tools.configuration.makefile.configurations.makeArgs.description": "Arguments to pass to the make command", + "makefile-tools.configuration.makefile.configurations.problemMatchers.description": "Problem matcher names to use when building the current target", + "makefile-tools.configuration.makefile.configurations.buildLog.description": "File path to the build log used instead of dry-run output", + "makefile-tools.configuration.makefile.defaultLaunchConfiguration.description": "Various global debugger settings", + "makefile-tools.configuration.makefile.defaultLaunchConfiguration.MIMode.description": "The non VS debugger type: gdb or lldb", + "makefile-tools.configuration.makefile.defaultLaunchConfiguration.miDebuggerPath.description": "Path to the non VS debugger (gdb or lldb)", + "makefile-tools.configuration.makefile.defaultLaunchConfiguration.stopAtEntry.description": "Stop at the entry point of the target", + "makefile-tools.configuration.makefile.defaultLaunchConfiguration.symbolSearchPath.description": "The path to the symbols", + "makefile-tools.configuration.makefile.launchConfigurations.description": "The user defined launch (debug/run) configurations", + "makefile-tools.configuration.makefile.launchConfigurations.binaryPath.description": "The full path to the binary to run or debug", + "makefile-tools.configuration.makefile.launchConfigurations.binaryArgs.description": "Arguments to pass to program command line", + "makefile-tools.configuration.makefile.launchConfigurations.cwd.description": "Set the working directory for the program", + "makefile-tools.configuration.makefile.launchConfigurations.MIMode.description": "The non VS debugger type: gdb or lldb", + "makefile-tools.configuration.makefile.launchConfigurations.miDebuggerPath.description": "Path to the non VS debugger (gdb or lldb)", + "makefile-tools.configuration.makefile.launchConfigurations.stopAtEntry.description": "Stop at the entry point of the target", + "makefile-tools.configuration.makefile.launchConfigurations.symbolSearchPath.description": "The path to the symbols", + "makefile-tools.configuration.makefile.loggingLevel.description": "The logging level for the makefile tools extension", + "makefile-tools.configuration.makefile.makeDirectory.description": "The folder path to be passed to make via the switch -C", + "makefile-tools.configuration.makefile.makefilePath.description": "The path to the makefile of the project", + "makefile-tools.configuration.makefile.buildLog.description": "The path to the build log that is read to bypass a dry-run", + "makefile-tools.configuration.makefile.extensionOutputFolder.description": "The path to various output files produced by the extension. Defaults to the VS Code workspace storage location.", + "makefile-tools.configuration.makefile.extensionLog.description": "The path to an output file storing all content from the Makefile output channel. Defaults to the value of the 'makefile.extensionOutputFolder' setting.", + "makefile-tools.configuration.makefile.configurationCachePath.description": "The path to a cache file storing the output of the last dry-run make command. When unset, a file named 'configurationCache.log' is stored at the path specified by the 'makefile.extensionOutputFolder' setting.", + "makefile-tools.configuration.makefile.dryrunSwitches.description": "Arguments to pass to the dry-run make invocation", + "makefile-tools.configuration.makefile.additionalCompilerNames.description": "Names of compiler tools to be added to the extension known list", + "makefile-tools.configuration.makefile.excludeCompilerNames.description": "Names of compiler tools to be excluded from the extension known list", + "makefile-tools.configuration.makefile.configureOnOpen.description": "Automatically configure Makefile project directories when they are opened", + "makefile-tools.configuration.makefile.configureOnEdit.description": "Automatically configure Makefile project directories when any relevant makefiles and/or settings are changed", + "makefile-tools.configuration.makefile.configureAfterCommand.description": "Automatically configure Makefile project directories after relevant operations, like change build configuration or makefile target", + "makefile-tools.configuration.makefile.preConfigureScript.description": "The path to the script that needs to be run at least once before configure", + "makefile-tools.configuration.makefile.preConfigureArgs.description": "Arguments to pass to the pre-configure script", + "makefile-tools.configuration.makefile.postConfigureScript.description": "The path to the script that needs to be run at least once after configure", + "makefile-tools.configuration.makefile.postConfigureArgs.description": "Arguments to pass to the post-configure script", + "makefile-tools.configuration.makefile.alwaysPreConfigure.description": "Always run the pre-configure script before configure", + "makefile-tools.configuration.makefile.alwaysPostConfigure.description": "Always run the post-configure script after configure", + "makefile-tools.configuration.makefile.ignoreDirectoryCommands.description": "Don't analyze directory changing commands like cd, push, pop.", + "makefile-tools.configuration.makefile.phonyOnlyTargets.description": "Display only the phony targets", + "makefile-tools.configuration.makefile.saveBeforeBuildOrConfigure.description": "Save opened files before building or configuring", + "makefile-tools.configuration.makefile.buildBeforeLaunch.description": "Build the current target before launch (debug/run)", + "makefile-tools.configuration.makefile.clearOutputBeforeBuild.description": "Clear the output channel at the beginning of a build", + "makefile-tools.configuration.makefile.compileCommandsPath.description": "The path to the compilation database file", + "makefile-tools.configuration.makefile.panel.visibility.debug.description": "Enable debugging locally (in this host) images built by this extension", + "makefile-tools.configuration.makefile.panel.visibility.run.description": "Enable running locally (in this host) images built by this extension" } diff --git a/src/configuration.ts b/src/configuration.ts index 2424c40..2dff02f 100644 --- a/src/configuration.ts +++ b/src/configuration.ts @@ -3,17 +3,20 @@ // Configuration support -import {extension} from './extension'; -import * as logger from './logger'; -import * as make from './make'; -import * as ui from './ui'; -import * as util from './util'; -import * as vscode from 'vscode'; -import * as path from 'path'; -import * as telemetry from './telemetry'; +import { extension } from "./extension"; +import * as logger from "./logger"; +import * as make from "./make"; +import * as ui from "./ui"; +import * as util from "./util"; +import * as vscode from "vscode"; +import * as path from "path"; +import * as telemetry from "./telemetry"; -import * as nls from 'vscode-nls'; -nls.config({ messageFormat: nls.MessageFormat.bundle, bundleFormat: nls.BundleFormat.standalone })(); +import * as nls from "vscode-nls"; +nls.config({ + messageFormat: nls.MessageFormat.bundle, + bundleFormat: nls.BundleFormat.standalone, +})(); const localize: nls.LocalizeFunc = nls.loadMessageBundle(); let statusBar: ui.UI = ui.getUI(); @@ -31,81 +34,90 @@ let statusBar: ui.UI = ui.getUI(); // 5. default make tool and args export interface MakefileConfiguration { - // A name associated with a particular build command process and args/options - name: string; + // A name associated with a particular build command process and args/options + name: string; - // The path (full or relative to the workspace folder) to the makefile - makefilePath?: string; + // The path (full or relative to the workspace folder) to the makefile + makefilePath?: string; - // make, nmake, specmake... - // This is sent to spawnChildProcess as process name - // It can have full path, relative path to the workspace folder or only tool name - // Don't include args in makePath - makePath?: string; + // make, nmake, specmake... + // This is sent to spawnChildProcess as process name + // It can have full path, relative path to the workspace folder or only tool name + // Don't include args in makePath + makePath?: string; - // a folder path (full or relative) containing the entrypoint makefile - makeDirectory?: string; + // a folder path (full or relative) containing the entrypoint makefile + makeDirectory?: string; - // options used in the build invocation - // don't use more than one argument in a string - makeArgs?: string[]; + // options used in the build invocation + // don't use more than one argument in a string + makeArgs?: string[]; - // list of problem matcher names to be used when building the current target - problemMatchers?: string[]; + // list of problem matcher names to be used when building the current target + problemMatchers?: string[]; - // a pre-generated build log, from which it is preffered to parse from, - // instead of the dry-run output of the make tool - buildLog?: string; + // a pre-generated build log, from which it is preffered to parse from, + // instead of the dry-run output of the make tool + buildLog?: string; - // arguments for the pre configure script - preConfigureArgs?: string[]; + // arguments for the pre configure script + preConfigureArgs?: string[]; - // arguments for the post configure script - postConfigureArgs?: string[] + // arguments for the post configure script + postConfigureArgs?: string[]; - // TODO: investigate how flexible this is to integrate with other build systems than the MAKE family - // (basically anything that can produce a dry-run output is sufficient) - // Implement set-able dry-run, verbose, change-directory and always-make switches - // since different tools may use different arguments for the same behavior + // TODO: investigate how flexible this is to integrate with other build systems than the MAKE family + // (basically anything that can produce a dry-run output is sufficient) + // Implement set-able dry-run, verbose, change-directory and always-make switches + // since different tools may use different arguments for the same behavior } // Last configuration name picked from the set defined in makefile.configurations setting. // Saved into the workspace state. Also reflected in the configuration status bar button. // If no particular current configuration is defined in settings, set to 'Default'. let currentMakefileConfiguration: string; -export function getCurrentMakefileConfiguration(): string { return currentMakefileConfiguration; } -export async function setCurrentMakefileConfiguration(configuration: string): Promise { - currentMakefileConfiguration = configuration; - statusBar.setConfiguration(currentMakefileConfiguration); - await analyzeConfigureParams(); +export function getCurrentMakefileConfiguration(): string { + return currentMakefileConfiguration; +} +export async function setCurrentMakefileConfiguration( + configuration: string +): Promise { + currentMakefileConfiguration = configuration; + statusBar.setConfiguration(currentMakefileConfiguration); + await analyzeConfigureParams(); } // Read the current configuration from workspace state, update status bar item export function readCurrentMakefileConfiguration(): void { - let buildConfiguration : string | undefined = extension.getState().buildConfiguration; - if (!buildConfiguration) { - logger.message("No current configuration is defined in the workspace state. Assuming 'Default'."); - currentMakefileConfiguration = "Default"; - } else { - logger.message(`Reading current configuration "${buildConfiguration}" from the workspace state.`); - currentMakefileConfiguration = buildConfiguration; - } + let buildConfiguration: string | undefined = + extension.getState().buildConfiguration; + if (!buildConfiguration) { + logger.message( + "No current configuration is defined in the workspace state. Assuming 'Default'." + ); + currentMakefileConfiguration = "Default"; + } else { + logger.message( + `Reading current configuration "${buildConfiguration}" from the workspace state.` + ); + currentMakefileConfiguration = buildConfiguration; + } - statusBar.setConfiguration(currentMakefileConfiguration); + statusBar.setConfiguration(currentMakefileConfiguration); } // as described in makefile.panel.visibility type MakefilePanelVisibility = { - debug: boolean; - run: boolean; + debug: boolean; + run: boolean; }; // internal, runtime representation of an optional feature type MakefilePanelVisibilityDescription = { - propertyName: string; - default: boolean; - value: boolean; - enablement?: string; + propertyName: string; + default: boolean; + value: boolean; + enablement?: string; }; // To add an optional feature (one that can be enabled/disabled based @@ -116,120 +128,151 @@ type MakefilePanelVisibilityDescription = { // * make sure enablement is handled in package.json, you are done // * if not, then add code to check Feature state wherever is needed. class MakefilePanelVisibilityDescriptions { - features: MakefilePanelVisibilityDescription[] = [ - { propertyName: "debug", enablement: "makefile:localDebugFeature", default: true, value: false }, - { propertyName: "run", enablement: "makefile:localRunFeature", default: true, value: false } - ]; + features: MakefilePanelVisibilityDescription[] = [ + { + propertyName: "debug", + enablement: "makefile:localDebugFeature", + default: true, + value: false, + }, + { + propertyName: "run", + enablement: "makefile:localRunFeature", + default: true, + value: false, + }, + ]; } -let panelVisibility: MakefilePanelVisibilityDescriptions = new MakefilePanelVisibilityDescriptions(); +let panelVisibility: MakefilePanelVisibilityDescriptions = + new MakefilePanelVisibilityDescriptions(); // Set all features to their defaults (enabled or disabled) function initOptionalFeatures(): void { - for (let feature of panelVisibility.features) { - feature.value = feature.default; - } + for (let feature of panelVisibility.features) { + feature.value = feature.default; + } } export function isOptionalFeatureEnabled(propertyName: string): boolean { - for (let feature of panelVisibility.features) { - if (feature.propertyName === propertyName) { - return feature.value; - } + for (let feature of panelVisibility.features) { + if (feature.propertyName === propertyName) { + return feature.value; } - return false; + } + return false; } // Override default settings for each feature based on workspace current information async function updateOptionalFeaturesWithWorkspace(): Promise { - // optionalFeatures will be set with default values. - // override with values from the workspace - let features: MakefilePanelVisibility | undefined = await util.getExpandedSetting("panel.visibility") || undefined; - if (features) { - if (Object.entries(features).length < panelVisibility.features.length) { - // At least one feature is missing from the settings, which means we need to use defaults. - // If we don't refresh defaults here, we won't cover the following scenario: - // - default TRUE feature - // - which was set to false in the settings, causing knownFeature.value to be false - // - just got removed from settings now, meaning it won't be included in the features varibale and the FOR won't loop through it - // giving it no opportunity to switch .value back to the default of TRUE. - initOptionalFeatures(); - } - for (let propEntry of Object.entries(features)) { - for (let knownFeature of panelVisibility.features) { - if (propEntry[0] === knownFeature.propertyName) { - knownFeature.value = propEntry[1]; - } - } - } - } else { - initOptionalFeatures(); // no info in workspace, use defaults + // optionalFeatures will be set with default values. + // override with values from the workspace + let features: MakefilePanelVisibility | undefined = + (await util.getExpandedSetting( + "panel.visibility" + )) || undefined; + if (features) { + if (Object.entries(features).length < panelVisibility.features.length) { + // At least one feature is missing from the settings, which means we need to use defaults. + // If we don't refresh defaults here, we won't cover the following scenario: + // - default TRUE feature + // - which was set to false in the settings, causing knownFeature.value to be false + // - just got removed from settings now, meaning it won't be included in the features varibale and the FOR won't loop through it + // giving it no opportunity to switch .value back to the default of TRUE. + initOptionalFeatures(); } + for (let propEntry of Object.entries(features)) { + for (let knownFeature of panelVisibility.features) { + if (propEntry[0] === knownFeature.propertyName) { + knownFeature.value = propEntry[1]; + } + } + } + } else { + initOptionalFeatures(); // no info in workspace, use defaults + } } export function disableAllOptionallyVisibleCommands(): void { - for (let feature of panelVisibility.features) { - if (feature.enablement) { - vscode.commands.executeCommand('setContext', feature.enablement, false); - } + for (let feature of panelVisibility.features) { + if (feature.enablement) { + vscode.commands.executeCommand("setContext", feature.enablement, false); } - + } } function enableOptionallyVisibleCommands(): void { - for (let feature of panelVisibility.features) { - if (feature.enablement) { - vscode.commands.executeCommand('setContext', feature.enablement, feature.value); - } + for (let feature of panelVisibility.features) { + if (feature.enablement) { + vscode.commands.executeCommand( + "setContext", + feature.enablement, + feature.value + ); } + } } async function readFeaturesVisibility(): Promise { - await updateOptionalFeaturesWithWorkspace(); + await updateOptionalFeaturesWithWorkspace(); } let makePath: string | undefined; -export function getMakePath(): string | undefined { return makePath; } -export function setMakePath(path: string): void { makePath = path; } +export function getMakePath(): string | undefined { + return makePath; +} +export function setMakePath(path: string): void { + makePath = path; +} // Read the path (full or directory only) of the make tool if defined in settings. // It represents a default to look for if no other path is already included // in "makefile.configurations.makePath". async function readMakePath(): Promise { - makePath = await util.getExpandedSetting("makePath"); - if (!makePath) { - logger.message("No path to the make tool is defined in the settings file."); - } + makePath = await util.getExpandedSetting("makePath"); + if (!makePath) { + logger.message("No path to the make tool is defined in the settings file."); + } } let makefilePath: string | undefined; -export function getMakefilePath(): string | undefined { return makefilePath; } -export function setMakefilePath(path: string): void { makefilePath = path; } +export function getMakefilePath(): string | undefined { + return makefilePath; +} +export function setMakefilePath(path: string): void { + makefilePath = path; +} // Read the full path to the makefile if defined in settings. // It represents a default to look for if no other makefile is already provided // in makefile.configurations.makefilePath. // TODO: validate and integrate with "-f [Makefile]" passed in makefile.configurations.makeArgs. async function readMakefilePath(): Promise { - makefilePath = await util.getExpandedSetting("makefilePath"); - if (!makefilePath) { - logger.message("No path to the makefile is defined in the settings file."); - } else { - makefilePath = util.resolvePathToRoot(makefilePath); - } + makefilePath = await util.getExpandedSetting("makefilePath"); + if (!makefilePath) { + logger.message("No path to the makefile is defined in the settings file."); + } else { + makefilePath = util.resolvePathToRoot(makefilePath); + } } let makeDirectory: string | undefined; -export function getMakeDirectory(): string | undefined { return makeDirectory; } -export function setMakeDirectory(dir: string): void { makeDirectory = dir; } +export function getMakeDirectory(): string | undefined { + return makeDirectory; +} +export function setMakeDirectory(dir: string): void { + makeDirectory = dir; +} // Read the make working directory path if defined in settings. // It represents a default to look for if no other makeDirectory is already provided // in makefile.configurations.makeDirectory. // TODO: validate and integrate with "-C [DIR_PATH]" passed in makefile.configurations.makeArgs. async function readMakeDirectory(): Promise { - makeDirectory = await util.getExpandedSetting("makeDirectory"); - if (!makeDirectory) { - logger.message("No folder path to the makefile is defined in the settings file."); - } else { - makeDirectory = util.resolvePathToRoot(makeDirectory); - } + makeDirectory = await util.getExpandedSetting("makeDirectory"); + if (!makeDirectory) { + logger.message( + "No folder path to the makefile is defined in the settings file." + ); + } else { + makeDirectory = util.resolvePathToRoot(makeDirectory); + } } // Command property accessible from launch.json: @@ -237,31 +280,35 @@ async function readMakeDirectory(): Promise { // passed with -C (otherwise it is the workspace folder). // Note: -f does not change the current working directory. It only points to a makefile somewhere else. export function makeBaseDirectory(): string { - // In case more than one -C arguments are given to "make", it will chose the last one. - // getConfigurationMakeArgs will contain the final command we calculate for the "make" executable. - // We don't need to know here which -C gets pushed last (global makeDirectory, - // configuration local makeDirectory or one in makeArgs). Just reverse to easily get the last one. - const makeArgs: string[] = getConfigurationMakeArgs().concat().reverse(); - let prevArg: string = ""; - for (const arg of makeArgs) { - if (arg === "-C") { - return prevArg; - } else if (arg.startsWith("--directory")) { - const eqIdx: number = arg.indexOf("="); - return arg.substring(eqIdx + 1, arg.length); - } - - // Since we reversed the "make" command line arguments, the path of a -C will be seen before the switch. - // Remember every previous argument to have it available in case we find the first -C. - prevArg = arg; + // In case more than one -C arguments are given to "make", it will chose the last one. + // getConfigurationMakeArgs will contain the final command we calculate for the "make" executable. + // We don't need to know here which -C gets pushed last (global makeDirectory, + // configuration local makeDirectory or one in makeArgs). Just reverse to easily get the last one. + const makeArgs: string[] = getConfigurationMakeArgs().concat().reverse(); + let prevArg: string = ""; + for (const arg of makeArgs) { + if (arg === "-C") { + return prevArg; + } else if (arg.startsWith("--directory")) { + const eqIdx: number = arg.indexOf("="); + return arg.substring(eqIdx + 1, arg.length); } - return util.getWorkspaceRoot(); + // Since we reversed the "make" command line arguments, the path of a -C will be seen before the switch. + // Remember every previous argument to have it available in case we find the first -C. + prevArg = arg; + } + + return util.getWorkspaceRoot(); } let buildLog: string | undefined; -export function getBuildLog(): string | undefined { return buildLog; } -export function setBuildLog(path: string): void { buildLog = path; } +export function getBuildLog(): string | undefined { + return buildLog; +} +export function setBuildLog(path: string): void { + buildLog = path; +} // Read from settings the path of the build log that is desired to be parsed // instead of a dry-run command output. @@ -283,60 +330,81 @@ export function setBuildLog(path: string): void { buildLog = path; } // therefore offering less intellisense information for source files, // identifying less possible binaries to debug or not providing any makefile targets (other than the 'all' default). export async function readBuildLog(): Promise { - buildLog = await util.getExpandedSetting("buildLog"); - if (buildLog) { - buildLog = util.resolvePathToRoot(buildLog); - logger.message(`Build log defined at "${buildLog}"`); - if (!util.checkFileExistsSync(buildLog)) { - logger.message("Build log not found on disk."); - return false; - } + buildLog = await util.getExpandedSetting("buildLog"); + if (buildLog) { + buildLog = util.resolvePathToRoot(buildLog); + logger.message(`Build log defined at "${buildLog}"`); + if (!util.checkFileExistsSync(buildLog)) { + logger.message("Build log not found on disk."); + return false; } - return true; + } + return true; } let loggingLevel: string | undefined; -export function getLoggingLevel(): string | undefined { return loggingLevel; } -export function setLoggingLevel(logLevel: string): void { loggingLevel = logLevel; } +export function getLoggingLevel(): string | undefined { + return loggingLevel; +} +export function setLoggingLevel(logLevel: string): void { + loggingLevel = logLevel; +} // Read from settings the desired logging level for the Makefile Tools extension. export async function readLoggingLevel(): Promise { - loggingLevel = await util.getExpandedSetting("loggingLevel") || "Normal"; - logger.message(`Logging level: ${loggingLevel}`); + loggingLevel = + (await util.getExpandedSetting("loggingLevel")) || "Normal"; + logger.message(`Logging level: ${loggingLevel}`); } let extensionOutputFolder: string | undefined; -export function getExtensionOutputFolder(): string | undefined { return extensionOutputFolder; } -export function setExtensionOutputFolder(folder: string): void { extensionOutputFolder = folder; } +export function getExtensionOutputFolder(): string | undefined { + return extensionOutputFolder; +} +export function setExtensionOutputFolder(folder: string): void { + extensionOutputFolder = folder; +} // Read from settings the path to a folder where the extension is dropping various output files // (like extension.log, dry-run.log, targets.log). // Useful to control where such potentially large files should reside. export async function readExtensionOutputFolder(): Promise { - extensionOutputFolder = await util.getExpandedSetting("extensionOutputFolder"); - if (extensionOutputFolder) { - extensionOutputFolder = util.resolvePathToRoot(extensionOutputFolder); - } else { - extensionOutputFolder = extension.extensionContext.storagePath; - } + extensionOutputFolder = await util.getExpandedSetting( + "extensionOutputFolder" + ); + if (extensionOutputFolder) { + extensionOutputFolder = util.resolvePathToRoot(extensionOutputFolder); + } else { + extensionOutputFolder = extension.extensionContext.storagePath; + } - // Check one more time because the value can still be undefined if no folder was opened. - if (extensionOutputFolder) { - if (!util.checkDirectoryExistsSync(extensionOutputFolder)) { - if (!util.createDirectorySync(extensionOutputFolder)) { - extensionOutputFolder = extension.extensionContext.storagePath; - logger.message(`Extension output folder does not exist and could not be created: ${extensionOutputFolder}.`); - logger.message(`Reverting to '${extensionOutputFolder}' default for extension output folder.`); - return; - } - } - logger.message(`Dropping various extension output files at ${extensionOutputFolder}`); + // Check one more time because the value can still be undefined if no folder was opened. + if (extensionOutputFolder) { + if (!util.checkDirectoryExistsSync(extensionOutputFolder)) { + if (!util.createDirectorySync(extensionOutputFolder)) { + extensionOutputFolder = extension.extensionContext.storagePath; + logger.message( + `Extension output folder does not exist and could not be created: ${extensionOutputFolder}.` + ); + logger.message( + `Reverting to '${extensionOutputFolder}' default for extension output folder.` + ); + return; + } } + logger.message( + `Dropping various extension output files at ${extensionOutputFolder}` + ); + } } let extensionLog: string | undefined; -export function getExtensionLog(): string | undefined { return extensionLog; } -export function setExtensionLog(path: string): void { extensionLog = path; } +export function getExtensionLog(): string | undefined { + return extensionLog; +} +export function setExtensionLog(path: string): void { + extensionLog = path; +} // Read from settings the path to a log file capturing all the "Makefile Tools" output channel content. // Useful for very large repos, which would produce with a single command a log larger @@ -347,160 +415,240 @@ export function setExtensionLog(path: string): void { extensionLog = path; } // Any messages that are being logged throughout the lifetime of the extension // are going to be appended to this file. export async function readExtensionLog(): Promise { - extensionLog = await util.getExpandedSetting("extensionLog"); - if (extensionLog) { - // If there is a directory defined within the extension log path, - // honor it and don't append to extensionOutputFolder. - let parsePath: path.ParsedPath = path.parse(extensionLog); - if (extensionOutputFolder && !parsePath.dir) { - extensionLog = path.join(extensionOutputFolder, extensionLog); - } else { - extensionLog = util.resolvePathToRoot(extensionLog); - } - - logger.message(`Writing extension log at ${extensionLog}`); + extensionLog = await util.getExpandedSetting("extensionLog"); + if (extensionLog) { + // If there is a directory defined within the extension log path, + // honor it and don't append to extensionOutputFolder. + let parsePath: path.ParsedPath = path.parse(extensionLog); + if (extensionOutputFolder && !parsePath.dir) { + extensionLog = path.join(extensionOutputFolder, extensionLog); + } else { + extensionLog = util.resolvePathToRoot(extensionLog); } + + logger.message(`Writing extension log at ${extensionLog}`); + } } let preConfigureScript: string | undefined; -export function getPreConfigureScript(): string | undefined { return preConfigureScript; } -export function setPreConfigureScript(path: string): void { preConfigureScript = path; } +export function getPreConfigureScript(): string | undefined { + return preConfigureScript; +} +export function setPreConfigureScript(path: string): void { + preConfigureScript = path; +} let preConfigureArgs: string[] = []; -export function getPreConfigureArgs(): string[] { return preConfigureArgs; } -export function setPreConfigureArgs(args: string[]): void { preConfigureArgs = args; } +export function getPreConfigureArgs(): string[] { + return preConfigureArgs; +} +export function setPreConfigureArgs(args: string[]): void { + preConfigureArgs = args; +} let postConfigureScript: string | undefined; -export function getPostConfigureScript(): string | undefined { return postConfigureScript; } -export function setPostConfigureScript(path: string): void { postConfigureScript = path; } +export function getPostConfigureScript(): string | undefined { + return postConfigureScript; +} +export function setPostConfigureScript(path: string): void { + postConfigureScript = path; +} let postConfigureArgs: string[] = []; -export function getPostConfigureArgs(): string[] { return postConfigureArgs; } -export function setPostConfigureArgs(args: string[]): void { postConfigureArgs = args; } +export function getPostConfigureArgs(): string[] { + return postConfigureArgs; +} +export function setPostConfigureArgs(args: string[]): void { + postConfigureArgs = args; +} // Read from settings the path to a script file that needs to have been run at least once // before a sucessful configure of this project. export async function readPreConfigureScript(): Promise { - preConfigureScript = await util.getExpandedSetting("preConfigureScript"); - if (preConfigureScript) { - preConfigureScript = util.resolvePathToRoot(preConfigureScript); - logger.message(`Found pre-configure script defined as ${preConfigureScript}`); - if (!util.checkFileExistsSync(preConfigureScript)) { - logger.message("Pre-configure script not found on disk."); - } + preConfigureScript = await util.getExpandedSetting( + "preConfigureScript" + ); + if (preConfigureScript) { + preConfigureScript = util.resolvePathToRoot(preConfigureScript); + logger.message( + `Found pre-configure script defined as ${preConfigureScript}` + ); + if (!util.checkFileExistsSync(preConfigureScript)) { + logger.message("Pre-configure script not found on disk."); } + } } export async function readPreConfigureArgs(): Promise { - preConfigureArgs = await util.getExpandedSetting("preConfigureArgs") ?? []; - if (preConfigureArgs && preConfigureArgs.length > 0) { - logger.message(`Pre-configure arguments: '${preConfigureArgs.join("', '")}'`); - } + preConfigureArgs = + (await util.getExpandedSetting("preConfigureArgs")) ?? []; + if (preConfigureArgs && preConfigureArgs.length > 0) { + logger.message( + `Pre-configure arguments: '${preConfigureArgs.join("', '")}'` + ); + } } export async function readPostConfigureScript(): Promise { - postConfigureScript = await util.getExpandedSetting("postConfigureScript"); - if (postConfigureScript) { - postConfigureScript = util.resolvePathToRoot(postConfigureScript); - logger.message(`Found post-configure script defined as ${postConfigureScript}`); - if (!util.checkFileExistsSync(postConfigureScript)) { - logger.message("Post-configure script not found on disk"); - } + postConfigureScript = await util.getExpandedSetting( + "postConfigureScript" + ); + if (postConfigureScript) { + postConfigureScript = util.resolvePathToRoot(postConfigureScript); + logger.message( + `Found post-configure script defined as ${postConfigureScript}` + ); + if (!util.checkFileExistsSync(postConfigureScript)) { + logger.message("Post-configure script not found on disk"); } + } } export async function readPostConfigureArgs(): Promise { - postConfigureArgs = await util.getExpandedSetting("postConfigureArgs") ?? []; - if (postConfigureArgs && postConfigureArgs.length > 0) { - logger.message(`Post-configure arguments: '${postConfigureArgs.join("', '")}'`); - } + postConfigureArgs = + (await util.getExpandedSetting("postConfigureArgs")) ?? []; + if (postConfigureArgs && postConfigureArgs.length > 0) { + logger.message( + `Post-configure arguments: '${postConfigureArgs.join("', '")}'` + ); + } } let alwaysPreConfigure: boolean | undefined; -export function getAlwaysPreConfigure(): boolean | undefined { return alwaysPreConfigure; } -export function setAlwaysPreConfigure(path: boolean): void { alwaysPreConfigure = path; } +export function getAlwaysPreConfigure(): boolean | undefined { + return alwaysPreConfigure; +} +export function setAlwaysPreConfigure(path: boolean): void { + alwaysPreConfigure = path; +} let alwaysPostConfigure: boolean | undefined; -export function getAlwaysPostConfigure(): boolean | undefined { return alwaysPostConfigure; } -export function setAlwaysPostConfigure(path: boolean): void { alwaysPostConfigure = path; } +export function getAlwaysPostConfigure(): boolean | undefined { + return alwaysPostConfigure; +} +export function setAlwaysPostConfigure(path: boolean): void { + alwaysPostConfigure = path; +} // Read from settings whether the pre-configure step is supposed to be executed // always before the configure operation. export async function readAlwaysPreConfigure(): Promise { - alwaysPreConfigure = await util.getExpandedSetting("alwaysPreConfigure"); - logger.message(`Always pre-configure: ${alwaysPreConfigure}`); + alwaysPreConfigure = await util.getExpandedSetting( + "alwaysPreConfigure" + ); + logger.message(`Always pre-configure: ${alwaysPreConfigure}`); } export async function readAlwaysPostConfigure(): Promise { - alwaysPostConfigure = await util.getExpandedSetting("alwaysPostConfigure"); - logger.message(`Always post-configure: ${alwaysPostConfigure}`); + alwaysPostConfigure = await util.getExpandedSetting( + "alwaysPostConfigure" + ); + logger.message(`Always post-configure: ${alwaysPostConfigure}`); } let configurationCachePath: string | undefined; -export function getConfigurationCachePath(): string | undefined { return configurationCachePath; } -export function setConfigurationCachePath(path: string): void { configurationCachePath = path; } +export function getConfigurationCachePath(): string | undefined { + return configurationCachePath; +} +export function setConfigurationCachePath(path: string): void { + configurationCachePath = path; +} // Read from settings the path to a cache file containing the output of the last dry-run make command. // This file is recreated when opening a project, when changing the build configuration or the build target // and when the settings watcher detects a change of any properties that may impact the dryrun output. export async function readConfigurationCachePath(): Promise { - let oldConfigurationCachePath = configurationCachePath; - configurationCachePath = await util.getExpandedSetting("configurationCachePath"); - if (!configurationCachePath && extensionOutputFolder) { - configurationCachePath = path.join(extensionOutputFolder, 'configurationCache.log'); + let oldConfigurationCachePath = configurationCachePath; + configurationCachePath = await util.getExpandedSetting( + "configurationCachePath" + ); + if (!configurationCachePath && extensionOutputFolder) { + configurationCachePath = path.join( + extensionOutputFolder, + "configurationCache.log" + ); + } + + if (configurationCachePath) { + // If there is a directory defined within the configuration cache path, + // honor it and don't append to extensionOutputFolder. + let parsePath: path.ParsedPath = path.parse(configurationCachePath); + if (extensionOutputFolder && !parsePath.dir) { + configurationCachePath = path.join( + extensionOutputFolder, + configurationCachePath + ); + } else { + configurationCachePath = util.resolvePathToRoot(configurationCachePath); } - if (configurationCachePath) { - // If there is a directory defined within the configuration cache path, - // honor it and don't append to extensionOutputFolder. - let parsePath: path.ParsedPath = path.parse(configurationCachePath); - if (extensionOutputFolder && !parsePath.dir) { - configurationCachePath = path.join(extensionOutputFolder, configurationCachePath); - } else { - configurationCachePath = util.resolvePathToRoot(configurationCachePath); - } - - if (oldConfigurationCachePath !== configurationCachePath) { - logger.message(`Configurations cached at ${configurationCachePath}`); - } + if (oldConfigurationCachePath !== configurationCachePath) { + logger.message(`Configurations cached at ${configurationCachePath}`); } + } } let compileCommandsPath: string | undefined; -export function getCompileCommandsPath(): string | undefined { return compileCommandsPath; } -export function setCompileCommandsPath(path: string): void { compileCommandsPath = path; } +export function getCompileCommandsPath(): string | undefined { + return compileCommandsPath; +} +export function setCompileCommandsPath(path: string): void { + compileCommandsPath = path; +} export async function readCompileCommandsPath(): Promise { - compileCommandsPath = await util.getExpandedSetting("compileCommandsPath"); - if (compileCommandsPath) { - compileCommandsPath = util.resolvePathToRoot(compileCommandsPath); - } + compileCommandsPath = await util.getExpandedSetting( + "compileCommandsPath" + ); + if (compileCommandsPath) { + compileCommandsPath = util.resolvePathToRoot(compileCommandsPath); + } - logger.message(`compile_commands.json path: ${compileCommandsPath}`); + logger.message(`compile_commands.json path: ${compileCommandsPath}`); } let additionalCompilerNames: string[] | undefined; -export function getAdditionalCompilerNames(): string[] | undefined { return additionalCompilerNames; } -export function setAdditionalCompilerNames(compilerNames: string[]): void { additionalCompilerNames = compilerNames; } +export function getAdditionalCompilerNames(): string[] | undefined { + return additionalCompilerNames; +} +export function setAdditionalCompilerNames(compilerNames: string[]): void { + additionalCompilerNames = compilerNames; +} export async function readAdditionalCompilerNames(): Promise { - additionalCompilerNames = await util.getExpandedSetting("additionalCompilerNames"); - if (additionalCompilerNames && additionalCompilerNames.length > 0) { - logger.message(`Additional compiler names: '${additionalCompilerNames?.join("', '")}'`); - } + additionalCompilerNames = await util.getExpandedSetting( + "additionalCompilerNames" + ); + if (additionalCompilerNames && additionalCompilerNames.length > 0) { + logger.message( + `Additional compiler names: '${additionalCompilerNames?.join("', '")}'` + ); + } } let excludeCompilerNames: string[] | undefined; -export function getExcludeCompilerNames(): string[] | undefined { return excludeCompilerNames; } -export function setExcludeCompilerNames(compilerNames: string[]): void { excludeCompilerNames = compilerNames; } +export function getExcludeCompilerNames(): string[] | undefined { + return excludeCompilerNames; +} +export function setExcludeCompilerNames(compilerNames: string[]): void { + excludeCompilerNames = compilerNames; +} export async function readExcludeCompilerNames(): Promise { - excludeCompilerNames = await util.getExpandedSetting("excludeCompilerNames"); - if (excludeCompilerNames && excludeCompilerNames.length > 0) { - logger.message(`Exclude compiler names: '${excludeCompilerNames?.join("', '")}'`); - } + excludeCompilerNames = await util.getExpandedSetting( + "excludeCompilerNames" + ); + if (excludeCompilerNames && excludeCompilerNames.length > 0) { + logger.message( + `Exclude compiler names: '${excludeCompilerNames?.join("', '")}'` + ); + } } let dryrunSwitches: string[] | undefined; -export function getDryrunSwitches(): string[] | undefined { return dryrunSwitches; } -export function setDryrunSwitches(switches: string[]): void { dryrunSwitches = switches; } +export function getDryrunSwitches(): string[] | undefined { + return dryrunSwitches; +} +export function setDryrunSwitches(switches: string[]): void { + dryrunSwitches = switches; +} // Read from settings the dry-run switches array. If there is no user definition, the defaults are: // --always-make: to not skip over up-to-date targets @@ -511,39 +659,48 @@ export function setDryrunSwitches(switches: string[]): void { dryrunSwitches = s // To work around this, the setting makefile.dryrunSwitches is providing a way to skip over the problematic make arguments, // even if this results in not ideal behavior: less information available to be parsed, which leads to incomplete IntelliSense or missing targets. export async function readDryrunSwitches(): Promise { - dryrunSwitches = await util.getExpandedSetting("dryrunSwitches"); - if (dryrunSwitches && dryrunSwitches.length > 0) { - logger.message(`Dry-run switches: '${dryrunSwitches?.join("', '")}'`); - } + dryrunSwitches = await util.getExpandedSetting("dryrunSwitches"); + if (dryrunSwitches && dryrunSwitches.length > 0) { + logger.message(`Dry-run switches: '${dryrunSwitches?.join("', '")}'`); + } } // Currently, the makefile extension supports debugging only an executable. // TODO: Parse for symbol search paths // TODO: support dll debugging. export interface LaunchConfiguration { - // The following properties constitute a minimal launch configuration object. - // They all can be deduced from the dry-run output or build log. - // When the user is selecting a launch configuration, the extension is verifying - // whether there is an entry in the launch configurations array in settings - // and if not, it is generating a new one with the values computed by the parser. - binaryPath: string; // full path to the binary that this launch configuration is tied to - binaryArgs: string[]; // arguments that this binary is called with for this launch configuration - cwd: string; // folder from where the binary is run + // The following properties constitute a minimal launch configuration object. + // They all can be deduced from the dry-run output or build log. + // When the user is selecting a launch configuration, the extension is verifying + // whether there is an entry in the launch configurations array in settings + // and if not, it is generating a new one with the values computed by the parser. + binaryPath: string; // full path to the binary that this launch configuration is tied to + binaryArgs: string[]; // arguments that this binary is called with for this launch configuration + cwd: string; // folder from where the binary is run - // The following represent optional properties that can be additionally defined by the user in settings. - MIMode?: string; - miDebuggerPath?: string; - stopAtEntry?: boolean; - symbolSearchPath?: string; + // The following represent optional properties that can be additionally defined by the user in settings. + MIMode?: string; + miDebuggerPath?: string; + stopAtEntry?: boolean; + symbolSearchPath?: string; } let launchConfigurations: LaunchConfiguration[] = []; -export function getLaunchConfigurations(): LaunchConfiguration[] { return launchConfigurations; } -export function setLaunchConfigurations(configurations: LaunchConfiguration[]): void { launchConfigurations = configurations; } +export function getLaunchConfigurations(): LaunchConfiguration[] { + return launchConfigurations; +} +export function setLaunchConfigurations( + configurations: LaunchConfiguration[] +): void { + launchConfigurations = configurations; +} // Read launch configurations defined by the user in settings: makefile.launchConfigurations[] async function readLaunchConfigurations(): Promise { - launchConfigurations = await util.getExpandedSetting("launchConfigurations") || []; + launchConfigurations = + (await util.getExpandedSetting( + "launchConfigurations" + )) || []; } // Helper used to fill the launch configurations quick pick. @@ -556,48 +713,63 @@ async function readLaunchConfigurations(): Promise { // for the strings being used to populate the quick pick. // Syntax: // [CWD path]>[binaryPath]([binaryArg1,binaryArg2,binaryArg3,...]) -export function launchConfigurationToString(configuration: LaunchConfiguration): string { - let binPath: string = util.makeRelPath(configuration.binaryPath, configuration.cwd); - let binArgs: string = configuration.binaryArgs.join(","); - return `${configuration.cwd}>${binPath}(${binArgs})`; +export function launchConfigurationToString( + configuration: LaunchConfiguration +): string { + let binPath: string = util.makeRelPath( + configuration.binaryPath, + configuration.cwd + ); + let binArgs: string = configuration.binaryArgs.join(","); + return `${configuration.cwd}>${binPath}(${binArgs})`; } // Helper used to construct a minimal launch configuration object // (only cwd, binary path and arguments) from a string that respects // the syntax of its quick pick. -export async function stringToLaunchConfiguration(str: string): Promise { - let regexp: RegExp = /(.*)\>(.*)\((.*)\)/mg; - let match: RegExpExecArray | null = regexp.exec(str); +export async function stringToLaunchConfiguration( + str: string +): Promise { + let regexp: RegExp = /(.*)\>(.*)\((.*)\)/gm; + let match: RegExpExecArray | null = regexp.exec(str); - if (match) { - let fullPath: string = await util.makeFullPath(match[2], match[1]); - let splitArgs: string[] = (match[3] === "") ? [] : match[3].split(","); + if (match) { + let fullPath: string = await util.makeFullPath(match[2], match[1]); + let splitArgs: string[] = match[3] === "" ? [] : match[3].split(","); - return { - cwd: match[1], - binaryPath: fullPath, - binaryArgs: splitArgs - }; - } else { - return undefined; - } + return { + cwd: match[1], + binaryPath: fullPath, + binaryArgs: splitArgs, + }; + } else { + return undefined; + } } let currentLaunchConfiguration: LaunchConfiguration | undefined; -export function getCurrentLaunchConfiguration(): LaunchConfiguration | undefined { return currentLaunchConfiguration; } -export async function setCurrentLaunchConfiguration(configuration: LaunchConfiguration | undefined): Promise { - currentLaunchConfiguration = configuration; - let launchConfigStr: string = currentLaunchConfiguration ? launchConfigurationToString(currentLaunchConfiguration) : ""; - statusBar.setLaunchConfiguration(launchConfigStr); - await extension._projectOutlineProvider.updateLaunchTarget(launchConfigStr); +export function getCurrentLaunchConfiguration(): + | LaunchConfiguration + | undefined { + return currentLaunchConfiguration; +} +export async function setCurrentLaunchConfiguration( + configuration: LaunchConfiguration | undefined +): Promise { + currentLaunchConfiguration = configuration; + let launchConfigStr: string = currentLaunchConfiguration + ? launchConfigurationToString(currentLaunchConfiguration) + : ""; + statusBar.setLaunchConfiguration(launchConfigStr); + await extension._projectOutlineProvider.updateLaunchTarget(launchConfigStr); } function getLaunchConfiguration(name: string): LaunchConfiguration | undefined { - return launchConfigurations.find(k => { - if (launchConfigurationToString(k) === name) { - return { ...k, keep: true }; - } - }); + return launchConfigurations.find((k) => { + if (launchConfigurationToString(k) === name) { + return { ...k, keep: true }; + } + }); } // Construct the current launch configuration object: @@ -605,48 +777,67 @@ function getLaunchConfiguration(name: string): LaunchConfiguration | undefined { // in the launch configurations array from settings. // Also update the status bar item. async function readCurrentLaunchConfiguration(): Promise { - await readLaunchConfigurations(); - let currentLaunchConfigurationName: string | undefined = extension.getState().launchConfiguration; - if (currentLaunchConfigurationName) { - currentLaunchConfiguration = getLaunchConfiguration(currentLaunchConfigurationName); - } + await readLaunchConfigurations(); + let currentLaunchConfigurationName: string | undefined = + extension.getState().launchConfiguration; + if (currentLaunchConfigurationName) { + currentLaunchConfiguration = getLaunchConfiguration( + currentLaunchConfigurationName + ); + } - let launchConfigStr : string = "No launch configuration set."; - if (currentLaunchConfiguration) { - launchConfigStr = launchConfigurationToString(currentLaunchConfiguration); - logger.message(`Reading current launch configuration "${launchConfigStr}" from the workspace state.`); + let launchConfigStr: string = "No launch configuration set."; + if (currentLaunchConfiguration) { + launchConfigStr = launchConfigurationToString(currentLaunchConfiguration); + logger.message( + `Reading current launch configuration "${launchConfigStr}" from the workspace state.` + ); + } else { + // A null launch configuration after a non empty launch configuration string name + // means that the name stored in the project state does not match any of the entries in settings. + // This typically happens after the user modifies manually "makefile.launchConfigurations" + // in the .vscode/settings.json, specifically the entry that corresponds to the current launch configuration. + // Make sure to unset the launch configuration in this scenario. + if ( + currentLaunchConfigurationName !== undefined && + currentLaunchConfigurationName !== "" + ) { + logger.message( + `Launch configuration "${currentLaunchConfigurationName}" is no longer defined in settings "makefile.launchConfigurations".` + ); + await setLaunchConfigurationByName(""); } else { - // A null launch configuration after a non empty launch configuration string name - // means that the name stored in the project state does not match any of the entries in settings. - // This typically happens after the user modifies manually "makefile.launchConfigurations" - // in the .vscode/settings.json, specifically the entry that corresponds to the current launch configuration. - // Make sure to unset the launch configuration in this scenario. - if (currentLaunchConfigurationName !== undefined && currentLaunchConfigurationName !== "") { - logger.message(`Launch configuration "${currentLaunchConfigurationName}" is no longer defined in settings "makefile.launchConfigurations".`); - await setLaunchConfigurationByName(""); - } else { - logger.message("No current launch configuration is set in the workspace state."); - } + logger.message( + "No current launch configuration is set in the workspace state." + ); } + } - statusBar.setLaunchConfiguration(launchConfigStr); - await extension._projectOutlineProvider.updateLaunchTarget(launchConfigStr); + statusBar.setLaunchConfiguration(launchConfigStr); + await extension._projectOutlineProvider.updateLaunchTarget(launchConfigStr); } export interface DefaultLaunchConfiguration { - MIMode?: string; - miDebuggerPath?: string; - stopAtEntry?: boolean; - symbolSearchPath?: string; + MIMode?: string; + miDebuggerPath?: string; + stopAtEntry?: boolean; + symbolSearchPath?: string; } let defaultLaunchConfiguration: DefaultLaunchConfiguration | undefined; -export function getDefaultLaunchConfiguration(): DefaultLaunchConfiguration | undefined { return defaultLaunchConfiguration; } +export function getDefaultLaunchConfiguration(): + | DefaultLaunchConfiguration + | undefined { + return defaultLaunchConfiguration; +} // No setter needed. Currently only the user can define makefile.defaultLaunchConfiguration export async function readDefaultLaunchConfiguration(): Promise { - defaultLaunchConfiguration = await util.getExpandedSetting("defaultLaunchConfiguration"); - logger.message(`Default launch configuration: MIMode = ${defaultLaunchConfiguration?.MIMode}, + defaultLaunchConfiguration = + await util.getExpandedSetting( + "defaultLaunchConfiguration" + ); + logger.message(`Default launch configuration: MIMode = ${defaultLaunchConfiguration?.MIMode}, miDebuggerPath = ${defaultLaunchConfiguration?.miDebuggerPath}, stopAtEntry = ${defaultLaunchConfiguration?.stopAtEntry}, symbolSearchPath = ${defaultLaunchConfiguration?.symbolSearchPath}`); @@ -656,425 +847,603 @@ export async function readDefaultLaunchConfiguration(): Promise { // when parsing all the targets that exist and when updating the cpptools configuration provider // for IntelliSense. let configurationMakeCommand: string; -export function getConfigurationMakeCommand(): string { return configurationMakeCommand; } -export function setConfigurationMakeCommand(name: string): void { configurationMakeCommand = name; } +export function getConfigurationMakeCommand(): string { + return configurationMakeCommand; +} +export function setConfigurationMakeCommand(name: string): void { + configurationMakeCommand = name; +} let configurationMakeArgs: string[] = []; -export function getConfigurationMakeArgs(): string[] { return configurationMakeArgs; } -export function setConfigurationMakeArgs(args: string[]): void { configurationMakeArgs = args; } +export function getConfigurationMakeArgs(): string[] { + return configurationMakeArgs; +} +export function setConfigurationMakeArgs(args: string[]): void { + configurationMakeArgs = args; +} // The following (makefile, problem matchers, build log), same as command&args above // are deduced via a set of rules of defaults and overrides that we calculate only when necessary // and access the last result otherwise. let configurationMakefile: string | undefined; -export function getConfigurationMakefile(): string | undefined { return configurationMakefile; } -export function setConfigurationMakefile(makefilePath: string | undefined): void { configurationMakefile = makefilePath; } +export function getConfigurationMakefile(): string | undefined { + return configurationMakefile; +} +export function setConfigurationMakefile( + makefilePath: string | undefined +): void { + configurationMakefile = makefilePath; +} let configurationProblemMatchers: string[] = []; -export function getConfigurationProblemMatchers(): string[] { return configurationProblemMatchers; } -export function setConfigurationProblemMatchers(problemMatchers: string[]): void { configurationProblemMatchers = problemMatchers; } +export function getConfigurationProblemMatchers(): string[] { + return configurationProblemMatchers; +} +export function setConfigurationProblemMatchers( + problemMatchers: string[] +): void { + configurationProblemMatchers = problemMatchers; +} let configurationBuildLog: string | undefined; -export function getConfigurationBuildLog(): string | undefined { return configurationBuildLog; } -export function setConfigurationBuildLog(name: string): void { configurationBuildLog = name; } +export function getConfigurationBuildLog(): string | undefined { + return configurationBuildLog; +} +export function setConfigurationBuildLog(name: string): void { + configurationBuildLog = name; +} let configurationPreConfigureArgs: string[] = []; -export function getConfigurationPreConfigureArgs(): string[] { return configurationPreConfigureArgs; } -export function setConfigurationPreConfigureArgs(args: string[]): void { configurationPreConfigureArgs = args; } +export function getConfigurationPreConfigureArgs(): string[] { + return configurationPreConfigureArgs; +} +export function setConfigurationPreConfigureArgs(args: string[]): void { + configurationPreConfigureArgs = args; +} let configurationPostConfigureArgs: string[] = []; -export function getConfigurationPostConfigureArgs(): string[] { return configurationPostConfigureArgs; } -export function setConfigurationPostConfigureArgs(args: string[]): void { configurationPostConfigureArgs = args; } +export function getConfigurationPostConfigureArgs(): string[] { + return configurationPostConfigureArgs; +} +export function setConfigurationPostConfigureArgs(args: string[]): void { + configurationPostConfigureArgs = args; +} // Analyze the settings of the current makefile configuration and the global workspace settings, // according to various merging rules and decide what make command and build log // apply to the current makefile configuration. async function analyzeConfigureParams(): Promise { - getBuildLogForConfiguration(currentMakefileConfiguration); - await getCommandForConfiguration(currentMakefileConfiguration); - getProblemMatchersForConfiguration(currentMakefileConfiguration); - getPreConfigureArgsForConfiguration(currentMakefileConfiguration); - getPostConfigureArgsForConfiguration(currentMakefileConfiguration); + getBuildLogForConfiguration(currentMakefileConfiguration); + await getCommandForConfiguration(currentMakefileConfiguration); + getProblemMatchersForConfiguration(currentMakefileConfiguration); + getPreConfigureArgsForConfiguration(currentMakefileConfiguration); + getPostConfigureArgsForConfiguration(currentMakefileConfiguration); } -export function getMakefileConfiguration(configuration: string | undefined): MakefileConfiguration | undefined { - return makefileConfigurations.find(k => { - if (k.name === configuration) { - return k; - } +export function getMakefileConfiguration( + configuration: string | undefined +): MakefileConfiguration | undefined { + return makefileConfigurations.find((k) => { + if (k.name === configuration) { + return k; + } }); } // Helper to find in the array of MakefileConfiguration which command/args correspond to a configuration name. // Higher level settings (like makefile.makePath, makefile.makefilePath or makefile.makeDirectory) // also have an additional effect on the final command. -export async function getCommandForConfiguration(configuration: string | undefined): Promise { - let makefileConfiguration: MakefileConfiguration | undefined = getMakefileConfiguration(configuration); +export async function getCommandForConfiguration( + configuration: string | undefined +): Promise { + let makefileConfiguration: MakefileConfiguration | undefined = + getMakefileConfiguration(configuration); - let makeParsedPathSettings: path.ParsedPath | undefined = makePath ? path.parse(makePath) : undefined; - let makeParsedPathConfigurations: path.ParsedPath | undefined = makefileConfiguration?.makePath ? path.parse(makefileConfiguration?.makePath) : undefined; + let makeParsedPathSettings: path.ParsedPath | undefined = makePath + ? path.parse(makePath) + : undefined; + let makeParsedPathConfigurations: path.ParsedPath | undefined = + makefileConfiguration?.makePath + ? path.parse(makefileConfiguration?.makePath) + : undefined; - configurationMakeArgs = []; + configurationMakeArgs = []; - // Name of the make tool can be defined as makePath in makefile.configurations or as makefile.makePath. - // When none defined, default to "make". - configurationMakeCommand = makeParsedPathConfigurations?.base || makeParsedPathSettings?.base || "make"; - let configurationMakeCommandExtension: string | undefined = makeParsedPathConfigurations?.ext || makeParsedPathSettings?.ext; + // Name of the make tool can be defined as makePath in makefile.configurations or as makefile.makePath. + // When none defined, default to "make". + configurationMakeCommand = + makeParsedPathConfigurations?.base || + makeParsedPathSettings?.base || + "make"; + let configurationMakeCommandExtension: string | undefined = + makeParsedPathConfigurations?.ext || makeParsedPathSettings?.ext; - // Prepend the directory path, if defined in either makefile.configurations or makefile.makePath (first has priority). - let configurationCommandPath: string = makeParsedPathConfigurations?.dir || makeParsedPathSettings?.dir || ""; - configurationMakeCommand = path.join(configurationCommandPath, configurationMakeCommand); + // Prepend the directory path, if defined in either makefile.configurations or makefile.makePath (first has priority). + let configurationCommandPath: string = + makeParsedPathConfigurations?.dir || makeParsedPathSettings?.dir || ""; + configurationMakeCommand = path.join( + configurationCommandPath, + configurationMakeCommand + ); - // Add "make" when only a directory path was specified. - if (util.checkDirectoryExistsSync(configurationMakeCommand)) { - configurationMakeCommand = path.join(configurationMakeCommand, "make"); + // Add "make" when only a directory path was specified. + if (util.checkDirectoryExistsSync(configurationMakeCommand)) { + configurationMakeCommand = path.join(configurationMakeCommand, "make"); + } + + // Add the ".exe" extension on windows if no extension was specified, otherwise the file search APIs don't find it. + if ( + process.platform === "win32" && + configurationMakeCommandExtension === "" + ) { + configurationMakeCommand += ".exe"; + } + + // Add the makefile path via the -f make switch. + // makefile.configurations.makefilePath overwrites makefile.makefilePath. + configurationMakefile = makefileConfiguration?.makefilePath + ? util.resolvePathToRoot(makefileConfiguration?.makefilePath) + : makefilePath; + if (configurationMakefile) { + // check if the makefile path is a directory. If so, try adding `Makefile` or `makefile` + if (util.checkDirectoryExistsSync(configurationMakefile)) { + let makeFileTest: string = path.join(configurationMakefile, "Makefile"); + if (!util.checkFileExistsSync(makeFileTest)) { + makeFileTest = path.join(configurationMakefile, "makefile"); + } + + // if we found the makefile in the directory, set the `configurationMakefile` to the found file path. + if (util.checkFileExistsSync(makeFileTest)) { + configurationMakefile = makeFileTest; + } } - // Add the ".exe" extension on windows if no extension was specified, otherwise the file search APIs don't find it. - if (process.platform === "win32" && configurationMakeCommandExtension === "") { - configurationMakeCommand += ".exe"; - } + configurationMakeArgs.push("-f"); + configurationMakeArgs.push(`${configurationMakefile}`); + // Need to rethink this (GitHub 59). + // Some repos don't work when we automatically add -C, others don't work when we don't. + // configurationMakeArgs.push("-C"); + // configurationMakeArgs.push(path.parse(configurationMakefile).dir); + } - // Add the makefile path via the -f make switch. - // makefile.configurations.makefilePath overwrites makefile.makefilePath. - configurationMakefile = makefileConfiguration?.makefilePath ? util.resolvePathToRoot(makefileConfiguration?.makefilePath) : makefilePath; - if (configurationMakefile) { - // check if the makefile path is a directory. If so, try adding `Makefile` or `makefile` - if (util.checkDirectoryExistsSync(configurationMakefile)) { - let makeFileTest: string = path.join(configurationMakefile, "Makefile"); - if (!util.checkFileExistsSync(makeFileTest)) { - makeFileTest = path.join(configurationMakefile, "makefile"); - } + // Add the working directory path via the -C switch. + // makefile.configurations.makeDirectory overwrites makefile.makeDirectory. + let makeDirectoryUsed: string | undefined = + makefileConfiguration?.makeDirectory + ? util.resolvePathToRoot(makefileConfiguration?.makeDirectory) + : makeDirectory; + if (makeDirectoryUsed) { + configurationMakeArgs.push("-C"); + configurationMakeArgs.push(`${makeDirectoryUsed}`); + } - // if we found the makefile in the directory, set the `configurationMakefile` to the found file path. - if (util.checkFileExistsSync(makeFileTest)) { - configurationMakefile = makeFileTest; - } - } + // Make sure we append "makefile.configurations[].makeArgs" last, in case the developer wants to overwrite any arguments that the extension + // deduces from the settings. Additionally, for -f/-C, resolve path to root. + if (makefileConfiguration?.makeArgs) { + let prevArg: string = ""; + makefileConfiguration.makeArgs.forEach((arg) => { + if (prevArg === "-C") { + configurationMakeArgs.push(util.resolvePathToRoot(arg)); + } else if (arg.startsWith("--directory")) { + const eqIdx: number = arg.indexOf("="); + const folderStr: string = arg.substring(eqIdx + 1, arg.length); + configurationMakeArgs.push( + `--directory=${util.resolvePathToRoot(folderStr)}` + ); + } else { + configurationMakeArgs.push(arg); + } - configurationMakeArgs.push("-f"); - configurationMakeArgs.push(`${configurationMakefile}`); - // Need to rethink this (GitHub 59). - // Some repos don't work when we automatically add -C, others don't work when we don't. - // configurationMakeArgs.push("-C"); - // configurationMakeArgs.push(path.parse(configurationMakefile).dir); - } + prevArg = arg; + }); + } - // Add the working directory path via the -C switch. - // makefile.configurations.makeDirectory overwrites makefile.makeDirectory. - let makeDirectoryUsed: string | undefined = makefileConfiguration?.makeDirectory ? util.resolvePathToRoot(makefileConfiguration?.makeDirectory) : makeDirectory; + if (configurationMakeCommand) { + logger.message( + `Deduced command '${configurationMakeCommand} ${configurationMakeArgs.join( + " " + )}' for configuration "${configuration}"` + ); + } + + // Check for makefile path on disk: we search first for any makefile specified via the makefilePath setting, + // then via the makeDirectory setting and then in the root of the workspace. On linux/mac, it often is 'Makefile', so verify that we default to the right filename. + if (!configurationMakefile) { if (makeDirectoryUsed) { - configurationMakeArgs.push("-C"); - configurationMakeArgs.push(`${makeDirectoryUsed}`); - } - - // Make sure we append "makefile.configurations[].makeArgs" last, in case the developer wants to overwrite any arguments that the extension - // deduces from the settings. Additionally, for -f/-C, resolve path to root. - if (makefileConfiguration?.makeArgs) { - let prevArg: string = ""; - makefileConfiguration.makeArgs.forEach(arg => { - if (prevArg === "-C") { - configurationMakeArgs.push(util.resolvePathToRoot(arg)); - } else if (arg.startsWith("--directory")) { - const eqIdx: number = arg.indexOf("="); - const folderStr: string = arg.substring(eqIdx + 1, arg.length); - configurationMakeArgs.push(`--directory=${util.resolvePathToRoot(folderStr)}`); - } else { - configurationMakeArgs.push(arg); - } - - prevArg = arg; - }); - } - - if (configurationMakeCommand) { - logger.message(`Deduced command '${configurationMakeCommand} ${configurationMakeArgs.join(" ")}' for configuration "${configuration}"`); - } - - // Check for makefile path on disk: we search first for any makefile specified via the makefilePath setting, - // then via the makeDirectory setting and then in the root of the workspace. On linux/mac, it often is 'Makefile', so verify that we default to the right filename. - if (!configurationMakefile) { - if (makeDirectoryUsed) { - configurationMakefile = util.resolvePathToRoot(path.join(makeDirectoryUsed, "Makefile")); - if (!util.checkFileExistsSync(configurationMakefile)) { - configurationMakefile = util.resolvePathToRoot(path.join(makeDirectoryUsed, "makefile")); - } - } else { - configurationMakefile = util.resolvePathToRoot("./Makefile"); - if (!util.checkFileExistsSync(configurationMakefile)) { - configurationMakefile = util.resolvePathToRoot("./makefile"); - } - } - } - - // Validation and warnings about properly defining the makefile and make tool. - // These are not needed if the current configuration reads from a build log instead of dry-run output. - let buildLog: string | undefined = getConfigurationBuildLog(); - let buildLogContent: string | undefined = buildLog ? util.readFile(buildLog) : undefined; - if (!buildLogContent) { - if ((!makeParsedPathSettings || makeParsedPathSettings.name === "") && - (!makeParsedPathConfigurations || makeParsedPathConfigurations.name === "")) { - logger.message("Could not find any make tool file name in makefile.configurations.makePath, nor in makefile.makePath. Assuming make."); - } - - // If configuration command has a path (absolute or relative), check if it exists on disk and error if not. - // If no path is given to the make tool, search all paths in the environment and error if make is not on the path. - if (configurationCommandPath !== "") { - if (!util.checkFileExistsSync(configurationMakeCommand)) { - logger.message("Make was not found on disk at the location provided via makefile.makePath or makefile.configurations[].makePath."); - - // How often location settings don't work (maybe because not yet expanding variables)? - const telemetryProperties: telemetry.Properties = { - reason: "not found at path given in settings" - }; - telemetry.logEvent("makeNotFound", telemetryProperties); - } - } else { - const makeBaseName: string = path.parse(configurationMakeCommand).base; - const makePathInEnv: string | undefined = util.toolPathInEnv(makeBaseName); - if (!makePathInEnv) { - logger.message("Make was not given any path in settings and is also not found on the environment path."); - - // Do the users need an environment automatically set by the extension? - // With a kits feature or expanding on the pre-configure script. - const telemetryProperties: telemetry.Properties = { - reason: "not found in environment path" - }; - telemetry.logEvent("makeNotFound", telemetryProperties); - } - } - - if (!util.checkFileExistsSync(configurationMakefile)) { - logger.message("The makefile entry point was not found. " + - "Make sure it exists at the location defined by makefile.makefilePath, makefile.configurations[].makefilePath, " + - "makefile.makeDirectory, makefile.configurations[].makeDirectory" + - "or in the root of the workspace."); - - // we may need more advanced ability to process settings - // insight into different project structures - const telemetryProperties: telemetry.Properties = { - reason: makefileConfiguration?.makefilePath || makefilePath ? - "not found at path given in settings" : - (makeDirectoryUsed ? "not found in -C provided make directory" : - "not found in workspace root") - }; - - telemetry.logEvent("makefileNotFound", telemetryProperties); - await extension.setFullFeatureSet(false); - disableAllOptionallyVisibleCommands(); - } else { - if (vscode.workspace.isTrusted) { // full feature set ON only for trusted workspaces - await extension.setFullFeatureSet(true); - enableOptionallyVisibleCommands(); - } - } + configurationMakefile = util.resolvePathToRoot( + path.join(makeDirectoryUsed, "Makefile") + ); + if (!util.checkFileExistsSync(configurationMakefile)) { + configurationMakefile = util.resolvePathToRoot( + path.join(makeDirectoryUsed, "makefile") + ); + } } else { - // If we have a build log, then we want Makefile Tools to be fully active and the UI visible, - // unless the workspace is untrusted. - if (vscode.workspace.isTrusted) { - await extension.setFullFeatureSet(true); - enableOptionallyVisibleCommands(); - } + configurationMakefile = util.resolvePathToRoot("./Makefile"); + if (!util.checkFileExistsSync(configurationMakefile)) { + configurationMakefile = util.resolvePathToRoot("./makefile"); + } } + } + + // Validation and warnings about properly defining the makefile and make tool. + // These are not needed if the current configuration reads from a build log instead of dry-run output. + let buildLog: string | undefined = getConfigurationBuildLog(); + let buildLogContent: string | undefined = buildLog + ? util.readFile(buildLog) + : undefined; + if (!buildLogContent) { + if ( + (!makeParsedPathSettings || makeParsedPathSettings.name === "") && + (!makeParsedPathConfigurations || + makeParsedPathConfigurations.name === "") + ) { + logger.message( + "Could not find any make tool file name in makefile.configurations.makePath, nor in makefile.makePath. Assuming make." + ); + } + + // If configuration command has a path (absolute or relative), check if it exists on disk and error if not. + // If no path is given to the make tool, search all paths in the environment and error if make is not on the path. + if (configurationCommandPath !== "") { + if (!util.checkFileExistsSync(configurationMakeCommand)) { + logger.message( + "Make was not found on disk at the location provided via makefile.makePath or makefile.configurations[].makePath." + ); + + // How often location settings don't work (maybe because not yet expanding variables)? + const telemetryProperties: telemetry.Properties = { + reason: "not found at path given in settings", + }; + telemetry.logEvent("makeNotFound", telemetryProperties); + } + } else { + const makeBaseName: string = path.parse(configurationMakeCommand).base; + const makePathInEnv: string | undefined = + util.toolPathInEnv(makeBaseName); + if (!makePathInEnv) { + logger.message( + "Make was not given any path in settings and is also not found on the environment path." + ); + + // Do the users need an environment automatically set by the extension? + // With a kits feature or expanding on the pre-configure script. + const telemetryProperties: telemetry.Properties = { + reason: "not found in environment path", + }; + telemetry.logEvent("makeNotFound", telemetryProperties); + } + } + + if (!util.checkFileExistsSync(configurationMakefile)) { + logger.message( + "The makefile entry point was not found. " + + "Make sure it exists at the location defined by makefile.makefilePath, makefile.configurations[].makefilePath, " + + "makefile.makeDirectory, makefile.configurations[].makeDirectory" + + "or in the root of the workspace." + ); + + // we may need more advanced ability to process settings + // insight into different project structures + const telemetryProperties: telemetry.Properties = { + reason: + makefileConfiguration?.makefilePath || makefilePath + ? "not found at path given in settings" + : makeDirectoryUsed + ? "not found in -C provided make directory" + : "not found in workspace root", + }; + + telemetry.logEvent("makefileNotFound", telemetryProperties); + await extension.setFullFeatureSet(false); + disableAllOptionallyVisibleCommands(); + } else { + if (vscode.workspace.isTrusted) { + // full feature set ON only for trusted workspaces + await extension.setFullFeatureSet(true); + enableOptionallyVisibleCommands(); + } + } + } else { + // If we have a build log, then we want Makefile Tools to be fully active and the UI visible, + // unless the workspace is untrusted. + if (vscode.workspace.isTrusted) { + await extension.setFullFeatureSet(true); + enableOptionallyVisibleCommands(); + } + } } // Helper to find in the array of MakefileConfiguration which problemMatchers correspond to a configuration name -export function getProblemMatchersForConfiguration(configuration: string | undefined): void { - let makefileConfiguration: MakefileConfiguration | undefined = getMakefileConfiguration(configuration); +export function getProblemMatchersForConfiguration( + configuration: string | undefined +): void { + let makefileConfiguration: MakefileConfiguration | undefined = + getMakefileConfiguration(configuration); - configurationProblemMatchers = makefileConfiguration?.problemMatchers || []; + configurationProblemMatchers = makefileConfiguration?.problemMatchers || []; } // Helper to find in the array of MakefileConfiguration which buildLog correspond to a configuration name -export function getBuildLogForConfiguration(configuration: string | undefined): void { - let makefileConfiguration: MakefileConfiguration | undefined = getMakefileConfiguration(configuration); +export function getBuildLogForConfiguration( + configuration: string | undefined +): void { + let makefileConfiguration: MakefileConfiguration | undefined = + getMakefileConfiguration(configuration); - configurationBuildLog = makefileConfiguration?.buildLog; + configurationBuildLog = makefileConfiguration?.buildLog; - if (configurationBuildLog) { - logger.message(`Found build log path setting "${configurationBuildLog}" defined for configuration "${configuration}"`); + if (configurationBuildLog) { + logger.message( + `Found build log path setting "${configurationBuildLog}" defined for configuration "${configuration}"` + ); - if (!path.isAbsolute(configurationBuildLog)) { - configurationBuildLog = path.join(util.getWorkspaceRoot(), configurationBuildLog); - logger.message(`Resolving build log path to "${configurationBuildLog}"`); - } - - if (!util.checkFileExistsSync(configurationBuildLog)) { - logger.message("Build log not found. Remove the build log setting or provide a build log file on disk at the given location."); - } - } else { - // Default to an eventual build log defined in settings - // If that one is not found on disk, the setting reader already warned about it. - configurationBuildLog = buildLog; + if (!path.isAbsolute(configurationBuildLog)) { + configurationBuildLog = path.join( + util.getWorkspaceRoot(), + configurationBuildLog + ); + logger.message(`Resolving build log path to "${configurationBuildLog}"`); } + + if (!util.checkFileExistsSync(configurationBuildLog)) { + logger.message( + "Build log not found. Remove the build log setting or provide a build log file on disk at the given location." + ); + } + } else { + // Default to an eventual build log defined in settings + // If that one is not found on disk, the setting reader already warned about it. + configurationBuildLog = buildLog; + } } -export function getPreConfigureArgsForConfiguration(configuration: string | undefined): void { - let makefileConfiguration: MakefileConfiguration | undefined = getMakefileConfiguration(configuration); - const localPreConfigArgs = makefileConfiguration?.preConfigureArgs; +export function getPreConfigureArgsForConfiguration( + configuration: string | undefined +): void { + let makefileConfiguration: MakefileConfiguration | undefined = + getMakefileConfiguration(configuration); + const localPreConfigArgs = makefileConfiguration?.preConfigureArgs; - if (localPreConfigArgs) { - configurationPreConfigureArgs = localPreConfigArgs; - } else { - configurationPreConfigureArgs = preConfigureArgs; - } + if (localPreConfigArgs) { + configurationPreConfigureArgs = localPreConfigArgs; + } else { + configurationPreConfigureArgs = preConfigureArgs; + } } -export function getPostConfigureArgsForConfiguration(configuration: string | undefined): void { - let makefileConfiguration: MakefileConfiguration | undefined = getMakefileConfiguration(configuration); - const localPostConfigArgs = makefileConfiguration?.postConfigureArgs; +export function getPostConfigureArgsForConfiguration( + configuration: string | undefined +): void { + let makefileConfiguration: MakefileConfiguration | undefined = + getMakefileConfiguration(configuration); + const localPostConfigArgs = makefileConfiguration?.postConfigureArgs; - if (localPostConfigArgs) { - configurationPostConfigureArgs = localPostConfigArgs; - } else { - configurationPostConfigureArgs = postConfigureArgs; - } + if (localPostConfigArgs) { + configurationPostConfigureArgs = localPostConfigArgs; + } else { + configurationPostConfigureArgs = postConfigureArgs; + } } let makefileConfigurations: MakefileConfiguration[] = []; -export function getMakefileConfigurations(): MakefileConfiguration[] { return makefileConfigurations; } -export function setMakefileConfigurations(configurations: MakefileConfiguration[]): void { makefileConfigurations = configurations; } +export function getMakefileConfigurations(): MakefileConfiguration[] { + return makefileConfigurations; +} +export function setMakefileConfigurations( + configurations: MakefileConfiguration[] +): void { + makefileConfigurations = configurations; +} // Read make configurations optionally defined by the user in settings: makefile.configurations. export async function readMakefileConfigurations(): Promise { - // We need to read "makefile.configurations" unexpanded first, because we may write back into these settings - // in case we indentify "name" missing. We'll expand later, see end of function. - let workspaceConfiguration: vscode.WorkspaceConfiguration = vscode.workspace.getConfiguration("makefile"); - makefileConfigurations = workspaceConfiguration.get("configurations") || []; - let detectedUnnamedConfigurations: boolean = false; - let unnamedConfigurationId: number = 0; + // We need to read "makefile.configurations" unexpanded first, because we may write back into these settings + // in case we indentify "name" missing. We'll expand later, see end of function. + let workspaceConfiguration: vscode.WorkspaceConfiguration = + vscode.workspace.getConfiguration("makefile"); + makefileConfigurations = + workspaceConfiguration.get("configurations") || []; + let detectedUnnamedConfigurations: boolean = false; + let unnamedConfigurationId: number = 0; - // Collect unnamed configurations (probably) defined by the extension earlier, - // to make sure we avoid duplicates in case any new configuration is in need of a name. - let unnamedConfigurationNames: string [] = makefileConfigurations.map((k => { - return k.name; - })); - unnamedConfigurationNames = unnamedConfigurationNames.filter(item => (item && item.startsWith("Unnamed configuration"))); + // Collect unnamed configurations (probably) defined by the extension earlier, + // to make sure we avoid duplicates in case any new configuration is in need of a name. + let unnamedConfigurationNames: string[] = makefileConfigurations.map((k) => { + return k.name; + }); + unnamedConfigurationNames = unnamedConfigurationNames.filter( + (item) => item && item.startsWith("Unnamed configuration") + ); - makefileConfigurations.forEach(element => { - if (!element.name) { - detectedUnnamedConfigurations = true; + makefileConfigurations.forEach((element) => { + if (!element.name) { + detectedUnnamedConfigurations = true; - // Just considering the possibility that there are already unnamed configurations - // defined with IDs other than the rule we assume (like not consecutive numbers, but not only). - // This may happen when the user deletes configurations at some point without updating the IDs. - unnamedConfigurationId++; - let autoGeneratedName: string = `Unnamed configuration ${unnamedConfigurationId}`; - while (unnamedConfigurationNames.includes(autoGeneratedName)) { - unnamedConfigurationId++; - autoGeneratedName = `Unnamed configuration ${unnamedConfigurationId}`; - } + // Just considering the possibility that there are already unnamed configurations + // defined with IDs other than the rule we assume (like not consecutive numbers, but not only). + // This may happen when the user deletes configurations at some point without updating the IDs. + unnamedConfigurationId++; + let autoGeneratedName: string = `Unnamed configuration ${unnamedConfigurationId}`; + while (unnamedConfigurationNames.includes(autoGeneratedName)) { + unnamedConfigurationId++; + autoGeneratedName = `Unnamed configuration ${unnamedConfigurationId}`; + } - element.name = autoGeneratedName; - logger.message(`Defining name ${autoGeneratedName} for unnamed configuration ${element}.`); - } - }); - - if (detectedUnnamedConfigurations) { - logger.message("Updating makefile configurations in settings."); - await workspaceConfiguration.update("configurations", makefileConfigurations); + element.name = autoGeneratedName; + logger.message( + `Defining name ${autoGeneratedName} for unnamed configuration ${element}.` + ); } + }); - // Now read "makefile.configurations" again and expand as needed. - makefileConfigurations = await util.getExpandedSetting("configurations") || []; + if (detectedUnnamedConfigurations) { + logger.message("Updating makefile configurations in settings."); + await workspaceConfiguration.update( + "configurations", + makefileConfigurations + ); + } - // Log the updated list of configuration names - const makefileConfigurationNames: string[] = makefileConfigurations.map((k => { - return k.name; - })); - if (makefileConfigurationNames.length > 0) { - logger.message("Found the following configurations defined in makefile.configurations setting: " + makefileConfigurationNames.join(";")); + // Now read "makefile.configurations" again and expand as needed. + makefileConfigurations = + (await util.getExpandedSetting( + "configurations" + )) || []; + + // Log the updated list of configuration names + const makefileConfigurationNames: string[] = makefileConfigurations.map( + (k) => { + return k.name; } + ); + if (makefileConfigurationNames.length > 0) { + logger.message( + "Found the following configurations defined in makefile.configurations setting: " + + makefileConfigurationNames.join(";") + ); + } - // Verify if the current makefile configuration (check against the expanded values) - // is still part of the list and unset otherwise. - // Exception: "Default" which means the user didn't set it and relies on whatever default - // the current set of makefiles support. "Default" is not going to be part of the list - // but we shouldn't log about it. - if (currentMakefileConfiguration !== "Default" && !makefileConfigurationNames.includes(currentMakefileConfiguration)) { - logger.message(`Current makefile configuration ${currentMakefileConfiguration} is no longer present in the available list.` + - ` Re-setting the current makefile configuration to default.`); - await setConfigurationByName("Default"); - } + // Verify if the current makefile configuration (check against the expanded values) + // is still part of the list and unset otherwise. + // Exception: "Default" which means the user didn't set it and relies on whatever default + // the current set of makefiles support. "Default" is not going to be part of the list + // but we shouldn't log about it. + if ( + currentMakefileConfiguration !== "Default" && + !makefileConfigurationNames.includes(currentMakefileConfiguration) + ) { + logger.message( + `Current makefile configuration ${currentMakefileConfiguration} is no longer present in the available list.` + + ` Re-setting the current makefile configuration to default.` + ); + await setConfigurationByName("Default"); + } } // Last target picked from the set of targets that are run by the makefiles // when building for the current configuration. // Saved into the settings storage. Also reflected in the configuration status bar button let currentTarget: string | undefined; -export function getCurrentTarget(): string | undefined { return currentTarget; } -export function setCurrentTarget(target: string | undefined): void { currentTarget = target; } +export function getCurrentTarget(): string | undefined { + return currentTarget; +} +export function setCurrentTarget(target: string | undefined): void { + currentTarget = target; +} // Read current target from workspace state, update status bar item function readCurrentTarget(): void { - let buildTarget : string | undefined = extension.getState().buildTarget; - if (!buildTarget) { - logger.message("No target defined in the workspace state. Assuming 'Default'."); - statusBar.setTarget("Default"); - // If no particular target is defined in settings, use 'Default' for the button - // but keep the variable empty, to not append it to the make command. - currentTarget = ""; - } else { - currentTarget = buildTarget; - logger.message(`Reading current build target "${currentTarget}" from the workspace state.`); - statusBar.setTarget(currentTarget); - } + let buildTarget: string | undefined = extension.getState().buildTarget; + if (!buildTarget) { + logger.message( + "No target defined in the workspace state. Assuming 'Default'." + ); + statusBar.setTarget("Default"); + // If no particular target is defined in settings, use 'Default' for the button + // but keep the variable empty, to not append it to the make command. + currentTarget = ""; + } else { + currentTarget = buildTarget; + logger.message( + `Reading current build target "${currentTarget}" from the workspace state.` + ); + statusBar.setTarget(currentTarget); + } } let configureOnOpen: boolean | undefined; -export function getConfigureOnOpen(): boolean | undefined { return configureOnOpen; } -export function setConfigureOnOpen(configure: boolean): void { configureOnOpen = configure; } +export function getConfigureOnOpen(): boolean | undefined { + return configureOnOpen; +} +export function setConfigureOnOpen(configure: boolean): void { + configureOnOpen = configure; +} export async function readConfigureOnOpen(): Promise { - configureOnOpen = await util.getExpandedSetting("configureOnOpen"); - logger.message(`Configure on open: ${configureOnOpen}`); + configureOnOpen = await util.getExpandedSetting("configureOnOpen"); + logger.message(`Configure on open: ${configureOnOpen}`); } let configureOnEdit: boolean | undefined; -export function getConfigureOnEdit(): boolean | undefined { return configureOnEdit; } -export function setConfigureOnEdit(configure: boolean): void { configureOnEdit = configure; } +export function getConfigureOnEdit(): boolean | undefined { + return configureOnEdit; +} +export function setConfigureOnEdit(configure: boolean): void { + configureOnEdit = configure; +} export async function readConfigureOnEdit(): Promise { - configureOnEdit = await util.getExpandedSetting("configureOnEdit"); - logger.message(`Configure on edit: ${configureOnEdit}`); + configureOnEdit = await util.getExpandedSetting("configureOnEdit"); + logger.message(`Configure on edit: ${configureOnEdit}`); } let configureAfterCommand: boolean | undefined; -export function getConfigureAfterCommand(): boolean | undefined { return configureAfterCommand; } -export function setConfigureAfterCommand(configure: boolean): void { configureAfterCommand = configure; } +export function getConfigureAfterCommand(): boolean | undefined { + return configureAfterCommand; +} +export function setConfigureAfterCommand(configure: boolean): void { + configureAfterCommand = configure; +} export async function readConfigureAfterCommand(): Promise { - configureAfterCommand = await util.getExpandedSetting("configureAfterCommand"); - logger.message(`Configure after command: ${configureAfterCommand}`); + configureAfterCommand = await util.getExpandedSetting( + "configureAfterCommand" + ); + logger.message(`Configure after command: ${configureAfterCommand}`); } let phonyOnlyTargets: boolean | undefined; -export function getPhonyOnlyTargets(): boolean | undefined { return phonyOnlyTargets; } -export function setPhonyOnlyTargets(phony: boolean): void { phonyOnlyTargets = phony; } +export function getPhonyOnlyTargets(): boolean | undefined { + return phonyOnlyTargets; +} +export function setPhonyOnlyTargets(phony: boolean): void { + phonyOnlyTargets = phony; +} export async function readPhonyOnlyTargets(): Promise { - phonyOnlyTargets = await util.getExpandedSetting("phonyOnlyTargets"); - logger.message(`Only .PHONY targets: ${phonyOnlyTargets}`); + phonyOnlyTargets = await util.getExpandedSetting("phonyOnlyTargets"); + logger.message(`Only .PHONY targets: ${phonyOnlyTargets}`); } let saveBeforeBuildOrConfigure: boolean | undefined; -export function getSaveBeforeBuildOrConfigure(): boolean | undefined { return saveBeforeBuildOrConfigure; } -export function setSaveBeforeBuildOrConfigure(save: boolean): void { saveBeforeBuildOrConfigure = save; } +export function getSaveBeforeBuildOrConfigure(): boolean | undefined { + return saveBeforeBuildOrConfigure; +} +export function setSaveBeforeBuildOrConfigure(save: boolean): void { + saveBeforeBuildOrConfigure = save; +} export async function readSaveBeforeBuildOrConfigure(): Promise { - saveBeforeBuildOrConfigure = await util.getExpandedSetting("saveBeforeBuildOrConfigure"); - logger.message(`Save before build or configure: ${saveBeforeBuildOrConfigure}`); + saveBeforeBuildOrConfigure = await util.getExpandedSetting( + "saveBeforeBuildOrConfigure" + ); + logger.message( + `Save before build or configure: ${saveBeforeBuildOrConfigure}` + ); } let buildBeforeLaunch: boolean | undefined; -export function getBuildBeforeLaunch(): boolean | undefined { return buildBeforeLaunch; } -export function setBuildBeforeLaunch(build: boolean): void { buildBeforeLaunch = build; } +export function getBuildBeforeLaunch(): boolean | undefined { + return buildBeforeLaunch; +} +export function setBuildBeforeLaunch(build: boolean): void { + buildBeforeLaunch = build; +} export async function readBuildBeforeLaunch(): Promise { - buildBeforeLaunch = await util.getExpandedSetting("buildBeforeLaunch"); - logger.message(`Build before launch: ${buildBeforeLaunch}`); + buildBeforeLaunch = await util.getExpandedSetting( + "buildBeforeLaunch" + ); + logger.message(`Build before launch: ${buildBeforeLaunch}`); } let clearOutputBeforeBuild: boolean | undefined; -export function getClearOutputBeforeBuild(): boolean | undefined { return clearOutputBeforeBuild; } -export function setClearOutputBeforeBuild(clear: boolean): void { clearOutputBeforeBuild = clear; } +export function getClearOutputBeforeBuild(): boolean | undefined { + return clearOutputBeforeBuild; +} +export function setClearOutputBeforeBuild(clear: boolean): void { + clearOutputBeforeBuild = clear; +} export async function readClearOutputBeforeBuild(): Promise { - clearOutputBeforeBuild = await util.getExpandedSetting("clearOutputBeforeBuild"); - logger.message(`Clear output before build: ${clearOutputBeforeBuild}`); + clearOutputBeforeBuild = await util.getExpandedSetting( + "clearOutputBeforeBuild" + ); + logger.message(`Clear output before build: ${clearOutputBeforeBuild}`); } // This setting is useful for some repos where directory changing commands (cd, push, pop) @@ -1084,11 +1453,17 @@ export async function readClearOutputBeforeBuild(): Promise { // (which prints the messages regarding "Entering direcory" and "Leaving directory"), // which is not perfect either for all repos. let ignoreDirectoryCommands: boolean | undefined; -export function getIgnoreDirectoryCommands(): boolean | undefined { return ignoreDirectoryCommands; } -export function setIgnoreDirectoryCommands(ignore: boolean): void { ignoreDirectoryCommands = ignore; } +export function getIgnoreDirectoryCommands(): boolean | undefined { + return ignoreDirectoryCommands; +} +export function setIgnoreDirectoryCommands(ignore: boolean): void { + ignoreDirectoryCommands = ignore; +} export async function readIgnoreDirectoryCommands(): Promise { - ignoreDirectoryCommands = await util.getExpandedSetting("ignoreDirectoryCommands"); - logger.message(`Ignore directory commands: ${ignoreDirectoryCommands}`); + ignoreDirectoryCommands = await util.getExpandedSetting( + "ignoreDirectoryCommands" + ); + logger.message(`Ignore directory commands: ${ignoreDirectoryCommands}`); } // Initialization from the state of the workspace. @@ -1099,636 +1474,767 @@ export async function readIgnoreDirectoryCommands(): Promise { // and commands become available to be run in settings via expansion. // These can also be resetted via the makefile.resetState command. export function initFromState(): void { - readCurrentMakefileConfiguration(); - readCurrentTarget(); + readCurrentMakefileConfiguration(); + readCurrentTarget(); } // Initialization from settings (or backup default rules). // This is called at activation time (with activation boolean being passed as true explicitly) // or after any change in the configuration/build-target workspace state variables, in which case // we need a refresh of all settings expanding ${configuration} or ${buildTarget}. -export async function initFromSettings(activation: boolean = false): Promise { - // Read first anything related to the output folder and the extension log, - // to be able to document any upcoming reads. - await readExtensionOutputFolder(); - await readExtensionLog(); +export async function initFromSettings( + activation: boolean = false +): Promise { + // Read first anything related to the output folder and the extension log, + // to be able to document any upcoming reads. + await readExtensionOutputFolder(); + await readExtensionLog(); - // Delete the extension log file, if exists, even if we lose what we logged earlier - // about reading the output folder and extension log. - // The deletion should happen only at activation time (to not allow the log file to grow indefinitely), - // while reading the settings is done at activation time and also anytime later, - // after changing a makefile configuration, a build or a launch target. - let extensionLog : string | undefined = getExtensionLog(); - if (extensionLog && activation && util.checkFileExistsSync(extensionLog)) { - util.deleteFileSync(extensionLog); + // Delete the extension log file, if exists, even if we lose what we logged earlier + // about reading the output folder and extension log. + // The deletion should happen only at activation time (to not allow the log file to grow indefinitely), + // while reading the settings is done at activation time and also anytime later, + // after changing a makefile configuration, a build or a launch target. + let extensionLog: string | undefined = getExtensionLog(); + if (extensionLog && activation && util.checkFileExistsSync(extensionLog)) { + util.deleteFileSync(extensionLog); + } + + await readLoggingLevel(); + await readConfigurationCachePath(); + await readMakePath(); + await readMakefilePath(); + await readMakeDirectory(); + extension.updateBuildLogPresent(await readBuildLog()); + await readPreConfigureScript(); + await readPreConfigureArgs(); + await readAlwaysPreConfigure(); + await readPostConfigureScript(); + await readPostConfigureArgs(); + await readAlwaysPostConfigure(); + await readDryrunSwitches(); + await readAdditionalCompilerNames(); + await readExcludeCompilerNames(); + await readMakefileConfigurations(); + await readCurrentLaunchConfiguration(); + await readDefaultLaunchConfiguration(); + await readConfigureOnOpen(); + await readConfigureOnEdit(); + await readConfigureAfterCommand(); + await readPhonyOnlyTargets(); + await readSaveBeforeBuildOrConfigure(); + await readBuildBeforeLaunch(); + await readClearOutputBeforeBuild(); + await readIgnoreDirectoryCommands(); + await readCompileCommandsPath(); + + initOptionalFeatures(); + await readFeaturesVisibility(); + + await analyzeConfigureParams(); + + await extension._projectOutlineProvider.update( + extension.getState().buildConfiguration, + extension.getState().buildTarget, + extension.getState().launchConfiguration, + getConfigurationMakefile(), + getConfigurationMakeCommand(), + getConfigurationBuildLog() + ); + + // Listen to the workspace trust change event + vscode.workspace.onDidGrantWorkspaceTrust(async (e) => { + await getCommandForConfiguration(currentMakefileConfiguration); // this refreshes fullFeatureSet and enables visible features + }); + + // Verify the dirty state of the IntelliSense config provider and update accordingly. + // The makefile.configureOnEdit setting can be set to false when this behavior is inconvenient. + vscode.window.onDidChangeActiveTextEditor(async (e) => { + let language: string = ""; + if (e) { + language = e.document.languageId; } - await readLoggingLevel(); - await readConfigurationCachePath(); - await readMakePath(); - await readMakefilePath(); - await readMakeDirectory(); - extension.updateBuildLogPresent(await readBuildLog()); - await readPreConfigureScript(); - await readPreConfigureArgs(); - await readAlwaysPreConfigure(); - await readPostConfigureScript(); - await readPostConfigureArgs(); - await readAlwaysPostConfigure(); - await readDryrunSwitches(); - await readAdditionalCompilerNames(); - await readExcludeCompilerNames(); - await readMakefileConfigurations(); - await readCurrentLaunchConfiguration(); - await readDefaultLaunchConfiguration(); - await readConfigureOnOpen(); - await readConfigureOnEdit(); - await readConfigureAfterCommand(); - await readPhonyOnlyTargets(); - await readSaveBeforeBuildOrConfigure(); - await readBuildBeforeLaunch(); - await readClearOutputBeforeBuild(); - await readIgnoreDirectoryCommands(); - await readCompileCommandsPath(); - - initOptionalFeatures(); - await readFeaturesVisibility(); - - await analyzeConfigureParams(); - - await extension._projectOutlineProvider.update(extension.getState().buildConfiguration, - extension.getState().buildTarget, - extension.getState().launchConfiguration, - getConfigurationMakefile(), - getConfigurationMakeCommand(), - getConfigurationBuildLog()); - - // Listen to the workspace trust change event - vscode.workspace.onDidGrantWorkspaceTrust(async e => { - await getCommandForConfiguration(currentMakefileConfiguration); // this refreshes fullFeatureSet and enables visible features - }); - - // Verify the dirty state of the IntelliSense config provider and update accordingly. - // The makefile.configureOnEdit setting can be set to false when this behavior is inconvenient. - vscode.window.onDidChangeActiveTextEditor(async e => { - let language: string = ""; - if (e) { - language = e.document.languageId; + // It is too annoying to generate a configure on any kind of editor focus change + // (for example even searching in the logging window generates this event). + // Since all the operations are guarded by the configureDirty state, + // the only "operation" left that we need to make sure it's up to date + // is IntelliSense, so trigger a configure when we switch editor focus + // into C/C++ source code. + switch (language) { + case "c": + case "cpp": + // If configureDirty is already set from a previous VSCode session, + // at workspace load this event (onDidChangeActiveTextEditor) is triggered automatically + // and if makefile.configureOnOpen is true, there is a race between two configure operations, + // one of which being unnecessary. If configureOnOpen is false, there is no race + // but still we don't want to override the behavior desired by the user. + // Additionally, if anything dirtied the configure state during a (pre)configure or build, + // skip this clean configure, to avoid annoying "blocked operation" notifications. + // The configure state remains dirty and a new configure will be triggered eventually: + // (selecting a new configuration, target or launch, build, editor focus change). + // Guarding only for not being blocked is not enough. For example, + // in the first scenario explained above, the race happens when nothing looks blocked + // here, but leading to a block notification soon. + if (extension.getState().configureDirty && configureOnEdit) { + if ( + extension.getCompletedConfigureInSession() && + !make.blockedByOp(make.Operations.configure, false) + ) { + logger.message("Configuring after settings or makefile changes..."); + await make.configure( + make.TriggeredBy.configureAfterEditorFocusChange + ); // this sets configureDirty back to false if it succeeds + } } - // It is too annoying to generate a configure on any kind of editor focus change - // (for example even searching in the logging window generates this event). - // Since all the operations are guarded by the configureDirty state, - // the only "operation" left that we need to make sure it's up to date - // is IntelliSense, so trigger a configure when we switch editor focus - // into C/C++ source code. - switch (language) { - case "c": - case "cpp": - // If configureDirty is already set from a previous VSCode session, - // at workspace load this event (onDidChangeActiveTextEditor) is triggered automatically - // and if makefile.configureOnOpen is true, there is a race between two configure operations, - // one of which being unnecessary. If configureOnOpen is false, there is no race - // but still we don't want to override the behavior desired by the user. - // Additionally, if anything dirtied the configure state during a (pre)configure or build, - // skip this clean configure, to avoid annoying "blocked operation" notifications. - // The configure state remains dirty and a new configure will be triggered eventually: - // (selecting a new configuration, target or launch, build, editor focus change). - // Guarding only for not being blocked is not enough. For example, - // in the first scenario explained above, the race happens when nothing looks blocked - // here, but leading to a block notification soon. - if (extension.getState().configureDirty && configureOnEdit) { - if ((extension.getCompletedConfigureInSession()) - && !make.blockedByOp(make.Operations.configure, false)) { - logger.message("Configuring after settings or makefile changes..."); - await make.configure(make.TriggeredBy.configureAfterEditorFocusChange); // this sets configureDirty back to false if it succeeds - } - } + break; - break; + default: + break; + } + }); - default: - break; + // Modifying any makefile should trigger an IntelliSense config provider update, + // so make the dirty state true. + // TODO: limit to makefiles relevant to this project, instead of any random makefile anywhere. + // We can't listen only to the makefile pointed to by makefile.makefilePath or makefile.makeDirectory, + // because that is only the entry point and can refer to other relevant makefiles. + // TODO: don't trigger an update for any dummy save, verify how the content changed. + vscode.workspace.onDidSaveTextDocument((e) => { + if (e.uri.fsPath.toLowerCase().endsWith("makefile")) { + extension.getState().configureDirty = true; + } + }); + + // Watch for Makefile Tools setting updates that can change the IntelliSense config provider dirty state. + // More than one setting may be updated on one settings.json save, + // so make sure to OR the dirty state when it's calculated by a formula (not a simple TRUE value). + vscode.workspace.onDidChangeConfiguration(async (e) => { + if ( + vscode.workspace.workspaceFolders && + e.affectsConfiguration("makefile") + ) { + // We are interested in updating only some relevant properties. + // A subset of these should also trigger an IntelliSense config provider update. + // Avoid unnecessary updates (for example, when settings are modified via the extension quickPick). + let telemetryProperties: telemetry.Properties | null = {}; + let updatedSettingsSubkeys: string[] = []; + const keyRoot: string = "makefile"; + let subKey: string = "launchConfigurations"; + + let updatedLaunchConfigurations: LaunchConfiguration[] | undefined = + await util.getExpandedSetting(subKey); + if (!util.areEqual(updatedLaunchConfigurations, launchConfigurations)) { + // Changing a launch configuration does not impact the make or compiler tools invocations, + // so no IntelliSense update is needed. + await readCurrentLaunchConfiguration(); // this gets a refreshed view of all launch configurations + // and also updates the current one in case it was affected + updatedSettingsSubkeys.push(subKey); + } + + subKey = "defaultLaunchConfiguration"; + let updatedDefaultLaunchConfiguration: + | DefaultLaunchConfiguration + | undefined = await util.getExpandedSetting( + subKey + ); + if ( + !util.areEqual( + updatedDefaultLaunchConfiguration, + defaultLaunchConfiguration + ) + ) { + // Changing a global debug configuration does not impact the make or compiler tools invocations, + // so no IntelliSense update is needed. + await readDefaultLaunchConfiguration(); + updatedSettingsSubkeys.push(subKey); + } + + subKey = "loggingLevel"; + let updatedLoggingLevel: string | undefined = + await util.getExpandedSetting(subKey); + if (updatedLoggingLevel !== loggingLevel) { + await readLoggingLevel(); + updatedSettingsSubkeys.push(subKey); + } + + subKey = "buildLog"; + let updatedBuildLog: string | undefined = + await util.getExpandedSetting(subKey); + if (updatedBuildLog) { + updatedBuildLog = util.resolvePathToRoot(updatedBuildLog); + } + if (updatedBuildLog !== buildLog) { + // Configure is dirty only if the current configuration + // doesn't have already another build log set + // (which overrides the global one). + let currentMakefileConfiguration: MakefileConfiguration | undefined = + makefileConfigurations.find((k) => { + if (k.name === getCurrentMakefileConfiguration()) { + return k; + } + }); + + extension.getState().configureDirty = + extension.getState().configureDirty || + !currentMakefileConfiguration || + !currentMakefileConfiguration.buildLog; + extension.updateBuildLogPresent(await readBuildLog()); + updatedSettingsSubkeys.push(subKey); + } + + subKey = "extensionOutputFolder"; + let updatedExtensionOutputFolder: string | undefined = + await util.getExpandedSetting(subKey); + if (updatedExtensionOutputFolder) { + updatedExtensionOutputFolder = util.resolvePathToRoot( + updatedExtensionOutputFolder + ); + if ( + !util.checkDirectoryExistsSync(updatedExtensionOutputFolder) && + !util.createDirectorySync(updatedExtensionOutputFolder) + ) { + // No logging necessary about not being able to create the directory, + // readExtensionOutputFolder called below will complain if it's the case. + updatedExtensionOutputFolder = undefined; } - }); + } + if (updatedExtensionOutputFolder !== extensionOutputFolder) { + // No IntelliSense update needed. + await readExtensionOutputFolder(); + updatedSettingsSubkeys.push(subKey); + } - // Modifying any makefile should trigger an IntelliSense config provider update, - // so make the dirty state true. - // TODO: limit to makefiles relevant to this project, instead of any random makefile anywhere. - // We can't listen only to the makefile pointed to by makefile.makefilePath or makefile.makeDirectory, - // because that is only the entry point and can refer to other relevant makefiles. - // TODO: don't trigger an update for any dummy save, verify how the content changed. - vscode.workspace.onDidSaveTextDocument(e => { - if (e.uri.fsPath.toLowerCase().endsWith("makefile")) { - extension.getState().configureDirty = true; + subKey = "extensionLog"; + let updatedExtensionLog: string | undefined = + await util.getExpandedSetting(subKey); + if (updatedExtensionLog) { + // If there is a directory defined within the extension log path, + // honor it and don't append to extensionOutputFolder. + let parsePath: path.ParsedPath = path.parse(updatedExtensionLog); + if (extensionOutputFolder && !parsePath.dir) { + updatedExtensionLog = path.join( + extensionOutputFolder, + updatedExtensionLog + ); + } else { + updatedExtensionLog = util.resolvePathToRoot(updatedExtensionLog); } - }); + } + if (updatedExtensionLog !== extensionLog) { + // No IntelliSense update needed. + await readExtensionLog(); + updatedSettingsSubkeys.push(subKey); + } - // Watch for Makefile Tools setting updates that can change the IntelliSense config provider dirty state. - // More than one setting may be updated on one settings.json save, - // so make sure to OR the dirty state when it's calculated by a formula (not a simple TRUE value). - vscode.workspace.onDidChangeConfiguration(async e => { - if (vscode.workspace.workspaceFolders && e.affectsConfiguration('makefile')) { - // We are interested in updating only some relevant properties. - // A subset of these should also trigger an IntelliSense config provider update. - // Avoid unnecessary updates (for example, when settings are modified via the extension quickPick). - let telemetryProperties: telemetry.Properties | null = {}; - let updatedSettingsSubkeys: string[] = []; - const keyRoot: string = "makefile"; - let subKey: string = "launchConfigurations"; + subKey = "preConfigureScript"; + let updatedPreConfigureScript: string | undefined = + await util.getExpandedSetting(subKey); + if (updatedPreConfigureScript) { + updatedPreConfigureScript = util.resolvePathToRoot( + updatedPreConfigureScript + ); + } + if (updatedPreConfigureScript !== preConfigureScript) { + // No IntelliSense update needed. + await readPreConfigureScript(); + updatedSettingsSubkeys.push(subKey); + } - let updatedLaunchConfigurations : LaunchConfiguration[] | undefined = await util.getExpandedSetting(subKey); - if (!util.areEqual(updatedLaunchConfigurations, launchConfigurations)) { - // Changing a launch configuration does not impact the make or compiler tools invocations, - // so no IntelliSense update is needed. - await readCurrentLaunchConfiguration(); // this gets a refreshed view of all launch configurations - // and also updates the current one in case it was affected - updatedSettingsSubkeys.push(subKey); - } + subKey = "preConfigureArgs"; + let updatedPreConfigureArgs: string[] | undefined = + await util.getExpandedSetting(subKey); + if ( + updatedPreConfigureArgs && + !util.areEqual(updatedPreConfigureArgs, preConfigureArgs) + ) { + await readPreConfigureArgs(); + updatedSettingsSubkeys.push(subKey); + } - subKey = "defaultLaunchConfiguration"; - let updatedDefaultLaunchConfiguration : DefaultLaunchConfiguration | undefined = await util.getExpandedSetting(subKey); - if (!util.areEqual(updatedDefaultLaunchConfiguration, defaultLaunchConfiguration)) { - // Changing a global debug configuration does not impact the make or compiler tools invocations, - // so no IntelliSense update is needed. - await readDefaultLaunchConfiguration(); - updatedSettingsSubkeys.push(subKey); - } + subKey = "postConfigureScript"; + let updatedPostConfigureScript: string | undefined = + await util.getExpandedSetting(subKey); + if (updatedPostConfigureScript) { + updatedPostConfigureScript = util.resolvePathToRoot( + updatedPostConfigureScript + ); + } + if (updatedPostConfigureScript !== postConfigureScript) { + await readPostConfigureScript(); + updatedSettingsSubkeys.push(subKey); + } - subKey = "loggingLevel"; - let updatedLoggingLevel : string | undefined = await util.getExpandedSetting(subKey); - if (updatedLoggingLevel !== loggingLevel) { - await readLoggingLevel(); - updatedSettingsSubkeys.push(subKey); - } + subKey = "postConfigureArgs"; + let updatedPostConfigureArgs: string[] | undefined = + await util.getExpandedSetting(subKey); + if ( + updatedPostConfigureArgs && + !util.areEqual(updatedPostConfigureArgs, postConfigureArgs) + ) { + await readPostConfigureArgs(); + updatedSettingsSubkeys.push(subKey); + } - subKey = "buildLog"; - let updatedBuildLog : string | undefined = await util.getExpandedSetting(subKey); - if (updatedBuildLog) { - updatedBuildLog = util.resolvePathToRoot(updatedBuildLog); - } - if (updatedBuildLog !== buildLog) { - // Configure is dirty only if the current configuration - // doesn't have already another build log set - // (which overrides the global one). - let currentMakefileConfiguration: MakefileConfiguration | undefined = makefileConfigurations.find(k => { - if (k.name === getCurrentMakefileConfiguration()) { - return k; - } - }); + subKey = "alwaysPreConfigure"; + let updatedAlwaysPreConfigure: boolean | undefined = + await util.getExpandedSetting(subKey); + if (updatedAlwaysPreConfigure !== alwaysPreConfigure) { + // No IntelliSense update needed. + await readAlwaysPreConfigure(); + updatedSettingsSubkeys.push(subKey); + } - extension.getState().configureDirty = extension.getState().configureDirty || - !currentMakefileConfiguration || !currentMakefileConfiguration.buildLog; - extension.updateBuildLogPresent(await readBuildLog()); - updatedSettingsSubkeys.push(subKey); - } + subKey = "alwaysPostConfigure"; + let updatedAlwaysPostConfigure: boolean | undefined = + await util.getExpandedSetting(subKey); + if (updatedAlwaysPostConfigure !== alwaysPostConfigure) { + await readAlwaysPostConfigure(); + updatedSettingsSubkeys.push(subKey); + } - subKey = "extensionOutputFolder"; - let updatedExtensionOutputFolder : string | undefined = await util.getExpandedSetting(subKey); - if (updatedExtensionOutputFolder) { - updatedExtensionOutputFolder = util.resolvePathToRoot(updatedExtensionOutputFolder); - if (!util.checkDirectoryExistsSync(updatedExtensionOutputFolder) && - !util.createDirectorySync(updatedExtensionOutputFolder)) { - // No logging necessary about not being able to create the directory, - // readExtensionOutputFolder called below will complain if it's the case. - updatedExtensionOutputFolder = undefined; - } - } - if (updatedExtensionOutputFolder !== extensionOutputFolder) { - // No IntelliSense update needed. - await readExtensionOutputFolder(); - updatedSettingsSubkeys.push(subKey); - } + subKey = "configurationCachePath"; + let oldConfigurationCachePath = configurationCachePath; + await readConfigurationCachePath(); + if (oldConfigurationCachePath !== configurationCachePath) { + // A change in makefile.configurationCachePath should trigger an IntelliSense update + // only if the extension is not currently reading from a build log. + extension.getState().configureDirty = + extension.getState().configureDirty || + !buildLog || + !util.checkFileExistsSync(buildLog); + updatedSettingsSubkeys.push(subKey); + } - subKey = "extensionLog"; - let updatedExtensionLog : string | undefined = await util.getExpandedSetting(subKey); - if (updatedExtensionLog) { - // If there is a directory defined within the extension log path, - // honor it and don't append to extensionOutputFolder. - let parsePath: path.ParsedPath = path.parse(updatedExtensionLog); - if (extensionOutputFolder && !parsePath.dir) { - updatedExtensionLog = path.join(extensionOutputFolder, updatedExtensionLog); - } else { - updatedExtensionLog = util.resolvePathToRoot(updatedExtensionLog); - } - } - if (updatedExtensionLog !== extensionLog) { - // No IntelliSense update needed. - await readExtensionLog(); - updatedSettingsSubkeys.push(subKey); - } + subKey = "makePath"; + let updatedMakePath: string | undefined = + await util.getExpandedSetting(subKey); + if (updatedMakePath !== makePath) { + // Not very likely, but it is safe to consider that a different make tool + // may produce a different dry-run output with potential impact on IntelliSense, + // so trigger an update (unless we read from a build log). + extension.getState().configureDirty = + extension.getState().configureDirty || + !buildLog || + !util.checkFileExistsSync(buildLog); + await readMakePath(); + updatedSettingsSubkeys.push(subKey); + } - subKey = "preConfigureScript"; - let updatedPreConfigureScript : string | undefined = await util.getExpandedSetting(subKey); - if (updatedPreConfigureScript) { - updatedPreConfigureScript = util.resolvePathToRoot(updatedPreConfigureScript); - } - if (updatedPreConfigureScript !== preConfigureScript) { - // No IntelliSense update needed. - await readPreConfigureScript(); - updatedSettingsSubkeys.push(subKey); - } + subKey = "makefilePath"; + let updatedMakefilePath: string | undefined = + await util.getExpandedSetting(subKey); + if (updatedMakefilePath) { + updatedMakefilePath = util.resolvePathToRoot(updatedMakefilePath); + } + if (updatedMakefilePath !== makefilePath) { + // A change in makefile.makefilePath should trigger an IntelliSense update + // only if the extension is not currently reading from a build log. + extension.getState().configureDirty = + extension.getState().configureDirty || + !buildLog || + !util.checkFileExistsSync(buildLog); + await readMakefilePath(); + updatedSettingsSubkeys.push(subKey); + } - subKey = "preConfigureArgs"; - let updatedPreConfigureArgs: string[] | undefined = await util.getExpandedSetting(subKey); - if (updatedPreConfigureArgs && !util.areEqual(updatedPreConfigureArgs, preConfigureArgs)) { - await readPreConfigureArgs(); - updatedSettingsSubkeys.push(subKey); - } + subKey = "makeDirectory"; + let updatedMakeDirectory: string | undefined = + await util.getExpandedSetting(subKey); + if (updatedMakeDirectory) { + updatedMakeDirectory = util.resolvePathToRoot(updatedMakeDirectory); + } + if (updatedMakeDirectory !== makeDirectory) { + // A change in makefile.makeDirectory should trigger an IntelliSense update + // only if the extension is not currently reading from a build log. + extension.getState().configureDirty = + extension.getState().configureDirty || + !buildLog || + !util.checkFileExistsSync(buildLog); + await readMakeDirectory(); + updatedSettingsSubkeys.push(subKey); + } - subKey = "postConfigureScript"; - let updatedPostConfigureScript: string | undefined = await util.getExpandedSetting(subKey); - if (updatedPostConfigureScript) { - updatedPostConfigureScript = util.resolvePathToRoot(updatedPostConfigureScript); - } - if (updatedPostConfigureScript !== postConfigureScript) { - await readPostConfigureScript(); - updatedSettingsSubkeys.push(subKey); - } + subKey = "configurations"; + let updatedMakefileConfigurations: MakefileConfiguration[] | undefined = + await util.getExpandedSetting(subKey); + if ( + !util.areEqual(updatedMakefileConfigurations, makefileConfigurations) + ) { + // todo: skip over updating the IntelliSense configuration provider if the current makefile configuration + // is not among the subobjects that suffered modifications. + extension.getState().configureDirty = true; + await readMakefileConfigurations(); + updatedSettingsSubkeys.push(subKey); + } - subKey = "postConfigureArgs"; - let updatedPostConfigureArgs: string[] | undefined = await util.getExpandedSetting(subKey); - if (updatedPostConfigureArgs && !util.areEqual(updatedPostConfigureArgs, postConfigureArgs)) { - await readPostConfigureArgs(); - updatedSettingsSubkeys.push(subKey); - } + subKey = "dryrunSwitches"; + let updatedDryrunSwitches: string[] | undefined = + await util.getExpandedSetting(subKey); + if (!util.areEqual(updatedDryrunSwitches, dryrunSwitches)) { + // A change in makefile.dryrunSwitches should trigger an IntelliSense update + // only if the extension is not currently reading from a build log. + extension.getState().configureDirty = + extension.getState().configureDirty || + !buildLog || + !util.checkFileExistsSync(buildLog); + await readDryrunSwitches(); + updatedSettingsSubkeys.push(subKey); + } - subKey = "alwaysPreConfigure"; - let updatedAlwaysPreConfigure : boolean | undefined = await util.getExpandedSetting(subKey); - if (updatedAlwaysPreConfigure !== alwaysPreConfigure) { - // No IntelliSense update needed. - await readAlwaysPreConfigure(); - updatedSettingsSubkeys.push(subKey); - } + subKey = "additionalCompilerNames"; + let updatedAdditionalCompilerNames: string[] | undefined = + await util.getExpandedSetting(subKey); + if ( + !util.areEqual(updatedAdditionalCompilerNames, additionalCompilerNames) + ) { + await readAdditionalCompilerNames(); + updatedSettingsSubkeys.push(subKey); + } - subKey = "alwaysPostConfigure"; - let updatedAlwaysPostConfigure: boolean | undefined = await util.getExpandedSetting(subKey); - if (updatedAlwaysPostConfigure !== alwaysPostConfigure) { - await readAlwaysPostConfigure(); - updatedSettingsSubkeys.push(subKey); - } + subKey = "excludeCompilerNames"; + let updatedExcludeCompilerNames: string[] | undefined = + await util.getExpandedSetting(subKey); + if (!util.areEqual(updatedExcludeCompilerNames, excludeCompilerNames)) { + await readExcludeCompilerNames(); + updatedSettingsSubkeys.push(subKey); + } - subKey = "configurationCachePath"; - let oldConfigurationCachePath = configurationCachePath; - await readConfigurationCachePath(); - if (oldConfigurationCachePath !== configurationCachePath) { - // A change in makefile.configurationCachePath should trigger an IntelliSense update - // only if the extension is not currently reading from a build log. - extension.getState().configureDirty = extension.getState().configureDirty || - !buildLog || !util.checkFileExistsSync(buildLog); - updatedSettingsSubkeys.push(subKey); - } + subKey = "configureOnOpen"; + let updatedConfigureOnOpen: boolean | undefined = + await util.getExpandedSetting(subKey); + if (updatedConfigureOnOpen !== configureOnOpen) { + await readConfigureOnOpen(); + updatedSettingsSubkeys.push(subKey); + } - subKey = "makePath"; - let updatedMakePath : string | undefined = await util.getExpandedSetting(subKey); - if (updatedMakePath !== makePath) { - // Not very likely, but it is safe to consider that a different make tool - // may produce a different dry-run output with potential impact on IntelliSense, - // so trigger an update (unless we read from a build log). - extension.getState().configureDirty = extension.getState().configureDirty || - !buildLog || !util.checkFileExistsSync(buildLog); - await readMakePath(); - updatedSettingsSubkeys.push(subKey); - } + subKey = "configureOnEdit"; + let updatedConfigureOnEdit: boolean | undefined = + await util.getExpandedSetting(subKey); + if (updatedConfigureOnEdit !== configureOnEdit) { + await readConfigureOnEdit(); + updatedSettingsSubkeys.push(subKey); + } - subKey = "makefilePath"; - let updatedMakefilePath : string | undefined = await util.getExpandedSetting(subKey); - if (updatedMakefilePath) { - updatedMakefilePath = util.resolvePathToRoot(updatedMakefilePath); - } - if (updatedMakefilePath !== makefilePath) { - // A change in makefile.makefilePath should trigger an IntelliSense update - // only if the extension is not currently reading from a build log. - extension.getState().configureDirty = extension.getState().configureDirty || - !buildLog || !util.checkFileExistsSync(buildLog); - await readMakefilePath(); - updatedSettingsSubkeys.push(subKey); - } + subKey = "configureAfterCommand"; + let updatedConfigureAfterCommand: boolean | undefined = + await util.getExpandedSetting(subKey); + if (updatedConfigureAfterCommand !== configureAfterCommand) { + await readConfigureAfterCommand(); + updatedSettingsSubkeys.push(subKey); + } - subKey = "makeDirectory"; - let updatedMakeDirectory : string | undefined = await util.getExpandedSetting(subKey); - if (updatedMakeDirectory) { - updatedMakeDirectory = util.resolvePathToRoot(updatedMakeDirectory); - } - if (updatedMakeDirectory !== makeDirectory) { - // A change in makefile.makeDirectory should trigger an IntelliSense update - // only if the extension is not currently reading from a build log. - extension.getState().configureDirty = extension.getState().configureDirty || - !buildLog || !util.checkFileExistsSync(buildLog); - await readMakeDirectory(); - updatedSettingsSubkeys.push(subKey); - } + subKey = "phonyOnlyTargets"; + let updatedPhonyOnlyTargets: boolean | undefined = + await util.getExpandedSetting(subKey); + if (updatedPhonyOnlyTargets !== phonyOnlyTargets) { + await readPhonyOnlyTargets(); + updatedSettingsSubkeys.push(subKey); + } - subKey = "configurations"; - let updatedMakefileConfigurations : MakefileConfiguration[] | undefined = await util.getExpandedSetting(subKey); - if (!util.areEqual(updatedMakefileConfigurations, makefileConfigurations)) { - // todo: skip over updating the IntelliSense configuration provider if the current makefile configuration - // is not among the subobjects that suffered modifications. - extension.getState().configureDirty = true; - await readMakefileConfigurations(); - updatedSettingsSubkeys.push(subKey); - } + subKey = "saveBeforeBuildOrConfigure"; + let updatedSaveBeforeBuildOrConfigure: boolean | undefined = + await util.getExpandedSetting(subKey); + if (updatedSaveBeforeBuildOrConfigure !== saveBeforeBuildOrConfigure) { + await readSaveBeforeBuildOrConfigure(); + updatedSettingsSubkeys.push(subKey); + } - subKey = "dryrunSwitches"; - let updatedDryrunSwitches : string[] | undefined = await util.getExpandedSetting(subKey); - if (!util.areEqual(updatedDryrunSwitches, dryrunSwitches)) { - // A change in makefile.dryrunSwitches should trigger an IntelliSense update - // only if the extension is not currently reading from a build log. - extension.getState().configureDirty = extension.getState().configureDirty || - !buildLog || !util.checkFileExistsSync(buildLog); - await readDryrunSwitches(); - updatedSettingsSubkeys.push(subKey); - } + subKey = "buildBeforeLaunch"; + let updatedBuildBeforeLaunch: boolean | undefined = + await util.getExpandedSetting(subKey); + if (updatedBuildBeforeLaunch !== buildBeforeLaunch) { + await readBuildBeforeLaunch(); + updatedSettingsSubkeys.push(subKey); + } - subKey = "additionalCompilerNames"; - let updatedAdditionalCompilerNames : string[] | undefined = await util.getExpandedSetting(subKey); - if (!util.areEqual(updatedAdditionalCompilerNames, additionalCompilerNames)) { - await readAdditionalCompilerNames(); - updatedSettingsSubkeys.push(subKey); - } + subKey = "clearOutputBeforeBuild"; + let updatedClearOutputBeforeBuild: boolean | undefined = + await util.getExpandedSetting(subKey); + if (updatedClearOutputBeforeBuild !== clearOutputBeforeBuild) { + await readClearOutputBeforeBuild(); + updatedSettingsSubkeys.push(subKey); + } - subKey = "excludeCompilerNames"; - let updatedExcludeCompilerNames : string[] | undefined = await util.getExpandedSetting(subKey); - if (!util.areEqual(updatedExcludeCompilerNames, excludeCompilerNames)) { - await readExcludeCompilerNames(); - updatedSettingsSubkeys.push(subKey); - } + subKey = "ignoreDirectoryCommands"; + let updatedIgnoreDirectoryCommands: boolean | undefined = + await util.getExpandedSetting(subKey); + if (updatedIgnoreDirectoryCommands !== ignoreDirectoryCommands) { + await readIgnoreDirectoryCommands(); + updatedSettingsSubkeys.push(subKey); + } - subKey = "configureOnOpen"; - let updatedConfigureOnOpen : boolean | undefined = await util.getExpandedSetting(subKey); - if (updatedConfigureOnOpen !== configureOnOpen) { - await readConfigureOnOpen(); - updatedSettingsSubkeys.push(subKey); - } + subKey = "compileCommandsPath"; + let updatedCompileCommandsPath: string | undefined = + await util.getExpandedSetting(subKey); + if (updatedCompileCommandsPath) { + updatedCompileCommandsPath = util.resolvePathToRoot( + updatedCompileCommandsPath + ); + } + if (updatedCompileCommandsPath !== compileCommandsPath) { + await readCompileCommandsPath(); + updatedSettingsSubkeys.push(subKey); + } - subKey = "configureOnEdit"; - let updatedConfigureOnEdit : boolean | undefined = await util.getExpandedSetting(subKey); - if (updatedConfigureOnEdit !== configureOnEdit) { - await readConfigureOnEdit(); - updatedSettingsSubkeys.push(subKey); - } + subKey = "panel.visibility"; + let wasLocalDebugEnabled: boolean = isOptionalFeatureEnabled("debug"); + let wasLocalRunningEnabled: boolean = isOptionalFeatureEnabled("run"); + await readFeaturesVisibility(); + if (vscode.workspace.isTrusted) { + enableOptionallyVisibleCommands(); + } + let isLocalDebugEnabled: boolean = isOptionalFeatureEnabled("debug"); + let isLocalRunningEnabled: boolean = isOptionalFeatureEnabled("run"); + if ( + wasLocalDebugEnabled !== isLocalDebugEnabled || + wasLocalRunningEnabled !== isLocalRunningEnabled + ) { + extension._projectOutlineProvider.updateTree(); + updatedSettingsSubkeys.push(subKey); + } - subKey = "configureAfterCommand"; - let updatedConfigureAfterCommand : boolean | undefined = await util.getExpandedSetting(subKey); - if (updatedConfigureAfterCommand !== configureAfterCommand) { - await readConfigureAfterCommand(); - updatedSettingsSubkeys.push(subKey); - } + // Final updates in some constructs that depend on more than one of the above settings. + await analyzeConfigureParams(); + await extension._projectOutlineProvider.updateMakePathInfo( + getConfigurationMakeCommand() + ); + await extension._projectOutlineProvider.updateMakefilePathInfo( + getConfigurationMakefile() + ); + await extension._projectOutlineProvider.updateBuildLogPathInfo( + getConfigurationBuildLog() + ); - subKey = "phonyOnlyTargets"; - let updatedPhonyOnlyTargets : boolean | undefined = await util.getExpandedSetting(subKey); - if (updatedPhonyOnlyTargets !== phonyOnlyTargets) { - await readPhonyOnlyTargets(); - updatedSettingsSubkeys.push(subKey); - } - - subKey = "saveBeforeBuildOrConfigure"; - let updatedSaveBeforeBuildOrConfigure : boolean | undefined = await util.getExpandedSetting(subKey); - if (updatedSaveBeforeBuildOrConfigure !== saveBeforeBuildOrConfigure) { - await readSaveBeforeBuildOrConfigure(); - updatedSettingsSubkeys.push(subKey); - } - - subKey = "buildBeforeLaunch"; - let updatedBuildBeforeLaunch : boolean | undefined = await util.getExpandedSetting(subKey); - if (updatedBuildBeforeLaunch !== buildBeforeLaunch) { - await readBuildBeforeLaunch(); - updatedSettingsSubkeys.push(subKey); - } - - subKey = "clearOutputBeforeBuild"; - let updatedClearOutputBeforeBuild : boolean | undefined = await util.getExpandedSetting(subKey); - if (updatedClearOutputBeforeBuild !== clearOutputBeforeBuild) { - await readClearOutputBeforeBuild(); - updatedSettingsSubkeys.push(subKey); - } - - subKey = "ignoreDirectoryCommands"; - let updatedIgnoreDirectoryCommands : boolean | undefined = await util.getExpandedSetting(subKey); - if (updatedIgnoreDirectoryCommands !== ignoreDirectoryCommands) { - await readIgnoreDirectoryCommands(); - updatedSettingsSubkeys.push(subKey); - } - - subKey = "compileCommandsPath"; - let updatedCompileCommandsPath: string | undefined = await util.getExpandedSetting(subKey); - if (updatedCompileCommandsPath) { - updatedCompileCommandsPath = util.resolvePathToRoot(updatedCompileCommandsPath); - } - if (updatedCompileCommandsPath !== compileCommandsPath) { - await readCompileCommandsPath(); - updatedSettingsSubkeys.push(subKey); - } - - subKey = "panel.visibility"; - let wasLocalDebugEnabled: boolean = isOptionalFeatureEnabled("debug"); - let wasLocalRunningEnabled: boolean = isOptionalFeatureEnabled("run"); - await readFeaturesVisibility(); - if (vscode.workspace.isTrusted) { - enableOptionallyVisibleCommands(); - } - let isLocalDebugEnabled: boolean = isOptionalFeatureEnabled("debug"); - let isLocalRunningEnabled: boolean = isOptionalFeatureEnabled("run"); - if ((wasLocalDebugEnabled !== isLocalDebugEnabled) || (wasLocalRunningEnabled !== isLocalRunningEnabled)) { - extension._projectOutlineProvider.updateTree(); - updatedSettingsSubkeys.push(subKey); - } - - // Final updates in some constructs that depend on more than one of the above settings. - await analyzeConfigureParams(); - await extension._projectOutlineProvider.updateMakePathInfo(getConfigurationMakeCommand()); - await extension._projectOutlineProvider.updateMakefilePathInfo(getConfigurationMakefile()); - await extension._projectOutlineProvider.updateBuildLogPathInfo(getConfigurationBuildLog()); - - // Report all the settings changes detected by now. - // TODO: to avoid unnecessary telemetry processing, evaluate whether the changes done - // in the object makefile.launchConfigurations and makefile.configurations - // apply exactly to the current launch configuration, since we don't collect and aggregate - // information from all the array yet. - updatedSettingsSubkeys.forEach(async (subKey) => { - let key: string = keyRoot + "." + subKey; - logger.message(`${key} setting changed.`, "Verbose"); - try { - // For settings that use "." in their name, make sure we send the right object - // to the telemetry function. Currently, the schema for such a setting - // is represented differently than the workspace setting value. - let settingObj: any; - let workspaceConfiguration: vscode.WorkspaceConfiguration = vscode.workspace.getConfiguration(keyRoot); - if (subKey.includes(".")) { - const subKeys: string[] = subKey.split("."); - settingObj = workspaceConfiguration; - subKeys.forEach(key => { - settingObj = settingObj[key]; - }); - } else { - settingObj = workspaceConfiguration[subKey]; - } - - telemetryProperties = await telemetry.analyzeSettings(settingObj, key, - util.thisExtensionPackage().contributes.configuration.properties[key], - false, telemetryProperties); - } catch (e) { - logger.message(e.message); - } + // Report all the settings changes detected by now. + // TODO: to avoid unnecessary telemetry processing, evaluate whether the changes done + // in the object makefile.launchConfigurations and makefile.configurations + // apply exactly to the current launch configuration, since we don't collect and aggregate + // information from all the array yet. + updatedSettingsSubkeys.forEach(async (subKey) => { + let key: string = keyRoot + "." + subKey; + logger.message(`${key} setting changed.`, "Verbose"); + try { + // For settings that use "." in their name, make sure we send the right object + // to the telemetry function. Currently, the schema for such a setting + // is represented differently than the workspace setting value. + let settingObj: any; + let workspaceConfiguration: vscode.WorkspaceConfiguration = + vscode.workspace.getConfiguration(keyRoot); + if (subKey.includes(".")) { + const subKeys: string[] = subKey.split("."); + settingObj = workspaceConfiguration; + subKeys.forEach((key) => { + settingObj = settingObj[key]; }); + } else { + settingObj = workspaceConfiguration[subKey]; + } - if (telemetryProperties && util.hasProperties(telemetryProperties)) { - telemetry.logEvent("settingsChanged", telemetryProperties); - } + telemetryProperties = await telemetry.analyzeSettings( + settingObj, + key, + util.thisExtensionPackage().contributes.configuration.properties[ + key + ], + false, + telemetryProperties + ); + } catch (e) { + logger.message(e.message); } - }); + }); + + if (telemetryProperties && util.hasProperties(telemetryProperties)) { + telemetry.logEvent("settingsChanged", telemetryProperties); + } + } + }); } -export async function setConfigurationByName(configurationName: string): Promise { - extension.getState().buildConfiguration = configurationName; - logger.message(`Setting configuration - ${configurationName}`); - logger.message("Re-reading settings after configuration change."); - await setCurrentMakefileConfiguration(configurationName); - // Refresh settings, they may reference variables or commands reading state configuration var (${configuration}). - await initFromSettings(); - extension._projectOutlineProvider.updateConfiguration(configurationName); +export async function setConfigurationByName( + configurationName: string +): Promise { + extension.getState().buildConfiguration = configurationName; + logger.message(`Setting configuration - ${configurationName}`); + logger.message("Re-reading settings after configuration change."); + await setCurrentMakefileConfiguration(configurationName); + // Refresh settings, they may reference variables or commands reading state configuration var (${configuration}). + await initFromSettings(); + extension._projectOutlineProvider.updateConfiguration(configurationName); } export function prepareConfigurationsQuickPick(): string[] { - const items: string[] = makefileConfigurations.map((k => { - return k.name; - })); + const items: string[] = makefileConfigurations.map((k) => { + return k.name; + }); - if (items.length === 0) { - logger.message("No configurations defined in makefile.configurations setting."); - items.push("Default"); - } + if (items.length === 0) { + logger.message( + "No configurations defined in makefile.configurations setting." + ); + items.push("Default"); + } - return items; + return items; } // Fill a drop-down with all the configuration names defined by the user in makefile.configurations setting. // Triggers a cpptools configuration provider update after selection. export async function setNewConfiguration(): Promise { - // Cannot set a new makefile configuration if the project is currently building or (pre-)configuring. - if (make.blockedByOp(make.Operations.changeConfiguration)) { - return; + // Cannot set a new makefile configuration if the project is currently building or (pre-)configuring. + if (make.blockedByOp(make.Operations.changeConfiguration)) { + return; + } + + const items: string[] = prepareConfigurationsQuickPick(); + + let options: vscode.QuickPickOptions = {}; + options.ignoreFocusOut = true; // so that the logger and the quick pick don't compete over focus + const chosen: string | undefined = await vscode.window.showQuickPick( + items, + options + ); + if (chosen && chosen !== getCurrentMakefileConfiguration()) { + let telemetryProperties: telemetry.Properties | null = { + state: "makefileConfiguration", + }; + telemetry.logEvent("stateChanged", telemetryProperties); + + await setConfigurationByName(chosen); + + if (configureAfterCommand) { + logger.message( + "Automatically reconfiguring the project after a makefile configuration change." + ); + await make.cleanConfigure( + make.TriggeredBy.configureAfterConfigurationChange + ); } - const items: string[] = prepareConfigurationsQuickPick(); + // Refresh telemetry for this new makefile configuration + // (this will find the corresponding item in the makefile.configurations array + // and report all the relevant settings of that object). + // Because of this, the event name is still "settingsChanged", even if + // we're doing a state change now. + let keyRoot: string = "makefile"; + let subKey: string = "configurations"; + let key: string = keyRoot + "." + subKey; + let workspaceConfiguration: vscode.WorkspaceConfiguration = + vscode.workspace.getConfiguration(keyRoot); + telemetryProperties = {}; - let options: vscode.QuickPickOptions = {}; - options.ignoreFocusOut = true; // so that the logger and the quick pick don't compete over focus - const chosen: string | undefined = await vscode.window.showQuickPick(items, options); - if (chosen && chosen !== getCurrentMakefileConfiguration()) { - let telemetryProperties: telemetry.Properties | null = { - state: "makefileConfiguration" - }; - telemetry.logEvent("stateChanged", telemetryProperties); + // We should have at least one item in the configurations array + // if the extension changes state for launch configuration, + // but guard just in case. + let makefileonfigurationSetting: any = workspaceConfiguration[subKey]; + if (makefileonfigurationSetting) { + try { + telemetryProperties = await telemetry.analyzeSettings( + makefileonfigurationSetting, + key, + util.thisExtensionPackage().contributes.configuration.properties[key], + true, + telemetryProperties + ); + } catch (e) { + logger.message(e.message); + } - await setConfigurationByName(chosen); - - if (configureAfterCommand) { - logger.message("Automatically reconfiguring the project after a makefile configuration change."); - await make.cleanConfigure(make.TriggeredBy.configureAfterConfigurationChange); - } - - // Refresh telemetry for this new makefile configuration - // (this will find the corresponding item in the makefile.configurations array - // and report all the relevant settings of that object). - // Because of this, the event name is still "settingsChanged", even if - // we're doing a state change now. - let keyRoot: string = "makefile"; - let subKey: string = "configurations"; - let key: string = keyRoot + "." + subKey; - let workspaceConfiguration: vscode.WorkspaceConfiguration = vscode.workspace.getConfiguration(keyRoot); - telemetryProperties = {}; - - // We should have at least one item in the configurations array - // if the extension changes state for launch configuration, - // but guard just in case. - let makefileonfigurationSetting: any = workspaceConfiguration[subKey]; - if (makefileonfigurationSetting) { - try { - telemetryProperties = await telemetry.analyzeSettings(makefileonfigurationSetting, key, - util.thisExtensionPackage().contributes.configuration.properties[key], - true, telemetryProperties); - } catch (e) { - logger.message(e.message); - } - - if (telemetryProperties && util.hasProperties(telemetryProperties)) { - telemetry.logEvent("settingsChanged", telemetryProperties); - } - } + if (telemetryProperties && util.hasProperties(telemetryProperties)) { + telemetry.logEvent("settingsChanged", telemetryProperties); + } } + } } -export async function setTargetByName(targetName: string) : Promise { - currentTarget = targetName; - let displayTarget: string = targetName ? currentTarget : "Default"; - statusBar.setTarget(displayTarget); - logger.message(`Setting target ${displayTarget}`); - logger.message("Re-reading settings after target change."); - // Refresh settings, they may reference variables or commands reading state target var (${buildTarget}). - extension.getState().buildTarget = currentTarget; - await initFromSettings(); - extension._projectOutlineProvider.updateBuildTarget(targetName); +export async function setTargetByName(targetName: string): Promise { + currentTarget = targetName; + let displayTarget: string = targetName ? currentTarget : "Default"; + statusBar.setTarget(displayTarget); + logger.message(`Setting target ${displayTarget}`); + logger.message("Re-reading settings after target change."); + // Refresh settings, they may reference variables or commands reading state target var (${buildTarget}). + extension.getState().buildTarget = currentTarget; + await initFromSettings(); + extension._projectOutlineProvider.updateBuildTarget(targetName); } // Fill a drop-down with all the target names run by building the makefile for the current configuration // Triggers a cpptools configuration provider update after selection. // TODO: change the UI list to multiple selections mode and store an array of current active targets export async function selectTarget(): Promise { - // Cannot select a new target if the project is currently building or (pre-)configuring. - if (make.blockedByOp(make.Operations.changeBuildTarget)) { - return; + // Cannot select a new target if the project is currently building or (pre-)configuring. + if (make.blockedByOp(make.Operations.changeBuildTarget)) { + return; + } + + // warn about an out of date configure state and configure if makefile.configureAfterCommand allows. + if ( + extension.getState().configureDirty || + // The configure state might not be dirty from the last session but if the project is set to skip + // configure on open and no configure happened yet we still must warn. + (configureOnOpen === false && !extension.getCompletedConfigureInSession()) + ) { + logger.message( + "The project needs a configure to populate the build targets correctly." + ); + if (configureAfterCommand) { + let retc: number = await make.configure( + make.TriggeredBy.configureBeforeTargetChange + ); + if (retc !== make.ConfigureBuildReturnCodeTypes.success) { + logger.message( + "The build targets list may not be accurate because configure failed." + ); + } } + } - // warn about an out of date configure state and configure if makefile.configureAfterCommand allows. - if (extension.getState().configureDirty || - // The configure state might not be dirty from the last session but if the project is set to skip - // configure on open and no configure happened yet we still must warn. - (configureOnOpen === false && !extension.getCompletedConfigureInSession())) { - logger.message("The project needs a configure to populate the build targets correctly."); - if (configureAfterCommand) { - let retc: number = await make.configure(make.TriggeredBy.configureBeforeTargetChange); - if (retc !== make.ConfigureBuildReturnCodeTypes.success) { - logger.message("The build targets list may not be accurate because configure failed."); - } - } - } - - let options: vscode.QuickPickOptions = {}; - options.ignoreFocusOut = true; // so that the logger and the quick pick don't compete over focus - - // Ensure "all" is always available as a target to select. - // There are scenarios when "all" might not be present in the list of available targets, - // for example when the extension is using a build log or dryrun cache of a previous state - // when a particular target was selected and a dryrun applied on that is producing a subset of targets, - // making it impossible to select "all" back again without resetting the Makefile Tools state - // or switching to a different makefile configuration or implementing an editable target quick pick. - // Another situation where "all" would inconveniently miss from the quick pick is when the user is - // providing a build log without the required verbosity for parsing targets (-p or --print-data-base switches). - // When the extension is not reading from build log or dryrun cache, we have logic to prevent - // "all" from getting lost: make sure the target is not appended to the make invocation - // whose output is used to parse the targets (as opposed to parsing for IntelliSense or launch targets - // when the current target must be appended to the make command). - if (!buildTargets.includes("all")) { - buildTargets.push("all"); - } - - const chosen: string | undefined = await vscode.window.showQuickPick(buildTargets, options); - - if (chosen && chosen !== getCurrentTarget()) { - const telemetryProperties: telemetry.Properties = { - state: "buildTarget" - }; - telemetry.logEvent("stateChanged", telemetryProperties); - - await setTargetByName(chosen); - - if (configureAfterCommand) { - // The set of build targets remains the same even if the current target has changed - logger.message("Automatically reconfiguring the project after a build target change."); - await make.cleanConfigure(make.TriggeredBy.configureAfterTargetChange, false); - } + let options: vscode.QuickPickOptions = {}; + options.ignoreFocusOut = true; // so that the logger and the quick pick don't compete over focus + + // Ensure "all" is always available as a target to select. + // There are scenarios when "all" might not be present in the list of available targets, + // for example when the extension is using a build log or dryrun cache of a previous state + // when a particular target was selected and a dryrun applied on that is producing a subset of targets, + // making it impossible to select "all" back again without resetting the Makefile Tools state + // or switching to a different makefile configuration or implementing an editable target quick pick. + // Another situation where "all" would inconveniently miss from the quick pick is when the user is + // providing a build log without the required verbosity for parsing targets (-p or --print-data-base switches). + // When the extension is not reading from build log or dryrun cache, we have logic to prevent + // "all" from getting lost: make sure the target is not appended to the make invocation + // whose output is used to parse the targets (as opposed to parsing for IntelliSense or launch targets + // when the current target must be appended to the make command). + if (!buildTargets.includes("all")) { + buildTargets.push("all"); + } + + const chosen: string | undefined = await vscode.window.showQuickPick( + buildTargets, + options + ); + + if (chosen && chosen !== getCurrentTarget()) { + const telemetryProperties: telemetry.Properties = { + state: "buildTarget", + }; + telemetry.logEvent("stateChanged", telemetryProperties); + + await setTargetByName(chosen); + + if (configureAfterCommand) { + // The set of build targets remains the same even if the current target has changed + logger.message( + "Automatically reconfiguring the project after a build target change." + ); + await make.cleanConfigure( + make.TriggeredBy.configureAfterTargetChange, + false + ); } + } } // The 'name' of a launch configuration is a string following this syntax: @@ -1739,128 +2245,169 @@ export async function selectTarget(): Promise { // the given binary in the exact same way more than once), incorporate also the containing target // name in the syntax (or, since in theory one can write a makefile target to run the same binary // in the same way more than once, add some number suffix). -export async function setLaunchConfigurationByName(launchConfigurationName: string) : Promise { - // Find the matching entry in the array of launch configurations - // or generate a new entry in settings if none are found. - currentLaunchConfiguration = getLaunchConfiguration(launchConfigurationName); - if (!currentLaunchConfiguration) { - currentLaunchConfiguration = await stringToLaunchConfiguration(launchConfigurationName); - if (currentLaunchConfiguration) { - // Read again all launch configurations from settings, so that we push this incoming into that array as well - // because we want to persist the original unexpanded content of launch configurations. - let workspaceConfiguration: vscode.WorkspaceConfiguration = vscode.workspace.getConfiguration("makefile"); - let launchConfigAsInSettings: LaunchConfiguration[] = workspaceConfiguration.get("launchConfigurations") || []; - launchConfigAsInSettings.push(currentLaunchConfiguration); - // Push into the processed 'in-memory' launch configurations array as well. - launchConfigurations.push(currentLaunchConfiguration); - await workspaceConfiguration.update("launchConfigurations", launchConfigAsInSettings); - logger.message(`Inserting a new entry for ${launchConfigurationName} in the array of makefile.launchConfigurations. ` + - "You may define any additional debug properties for it in settings."); - } - } - +export async function setLaunchConfigurationByName( + launchConfigurationName: string +): Promise { + // Find the matching entry in the array of launch configurations + // or generate a new entry in settings if none are found. + currentLaunchConfiguration = getLaunchConfiguration(launchConfigurationName); + if (!currentLaunchConfiguration) { + currentLaunchConfiguration = await stringToLaunchConfiguration( + launchConfigurationName + ); if (currentLaunchConfiguration) { - logger.message(`Setting current launch target "${launchConfigurationName}"`); - extension.getState().launchConfiguration = launchConfigurationName; - statusBar.setLaunchConfiguration(launchConfigurationName); - } else { - if (launchConfigurationName === "") { - logger.message("Unsetting the current launch configuration."); - } else { - logger.message(`A problem occured while analyzing launch configuration name ${launchConfigurationName}. Current launch configuration is unset.`); - } - extension.getState().launchConfiguration = undefined; - statusBar.setLaunchConfiguration("No launch configuration set"); + // Read again all launch configurations from settings, so that we push this incoming into that array as well + // because we want to persist the original unexpanded content of launch configurations. + let workspaceConfiguration: vscode.WorkspaceConfiguration = + vscode.workspace.getConfiguration("makefile"); + let launchConfigAsInSettings: LaunchConfiguration[] = + workspaceConfiguration.get( + "launchConfigurations" + ) || []; + launchConfigAsInSettings.push(currentLaunchConfiguration); + // Push into the processed 'in-memory' launch configurations array as well. + launchConfigurations.push(currentLaunchConfiguration); + await workspaceConfiguration.update( + "launchConfigurations", + launchConfigAsInSettings + ); + logger.message( + `Inserting a new entry for ${launchConfigurationName} in the array of makefile.launchConfigurations. ` + + "You may define any additional debug properties for it in settings." + ); } + } - // Refresh settings, they may reference variables or commands reading launch targets commands: ${command:makefile.getLaunchTargetPath} and others... - logger.message("Re-reading settings after launch target change."); - await initFromSettings(); - await extension._projectOutlineProvider.updateLaunchTarget(launchConfigurationName); + if (currentLaunchConfiguration) { + logger.message( + `Setting current launch target "${launchConfigurationName}"` + ); + extension.getState().launchConfiguration = launchConfigurationName; + statusBar.setLaunchConfiguration(launchConfigurationName); + } else { + if (launchConfigurationName === "") { + logger.message("Unsetting the current launch configuration."); + } else { + logger.message( + `A problem occured while analyzing launch configuration name ${launchConfigurationName}. Current launch configuration is unset.` + ); + } + extension.getState().launchConfiguration = undefined; + statusBar.setLaunchConfiguration("No launch configuration set"); + } + + // Refresh settings, they may reference variables or commands reading launch targets commands: ${command:makefile.getLaunchTargetPath} and others... + logger.message("Re-reading settings after launch target change."); + await initFromSettings(); + await extension._projectOutlineProvider.updateLaunchTarget( + launchConfigurationName + ); } // Fill a drop-down with all the launch configurations found for binaries built by the makefile // under the scope of the current build configuration and target // Selection updates current launch configuration that will be ready for the next debug/run operation export async function selectLaunchConfiguration(): Promise { - // Cannot select a new launch configuration if the project is currently building or (pre-)configuring. - if (make.blockedByOp(make.Operations.changeLaunchTarget)) { - return; - } + // Cannot select a new launch configuration if the project is currently building or (pre-)configuring. + if (make.blockedByOp(make.Operations.changeLaunchTarget)) { + return; + } - // warn about an out of date configure state and configure if makefile.configureAfterCommand allows. - if (extension.getState().configureDirty || - // The configure state might not be dirty from the last session but if the project is set to skip - // configure on open and no configure happened yet we still must warn. - (configureOnOpen === false && !extension.getCompletedConfigureInSession())) { - logger.message("The project needs a configure to populate the launch targets correctly."); - if (configureAfterCommand) { - let retc: number = await make.configure(make.TriggeredBy.configureBeforeLaunchTargetChange); - if (retc !== make.ConfigureBuildReturnCodeTypes.success) { - logger.message("The launch targets list may not be accurate because configure failed."); - } + // warn about an out of date configure state and configure if makefile.configureAfterCommand allows. + if ( + extension.getState().configureDirty || + // The configure state might not be dirty from the last session but if the project is set to skip + // configure on open and no configure happened yet we still must warn. + (configureOnOpen === false && !extension.getCompletedConfigureInSession()) + ) { + logger.message( + "The project needs a configure to populate the launch targets correctly." + ); + if (configureAfterCommand) { + let retc: number = await make.configure( + make.TriggeredBy.configureBeforeLaunchTargetChange + ); + if (retc !== make.ConfigureBuildReturnCodeTypes.success) { + logger.message( + "The launch targets list may not be accurate because configure failed." + ); + } + } + } + + // TODO: create a quick pick with description and details for items + // to better view the long targets commands + + // In the quick pick, include also any makefile.launchConfigurations entries, + // as long as they exist on disk and without allowing duplicates. + let launchTargetsNames: string[] = [...launchTargets]; + launchConfigurations.forEach((launchConfiguration) => { + if (util.checkFileExistsSync(launchConfiguration.binaryPath)) { + launchTargetsNames.push(launchConfigurationToString(launchConfiguration)); + } + }); + launchTargetsNames = util.sortAndRemoveDuplicates(launchTargetsNames); + let options: vscode.QuickPickOptions = {}; + options.ignoreFocusOut = true; // so that the logger and the quick pick don't compete over focus + if (launchTargets.length === 0) { + options.placeHolder = "No launch targets identified"; + } + const chosen: string | undefined = await vscode.window.showQuickPick( + launchTargetsNames, + options + ); + + if (chosen) { + let currentLaunchConfiguration: LaunchConfiguration | undefined = + getCurrentLaunchConfiguration(); + if ( + !currentLaunchConfiguration || + chosen !== launchConfigurationToString(currentLaunchConfiguration) + ) { + let telemetryProperties: telemetry.Properties | null = { + state: "launchConfiguration", + }; + telemetry.logEvent("stateChanged", telemetryProperties); + + await setLaunchConfigurationByName(chosen); + + // Refresh telemetry for this new launch configuration + // (this will find the corresponding item in the makefile.launchConfigurations array + // and report all the relevant settings of that object). + // Because of this, the event name is still "settingsChanged", even if + // we're doing a state change now. + let keyRoot: string = "makefile"; + let subKey: string = "launchConfigurations"; + let key: string = keyRoot + "." + subKey; + let workspaceConfiguration: vscode.WorkspaceConfiguration = + vscode.workspace.getConfiguration(keyRoot); + telemetryProperties = {}; + + // We should have at least one item in the launchConfigurations array + // if the extension changes state for launch configuration, + // but guard just in case. + let launchConfigurationSetting: any = workspaceConfiguration[subKey]; + if (launchConfigurationSetting) { + try { + telemetryProperties = await telemetry.analyzeSettings( + launchConfigurationSetting, + key, + util.thisExtensionPackage().contributes.configuration.properties[ + key + ], + true, + telemetryProperties + ); + } catch (e) { + logger.message(e.message); } - } - // TODO: create a quick pick with description and details for items - // to better view the long targets commands - - // In the quick pick, include also any makefile.launchConfigurations entries, - // as long as they exist on disk and without allowing duplicates. - let launchTargetsNames: string[] = [...launchTargets]; - launchConfigurations.forEach(launchConfiguration => { - if (util.checkFileExistsSync(launchConfiguration.binaryPath)) { - launchTargetsNames.push(launchConfigurationToString(launchConfiguration)); - } - }); - launchTargetsNames = util.sortAndRemoveDuplicates(launchTargetsNames); - let options: vscode.QuickPickOptions = {}; - options.ignoreFocusOut = true; // so that the logger and the quick pick don't compete over focus - if (launchTargets.length === 0) { - options.placeHolder = "No launch targets identified"; - } - const chosen: string | undefined = await vscode.window.showQuickPick(launchTargetsNames, options); - - if (chosen) { - let currentLaunchConfiguration: LaunchConfiguration | undefined = getCurrentLaunchConfiguration(); - if (!currentLaunchConfiguration || chosen !== launchConfigurationToString(currentLaunchConfiguration)) { - let telemetryProperties: telemetry.Properties | null = { - state: "launchConfiguration" - }; - telemetry.logEvent("stateChanged", telemetryProperties); - - await setLaunchConfigurationByName(chosen); - - // Refresh telemetry for this new launch configuration - // (this will find the corresponding item in the makefile.launchConfigurations array - // and report all the relevant settings of that object). - // Because of this, the event name is still "settingsChanged", even if - // we're doing a state change now. - let keyRoot: string = "makefile"; - let subKey: string = "launchConfigurations"; - let key: string = keyRoot + "." + subKey; - let workspaceConfiguration: vscode.WorkspaceConfiguration = vscode.workspace.getConfiguration(keyRoot); - telemetryProperties = {}; - - // We should have at least one item in the launchConfigurations array - // if the extension changes state for launch configuration, - // but guard just in case. - let launchConfigurationSetting: any = workspaceConfiguration[subKey]; - if (launchConfigurationSetting) { - try { - telemetryProperties = await telemetry.analyzeSettings(launchConfigurationSetting, key, - util.thisExtensionPackage().contributes.configuration.properties[key], - true, telemetryProperties); - } catch (e) { - logger.message(e.message); - } - - if (telemetryProperties && util.hasProperties(telemetryProperties)) { - telemetry.logEvent("settingsChanged", telemetryProperties); - } - } + if (telemetryProperties && util.hasProperties(telemetryProperties)) { + telemetry.logEvent("settingsChanged", telemetryProperties); } + } } + } } // List of targets defined in the makefile project. @@ -1869,8 +2416,12 @@ export async function selectLaunchConfiguration(): Promise { // (like any object produced by the compiler from a source code file). // TODO: filter only the relevant targets (binaries, libraries, etc...) from this list. let buildTargets: string[] = []; -export function getBuildTargets(): string[] { return buildTargets; } -export function setBuildTargets(targets: string[]): void { buildTargets = targets; } +export function getBuildTargets(): string[] { + return buildTargets; +} +export function setBuildTargets(targets: string[]): void { + buildTargets = targets; +} // List of all the binaries built by the current project and all the ways // they may be invoked (from what cwd, with what arguments). @@ -1890,5 +2441,9 @@ export function setBuildTargets(targets: string[]): void { buildTargets = target // out of date and most importantly a subset. We want the quick pick to reflect all the possibilities // that are found available with the current configuration of the project. let launchTargets: string[] = []; -export function getLaunchTargets(): string[] { return launchTargets; } -export function setLaunchTargets(targets: string[]): void { launchTargets = targets; } +export function getLaunchTargets(): string[] { + return launchTargets; +} +export function setLaunchTargets(targets: string[]): void { + launchTargets = targets; +} diff --git a/src/cpptools.ts b/src/cpptools.ts index f5e5336..b100ba0 100644 --- a/src/cpptools.ts +++ b/src/cpptools.ts @@ -3,167 +3,269 @@ // Support for integration with CppTools Custom Configuration Provider -import * as configuration from './configuration'; -import * as logger from './logger'; -import * as make from './make'; -import * as parser from './parser'; -import * as path from 'path'; -import * as util from './util'; -import * as vscode from 'vscode'; -import * as cpp from 'vscode-cpptools'; +import * as configuration from "./configuration"; +import * as logger from "./logger"; +import * as make from "./make"; +import * as parser from "./parser"; +import * as path from "path"; +import * as util from "./util"; +import * as vscode from "vscode"; +import * as cpp from "vscode-cpptools"; -export interface SourceFileConfigurationItem extends cpp.SourceFileConfigurationItem { - readonly compileCommand: parser.CompileCommand; +export interface SourceFileConfigurationItem + extends cpp.SourceFileConfigurationItem { + readonly compileCommand: parser.CompileCommand; } export interface CustomConfigurationProvider { - workspaceBrowse: cpp.WorkspaceBrowseConfiguration; - fileIndex: Map; + workspaceBrowse: cpp.WorkspaceBrowseConfiguration; + fileIndex: Map; } -export class CppConfigurationProvider implements cpp.CustomConfigurationProvider { - public readonly name = 'Makefile Tools'; - public readonly extensionId = 'ms-vscode.makefile-tools'; +export class CppConfigurationProvider + implements cpp.CustomConfigurationProvider +{ + public readonly name = "Makefile Tools"; + public readonly extensionId = "ms-vscode.makefile-tools"; - private workspaceBrowseConfiguration: cpp.WorkspaceBrowseConfiguration = { browsePath: [] }; + private workspaceBrowseConfiguration: cpp.WorkspaceBrowseConfiguration = { + browsePath: [], + }; - private getConfiguration(uri: vscode.Uri): SourceFileConfigurationItem | undefined { - let norm_path: string = path.normalize(uri.fsPath); - if (process.platform === "win32") { - norm_path = norm_path.toUpperCase(); - } - - // First look in the file index computed during the last configure. - // If nothing is found and there is a configure running right now, - // try also the temporary index of the current configure. - let sourceFileConfiguration: SourceFileConfigurationItem | undefined = this.fileIndex.get(norm_path); - if (!sourceFileConfiguration && make.getIsConfiguring()) { - sourceFileConfiguration = make.getDeltaCustomConfigurationProvider().fileIndex.get(norm_path); - logger.message(`Configuration for file ${norm_path} was not found. Searching in the current configure temporary file index.`); - } - - if (!sourceFileConfiguration) { - logger.message(`Configuration for file ${norm_path} was not found. CppTools will set a default configuration.`); - } - - return sourceFileConfiguration; + private getConfiguration( + uri: vscode.Uri + ): SourceFileConfigurationItem | undefined { + let norm_path: string = path.normalize(uri.fsPath); + if (process.platform === "win32") { + norm_path = norm_path.toUpperCase(); } - public async canProvideConfiguration(uri: vscode.Uri): Promise { - return !!this.getConfiguration(uri); + // First look in the file index computed during the last configure. + // If nothing is found and there is a configure running right now, + // try also the temporary index of the current configure. + let sourceFileConfiguration: SourceFileConfigurationItem | undefined = + this.fileIndex.get(norm_path); + if (!sourceFileConfiguration && make.getIsConfiguring()) { + sourceFileConfiguration = make + .getDeltaCustomConfigurationProvider() + .fileIndex.get(norm_path); + logger.message( + `Configuration for file ${norm_path} was not found. Searching in the current configure temporary file index.` + ); } - public async provideConfigurations(uris: vscode.Uri[]): Promise { - return util.dropNulls(uris.map(u => this.getConfiguration(u))); + if (!sourceFileConfiguration) { + logger.message( + `Configuration for file ${norm_path} was not found. CppTools will set a default configuration.` + ); } - // Used when saving all the computed configurations into a cache. - public getCustomConfigurationProvider(): CustomConfigurationProvider { - let provider: CustomConfigurationProvider = { - fileIndex: this.fileIndex, - workspaceBrowse: this.workspaceBrowseConfiguration - }; + return sourceFileConfiguration; + } - return provider; + public async canProvideConfiguration(uri: vscode.Uri): Promise { + return !!this.getConfiguration(uri); + } + + public async provideConfigurations( + uris: vscode.Uri[] + ): Promise { + return util.dropNulls(uris.map((u) => this.getConfiguration(u))); + } + + // Used when saving all the computed configurations into a cache. + public getCustomConfigurationProvider(): CustomConfigurationProvider { + let provider: CustomConfigurationProvider = { + fileIndex: this.fileIndex, + workspaceBrowse: this.workspaceBrowseConfiguration, + }; + + return provider; + } + + // Used to reset all the configurations with what was previously cached. + public setCustomConfigurationProvider( + provider: CustomConfigurationProvider + ): void { + this.fileIndex = provider.fileIndex; + this.workspaceBrowseConfiguration = provider.workspaceBrowse; + } + + // Used to merge a new set of configurations on top of what was calculated during the previous configure. + // If this is clean configure, clear all the arrays before the merge. + public mergeCustomConfigurationProvider( + provider: CustomConfigurationProvider + ): void { + if (make.getConfigureIsClean()) { + this.fileIndex.clear(); + this.workspaceBrowseConfiguration = { + browsePath: [], + compilerArgs: [], + compilerPath: undefined, + standard: undefined, + windowsSdkVersion: undefined, + }; } - // Used to reset all the configurations with what was previously cached. - public setCustomConfigurationProvider(provider: CustomConfigurationProvider): void { - this.fileIndex = provider.fileIndex; - this.workspaceBrowseConfiguration = provider.workspaceBrowse; + let map: Map = this.fileIndex; + provider.fileIndex.forEach(function (value, key): void { + map.set(key, value); + }); + + this.workspaceBrowseConfiguration = { + browsePath: util.sortAndRemoveDuplicates( + this.workspaceBrowseConfiguration.browsePath.concat( + provider.workspaceBrowse.browsePath + ) + ), + compilerArgs: this.workspaceBrowseConfiguration.compilerArgs?.concat( + provider.workspaceBrowse.compilerArgs || [] + ), + compilerPath: provider.workspaceBrowse.compilerPath, + standard: provider.workspaceBrowse.standard, + windowsSdkVersion: provider.workspaceBrowse.windowsSdkVersion, + }; + } + + public async canProvideBrowseConfiguration(): Promise { + return this.workspaceBrowseConfiguration.browsePath.length > 0; + } + + public async canProvideBrowseConfigurationsPerFolder(): Promise { + return false; + } + + public async provideFolderBrowseConfiguration( + _uri: vscode.Uri + ): Promise { + if (_uri.fsPath !== util.getWorkspaceRoot()) { + logger.message("Makefile Tools supports single root for now."); } - // Used to merge a new set of configurations on top of what was calculated during the previous configure. - // If this is clean configure, clear all the arrays before the merge. - public mergeCustomConfigurationProvider(provider: CustomConfigurationProvider): void { - if (make.getConfigureIsClean()) { - this.fileIndex.clear(); - this.workspaceBrowseConfiguration = { - browsePath: [], - compilerArgs: [], - compilerPath: undefined, - standard: undefined, - windowsSdkVersion: undefined - }; - } + return this.workspaceBrowseConfiguration; + } - let map: Map = this.fileIndex; - provider.fileIndex.forEach(function(value, key): void { - map.set(key, value); - }); + public async provideBrowseConfiguration(): Promise { + return this.workspaceBrowseConfiguration; + } + public setBrowseConfiguration( + browseConfiguration: cpp.WorkspaceBrowseConfiguration + ): void { + this.workspaceBrowseConfiguration = browseConfiguration; + } - this.workspaceBrowseConfiguration = { - browsePath: util.sortAndRemoveDuplicates(this.workspaceBrowseConfiguration.browsePath.concat(provider.workspaceBrowse.browsePath)), - compilerArgs: this.workspaceBrowseConfiguration.compilerArgs?.concat(provider.workspaceBrowse.compilerArgs || []), - compilerPath: provider.workspaceBrowse.compilerPath, - standard: provider.workspaceBrowse.standard, - windowsSdkVersion: provider.workspaceBrowse.windowsSdkVersion - }; + public dispose(): void {} + + private fileIndex = new Map(); + + public logConfigurationProviderBrowse(): void { + logger.message( + "Sending Workspace Browse Configuration: -----------------------------------", + "Verbose" + ); + logger.message( + " Browse Path: " + + this.workspaceBrowseConfiguration.browsePath.join(";"), + "Verbose" + ); + logger.message( + " Standard: " + this.workspaceBrowseConfiguration.standard, + "Verbose" + ); + logger.message( + " Compiler Path: " + this.workspaceBrowseConfiguration.compilerPath, + "Verbose" + ); + logger.message( + " Compiler Arguments: " + + this.workspaceBrowseConfiguration.compilerArgs?.join(";"), + "Verbose" + ); + if ( + process.platform === "win32" && + this.workspaceBrowseConfiguration.windowsSdkVersion + ) { + logger.message( + " Windows SDK Version: " + + this.workspaceBrowseConfiguration.windowsSdkVersion, + "Verbose" + ); } + logger.message( + "----------------------------------------------------------------------------", + "Verbose" + ); + } - public async canProvideBrowseConfiguration(): Promise { - return this.workspaceBrowseConfiguration.browsePath.length > 0; + public logConfigurationProviderItem( + filePath: SourceFileConfigurationItem, + fromCache: boolean = false + ): void { + let uriObj: vscode.Uri = filePath.uri; + logger.message( + "Sending configuration " + + (fromCache ? "(from cache) " : "") + + "for file " + + uriObj.fsPath + + " -----------------------------------", + "Normal" + ); + logger.message( + " Defines: " + filePath.configuration.defines.join(";"), + "Verbose" + ); + logger.message( + " Includes: " + filePath.configuration.includePath.join(";"), + "Verbose" + ); + if (filePath.configuration.forcedInclude) { + logger.message( + " Force Includes: " + filePath.configuration.forcedInclude.join(";"), + "Verbose" + ); } - - public async canProvideBrowseConfigurationsPerFolder(): Promise { - return false; + logger.message( + " Standard: " + filePath.configuration.standard, + "Verbose" + ); + logger.message( + " IntelliSense Mode: " + filePath.configuration.intelliSenseMode, + "Verbose" + ); + logger.message( + " Compiler Path: " + filePath.configuration.compilerPath, + "Verbose" + ); + logger.message( + " Compiler Arguments: " + + filePath.configuration.compilerArgs?.join(";"), + "Verbose" + ); + if ( + process.platform === "win32" && + filePath.configuration.windowsSdkVersion + ) { + logger.message( + " Windows SDK Version: " + filePath.configuration.windowsSdkVersion, + "Verbose" + ); } + logger.message( + "---------------------------------------------------------------------------------------------------", + "Verbose" + ); + } - public async provideFolderBrowseConfiguration(_uri: vscode.Uri): Promise { - if (_uri.fsPath !== util.getWorkspaceRoot()) { - logger.message("Makefile Tools supports single root for now."); - } + public logConfigurationProviderComplete(): void { + if (configuration.getLoggingLevel() !== "Normal") { + this.logConfigurationProviderBrowse(); - return this.workspaceBrowseConfiguration; - } - - public async provideBrowseConfiguration(): Promise { return this.workspaceBrowseConfiguration; } - public setBrowseConfiguration(browseConfiguration: cpp.WorkspaceBrowseConfiguration): void { this.workspaceBrowseConfiguration = browseConfiguration; } - - public dispose(): void { } - - private fileIndex = new Map(); - - public logConfigurationProviderBrowse(): void { - logger.message("Sending Workspace Browse Configuration: -----------------------------------", "Verbose"); - logger.message(" Browse Path: " + this.workspaceBrowseConfiguration.browsePath.join(";"), "Verbose"); - logger.message(" Standard: " + this.workspaceBrowseConfiguration.standard, "Verbose"); - logger.message(" Compiler Path: " + this.workspaceBrowseConfiguration.compilerPath, "Verbose"); - logger.message(" Compiler Arguments: " + this.workspaceBrowseConfiguration.compilerArgs?.join(";"), "Verbose"); - if (process.platform === "win32" && this.workspaceBrowseConfiguration.windowsSdkVersion) { - logger.message(" Windows SDK Version: " + this.workspaceBrowseConfiguration.windowsSdkVersion, "Verbose"); - } - logger.message("----------------------------------------------------------------------------", "Verbose"); - } - - public logConfigurationProviderItem(filePath: SourceFileConfigurationItem, fromCache: boolean = false): void { - let uriObj: vscode.Uri = filePath.uri; - logger.message("Sending configuration " + (fromCache ? "(from cache) " : "") + "for file " + uriObj.fsPath + " -----------------------------------", "Normal"); - logger.message(" Defines: " + filePath.configuration.defines.join(";"), "Verbose"); - logger.message(" Includes: " + filePath.configuration.includePath.join(";"), "Verbose"); - if (filePath.configuration.forcedInclude) { - logger.message(" Force Includes: " + filePath.configuration.forcedInclude.join(";"), "Verbose"); - } - logger.message(" Standard: " + filePath.configuration.standard, "Verbose"); - logger.message(" IntelliSense Mode: " + filePath.configuration.intelliSenseMode, "Verbose"); - logger.message(" Compiler Path: " + filePath.configuration.compilerPath, "Verbose"); - logger.message(" Compiler Arguments: " + filePath.configuration.compilerArgs?.join(";"), "Verbose"); - if (process.platform === "win32" && filePath.configuration.windowsSdkVersion) { - logger.message(" Windows SDK Version: " + filePath.configuration.windowsSdkVersion, "Verbose"); - } - logger.message("---------------------------------------------------------------------------------------------------", "Verbose"); - } - - public logConfigurationProviderComplete(): void { - if (configuration.getLoggingLevel() !== "Normal") { - this.logConfigurationProviderBrowse(); - - this.fileIndex.forEach(filePath => { - // logConfigurationProviderComplete is called (so far) only after loading - // the configurations from cache, so mark the boolean to be able to distinguish - // the log entries in case of interleaved output. - this.logConfigurationProviderItem(filePath, true); - }); - } + this.fileIndex.forEach((filePath) => { + // logConfigurationProviderComplete is called (so far) only after loading + // the configurations from cache, so mark the boolean to be able to distinguish + // the log entries in case of interleaved output. + this.logConfigurationProviderItem(filePath, true); + }); } + } } diff --git a/src/extension.ts b/src/extension.ts index 7dfc509..4732e41 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -3,25 +3,28 @@ // Makefile Tools extension -import * as configuration from './configuration'; -import * as cpptools from './cpptools'; -import * as launch from './launch'; +import * as configuration from "./configuration"; +import * as cpptools from "./cpptools"; +import * as launch from "./launch"; import { promises as fs } from "fs"; -import * as logger from './logger'; -import * as make from './make'; -import * as parser from './parser'; -import * as path from 'path'; -import * as state from './state'; -import * as telemetry from './telemetry'; -import * as tree from './tree'; -import * as ui from './ui'; -import * as util from './util'; -import * as vscode from 'vscode'; -import * as cpp from 'vscode-cpptools'; +import * as logger from "./logger"; +import * as make from "./make"; +import * as parser from "./parser"; +import * as path from "path"; +import * as state from "./state"; +import * as telemetry from "./telemetry"; +import * as tree from "./tree"; +import * as ui from "./ui"; +import * as util from "./util"; +import * as vscode from "vscode"; +import * as cpp from "vscode-cpptools"; -import * as nls from 'vscode-nls'; -import { readBuildLog } from './configuration'; -nls.config({ messageFormat: nls.MessageFormat.bundle, bundleFormat: nls.BundleFormat.standalone })(); +import * as nls from "vscode-nls"; +import { readBuildLog } from "./configuration"; +nls.config({ + messageFormat: nls.MessageFormat.bundle, + bundleFormat: nls.BundleFormat.standalone, +})(); const localize: nls.LocalizeFunc = nls.loadMessageBundle(); let statusBar: ui.UI = ui.getUI(); @@ -30,500 +33,755 @@ let launcher: launch.Launcher = launch.getLauncher(); export let extension: MakefileToolsExtension; export class MakefileToolsExtension { - public readonly _projectOutlineProvider = new tree.ProjectOutlineProvider(); - private readonly _projectOutlineTreeView = vscode.window.createTreeView('makefile.outline', { - treeDataProvider: this._projectOutlineProvider, - showCollapseAll: false + public readonly _projectOutlineProvider = new tree.ProjectOutlineProvider(); + private readonly _projectOutlineTreeView = vscode.window.createTreeView( + "makefile.outline", + { + treeDataProvider: this._projectOutlineProvider, + showCollapseAll: false, + } + ); + + private readonly cppConfigurationProvider = + new cpptools.CppConfigurationProvider(); + public getCppConfigurationProvider(): cpptools.CppConfigurationProvider { + return this.cppConfigurationProvider; + } + + private mementoState = new state.StateManager(this.extensionContext); + private cppToolsAPI?: cpp.CppToolsApi; + private cppConfigurationProviderRegister?: Promise; + private compilerFullPath?: string; + + public constructor( + public readonly extensionContext: vscode.ExtensionContext + ) {} + + public updateBuildLogPresent(newValue: boolean): void { + vscode.commands.executeCommand( + "setContext", + "makefile.buildLogFilePresent", + newValue + ); + } + + public updateMakefileFilePresent(newValue: boolean): void { + vscode.commands.executeCommand( + "setContext", + "makefile.makefileFilePresent", + newValue + ); + } + + public getState(): state.StateManager { + return this.mementoState; + } + + public dispose(): void { + this._projectOutlineTreeView.dispose(); + if (this.cppToolsAPI) { + this.cppToolsAPI.dispose(); + } + } + + private fullFeatureSet: boolean = false; + public getFullFeatureSet(): boolean { + return this.fullFeatureSet; + } + public async setFullFeatureSet(newValue: boolean): Promise { + await vscode.commands.executeCommand( + "setContext", + "makefile:fullFeatureSet", + newValue + ); + this.fullFeatureSet = newValue; + } + + // Used for calling cppToolsAPI.notifyReady only once in a VSCode session. + private ranNotifyReadyInSession: boolean = false; + public getRanNotifyReadyInSession(): boolean { + return this.ranNotifyReadyInSession; + } + public setRanNotifyReadyInSession(ran: boolean): void { + this.ranNotifyReadyInSession = ran; + } + + // Similar to state.ranConfigureInCodebaseLifetime, but at the scope of a VSCode session + private completedConfigureInSession: boolean = false; + public getCompletedConfigureInSession(): boolean | undefined { + return this.completedConfigureInSession; + } + public setCompletedConfigureInSession(completed: boolean): void { + this.completedConfigureInSession = completed; + } + + // Register this extension as a new provider or request an update + public async registerCppToolsProvider(): Promise { + await this.ensureCppToolsProviderRegistered(); + + // Call notifyReady earlier than when the provider is updated, + // as soon as we know that we are going to actually parse for IntelliSense. + // This allows CppTools to ask earlier about source files in use + // and Makefile Tools may return a targeted source file configuration + // if it was already computed in our internal arrays (make.ts: customConfigProviderItems). + // If the requested file isn't yet processed, it will get updated when configure is finished. + // TODO: remember all requests that are coming and send an update as soon as we detect + // any of them being pushed into make.customConfigProviderItems. + if (this.cppToolsAPI) { + if (!this.ranNotifyReadyInSession && this.cppToolsAPI.notifyReady) { + this.cppToolsAPI.notifyReady(this.cppConfigurationProvider); + this.setRanNotifyReadyInSession(true); + } + } + } + + // Request a custom config provider update. + public updateCppToolsProvider(): void { + this.cppConfigurationProvider.logConfigurationProviderBrowse(); + + if (this.cppToolsAPI) { + this.cppToolsAPI.didChangeCustomConfiguration( + this.cppConfigurationProvider + ); + } + } + + public ensureCppToolsProviderRegistered(): Promise { + // make sure this extension is registered as provider only once + if (!this.cppConfigurationProviderRegister) { + this.cppConfigurationProviderRegister = this.registerCppTools(); + } + + return this.cppConfigurationProviderRegister; + } + + public getCppToolsVersion(): cpp.Version | undefined { + return this.cppToolsAPI?.getVersion(); + } + + public async registerCppTools(): Promise { + if (!this.cppToolsAPI) { + this.cppToolsAPI = await cpp.getCppToolsApi(cpp.Version.v6); + } + + if (this.cppToolsAPI) { + this.cppToolsAPI.registerCustomConfigurationProvider( + this.cppConfigurationProvider + ); + } + } + + private cummulativeBrowsePath: string[] = []; + public clearCummulativeBrowsePath(): void { + this.cummulativeBrowsePath = []; + } + + public buildCustomConfigurationProvider( + customConfigProviderItem: parser.CustomConfigProviderItem + ): void { + this.compilerFullPath = customConfigProviderItem.compilerFullPath; + let provider: cpptools.CustomConfigurationProvider = + make.getDeltaCustomConfigurationProvider(); + + const configuration: cpp.SourceFileConfiguration = { + defines: customConfigProviderItem.defines, + standard: customConfigProviderItem.standard, + includePath: customConfigProviderItem.includes, + forcedInclude: customConfigProviderItem.forcedIncludes, + intelliSenseMode: customConfigProviderItem.intelliSenseMode, + compilerPath: customConfigProviderItem.compilerFullPath, + compilerArgs: customConfigProviderItem.compilerArgs, + windowsSdkVersion: customConfigProviderItem.windowsSDKVersion, + }; + + // cummulativeBrowsePath incorporates all the files and the includes paths + // of all the compiler invocations of the current configuration + customConfigProviderItem.files.forEach((filePath) => { + let uri: vscode.Uri = vscode.Uri.file(filePath); + let sourceFileConfigurationItem: cpptools.SourceFileConfigurationItem = { + uri, + configuration, + compileCommand: { + command: customConfigProviderItem.line, + directory: customConfigProviderItem.currentPath, + file: filePath, + }, + }; + + // These are the configurations processed during the current configure. + // Store them in the 'delta' file index instead of the final one. + provider.fileIndex.set( + path.normalize( + process.platform === "win32" ? uri.fsPath.toUpperCase() : uri.fsPath + ), + sourceFileConfigurationItem + ); + extension + .getCppConfigurationProvider() + .logConfigurationProviderItem(sourceFileConfigurationItem); + + let folder: string = path.dirname(filePath); + if (!this.cummulativeBrowsePath.includes(folder)) { + this.cummulativeBrowsePath.push(folder); + } }); - private readonly cppConfigurationProvider = new cpptools.CppConfigurationProvider(); - public getCppConfigurationProvider(): cpptools.CppConfigurationProvider { return this.cppConfigurationProvider; } + customConfigProviderItem.includes.forEach((incl) => { + if (!this.cummulativeBrowsePath.includes(incl)) { + this.cummulativeBrowsePath.push(incl); + } + }); - private mementoState = new state.StateManager(this.extensionContext); - private cppToolsAPI?: cpp.CppToolsApi; - private cppConfigurationProviderRegister?: Promise; - private compilerFullPath ?: string; + customConfigProviderItem.forcedIncludes.forEach((fincl) => { + let folder: string = path.dirname(fincl); + if (!this.cummulativeBrowsePath.includes(folder)) { + this.cummulativeBrowsePath.push(fincl); + } + }); - public constructor(public readonly extensionContext: vscode.ExtensionContext) { - } + provider.workspaceBrowse = { + browsePath: this.cummulativeBrowsePath, + standard: customConfigProviderItem.standard, + compilerPath: customConfigProviderItem.compilerFullPath, + compilerArgs: customConfigProviderItem.compilerArgs, + windowsSdkVersion: customConfigProviderItem.windowsSDKVersion, + }; - public updateBuildLogPresent(newValue: boolean): void { - vscode.commands.executeCommand("setContext", "makefile.buildLogFilePresent", newValue); - } + make.setCustomConfigurationProvider(provider); + } - public updateMakefileFilePresent(newValue: boolean): void { - vscode.commands.executeCommand("setContext", "makefile.makefileFilePresent", newValue); - } - - public getState(): state.StateManager { return this.mementoState; } - - public dispose(): void { - this._projectOutlineTreeView.dispose(); - if (this.cppToolsAPI) { - this.cppToolsAPI.dispose(); - } - } - - private fullFeatureSet: boolean = false; - public getFullFeatureSet(): boolean { return this.fullFeatureSet; } - public async setFullFeatureSet(newValue: boolean): Promise { - await vscode.commands.executeCommand('setContext', "makefile:fullFeatureSet", newValue); - this.fullFeatureSet = newValue; - } - - // Used for calling cppToolsAPI.notifyReady only once in a VSCode session. - private ranNotifyReadyInSession: boolean = false; - public getRanNotifyReadyInSession() : boolean { return this.ranNotifyReadyInSession; } - public setRanNotifyReadyInSession(ran: boolean) : void { this.ranNotifyReadyInSession = ran; } - - // Similar to state.ranConfigureInCodebaseLifetime, but at the scope of a VSCode session - private completedConfigureInSession: boolean = false; - public getCompletedConfigureInSession() : boolean | undefined { return this.completedConfigureInSession; } - public setCompletedConfigureInSession(completed: boolean) : void { this.completedConfigureInSession = completed; } - - // Register this extension as a new provider or request an update - public async registerCppToolsProvider(): Promise { - await this.ensureCppToolsProviderRegistered(); - - // Call notifyReady earlier than when the provider is updated, - // as soon as we know that we are going to actually parse for IntelliSense. - // This allows CppTools to ask earlier about source files in use - // and Makefile Tools may return a targeted source file configuration - // if it was already computed in our internal arrays (make.ts: customConfigProviderItems). - // If the requested file isn't yet processed, it will get updated when configure is finished. - // TODO: remember all requests that are coming and send an update as soon as we detect - // any of them being pushed into make.customConfigProviderItems. - if (this.cppToolsAPI) { - if (!this.ranNotifyReadyInSession && this.cppToolsAPI.notifyReady) { - this.cppToolsAPI.notifyReady(this.cppConfigurationProvider); - this.setRanNotifyReadyInSession(true); - } - } - } - - // Request a custom config provider update. - public updateCppToolsProvider(): void { - this.cppConfigurationProvider.logConfigurationProviderBrowse(); - - if (this.cppToolsAPI) { - this.cppToolsAPI.didChangeCustomConfiguration(this.cppConfigurationProvider); - } - } - - public ensureCppToolsProviderRegistered(): Promise { - // make sure this extension is registered as provider only once - if (!this.cppConfigurationProviderRegister) { - this.cppConfigurationProviderRegister = this.registerCppTools(); - } - - return this.cppConfigurationProviderRegister; - } - - public getCppToolsVersion(): cpp.Version | undefined { - return this.cppToolsAPI?.getVersion(); - } - - public async registerCppTools(): Promise { - if (!this.cppToolsAPI) { - this.cppToolsAPI = await cpp.getCppToolsApi(cpp.Version.v6); - } - - if (this.cppToolsAPI) { - this.cppToolsAPI.registerCustomConfigurationProvider(this.cppConfigurationProvider); - } - } - - private cummulativeBrowsePath: string[] = []; - public clearCummulativeBrowsePath(): void { - this.cummulativeBrowsePath = []; - } - - public buildCustomConfigurationProvider(customConfigProviderItem: parser.CustomConfigProviderItem): void { - this.compilerFullPath = customConfigProviderItem.compilerFullPath; - let provider: cpptools.CustomConfigurationProvider = make.getDeltaCustomConfigurationProvider(); - - const configuration: cpp.SourceFileConfiguration = { - defines: customConfigProviderItem.defines, - standard: customConfigProviderItem.standard, - includePath: customConfigProviderItem.includes, - forcedInclude: customConfigProviderItem.forcedIncludes, - intelliSenseMode: customConfigProviderItem.intelliSenseMode, - compilerPath: customConfigProviderItem.compilerFullPath, - compilerArgs: customConfigProviderItem.compilerArgs, - windowsSdkVersion: customConfigProviderItem.windowsSDKVersion - }; - - // cummulativeBrowsePath incorporates all the files and the includes paths - // of all the compiler invocations of the current configuration - customConfigProviderItem.files.forEach(filePath => { - let uri: vscode.Uri = vscode.Uri.file(filePath); - let sourceFileConfigurationItem: cpptools.SourceFileConfigurationItem = { - uri, - configuration, - compileCommand: { - command: customConfigProviderItem.line, - directory: customConfigProviderItem.currentPath, - file: filePath - } - }; - - // These are the configurations processed during the current configure. - // Store them in the 'delta' file index instead of the final one. - provider.fileIndex.set(path.normalize((process.platform === "win32") ? uri.fsPath.toUpperCase() : uri.fsPath), - sourceFileConfigurationItem); - extension.getCppConfigurationProvider().logConfigurationProviderItem(sourceFileConfigurationItem); - - let folder: string = path.dirname(filePath); - if (!this.cummulativeBrowsePath.includes(folder)) { - this.cummulativeBrowsePath.push(folder); - } - }); - - customConfigProviderItem.includes.forEach(incl => { - if (!this.cummulativeBrowsePath.includes(incl)) { - this.cummulativeBrowsePath.push(incl); - } - }); - - customConfigProviderItem.forcedIncludes.forEach(fincl => { - let folder: string = path.dirname(fincl); - if (!this.cummulativeBrowsePath.includes(folder)) { - this.cummulativeBrowsePath.push(fincl); - } - }); - - provider.workspaceBrowse = { - browsePath: this.cummulativeBrowsePath, - standard: customConfigProviderItem.standard, - compilerPath: customConfigProviderItem.compilerFullPath, - compilerArgs: customConfigProviderItem.compilerArgs, - windowsSdkVersion: customConfigProviderItem.windowsSDKVersion - }; - - make.setCustomConfigurationProvider(provider); - } - - public getCompilerFullPath() : string | undefined { return this.compilerFullPath; } + public getCompilerFullPath(): string | undefined { + return this.compilerFullPath; + } } -export async function activate(context: vscode.ExtensionContext): Promise { - if (process.env["MAKEFILE_TOOLS_TESTING"] === "1") { - await vscode.commands.executeCommand('setContext', "makefile:testing", true); - } else { - await vscode.commands.executeCommand('setContext', "makefile:testing", false); - } +export async function activate( + context: vscode.ExtensionContext +): Promise { + if (process.env["MAKEFILE_TOOLS_TESTING"] === "1") { + await vscode.commands.executeCommand( + "setContext", + "makefile:testing", + true + ); + } else { + await vscode.commands.executeCommand( + "setContext", + "makefile:testing", + false + ); + } - statusBar = ui.getUI(); - extension = new MakefileToolsExtension(context); - configuration.disableAllOptionallyVisibleCommands(); - await extension.setFullFeatureSet(false); + statusBar = ui.getUI(); + extension = new MakefileToolsExtension(context); + configuration.disableAllOptionallyVisibleCommands(); + await extension.setFullFeatureSet(false); - telemetry.activate(); - - context.subscriptions.push(vscode.commands.registerCommand('makefile.setBuildConfiguration', async () => { + telemetry.activate(); + + context.subscriptions.push( + vscode.commands.registerCommand( + "makefile.setBuildConfiguration", + async () => { await configuration.setNewConfiguration(); - })); + } + ) + ); - context.subscriptions.push(vscode.commands.registerCommand('makefile.getConfiguration', async () => { - telemetry.logEvent("getConfiguration"); - return configuration.getCurrentMakefileConfiguration(); - })); + context.subscriptions.push( + vscode.commands.registerCommand("makefile.getConfiguration", async () => { + telemetry.logEvent("getConfiguration"); + return configuration.getCurrentMakefileConfiguration(); + }) + ); - context.subscriptions.push(vscode.commands.registerCommand('makefile.setBuildTarget', async () => { - await configuration.selectTarget(); - })); + context.subscriptions.push( + vscode.commands.registerCommand("makefile.setBuildTarget", async () => { + await configuration.selectTarget(); + }) + ); - context.subscriptions.push(vscode.commands.registerCommand('makefile.getBuildTarget', async () => { - telemetry.logEvent("getBuildTarget"); - return configuration.getCurrentTarget() || ""; - })); + context.subscriptions.push( + vscode.commands.registerCommand("makefile.getBuildTarget", async () => { + telemetry.logEvent("getBuildTarget"); + return configuration.getCurrentTarget() || ""; + }) + ); - context.subscriptions.push(vscode.commands.registerCommand('makefile.buildTarget', async () => { - await make.buildTarget(make.TriggeredBy.buildTarget, configuration.getCurrentTarget() || "", false); - })); + context.subscriptions.push( + vscode.commands.registerCommand("makefile.buildTarget", async () => { + await make.buildTarget( + make.TriggeredBy.buildTarget, + configuration.getCurrentTarget() || "", + false + ); + }) + ); - context.subscriptions.push(vscode.commands.registerCommand('makefile.buildCleanTarget', async () => { - await make.buildTarget(make.TriggeredBy.buildCleanTarget, configuration.getCurrentTarget() || "", true); - })); + context.subscriptions.push( + vscode.commands.registerCommand("makefile.buildCleanTarget", async () => { + await make.buildTarget( + make.TriggeredBy.buildCleanTarget, + configuration.getCurrentTarget() || "", + true + ); + }) + ); - context.subscriptions.push(vscode.commands.registerCommand('makefile.buildAll', async () => { - await make.buildTarget(make.TriggeredBy.buildAll, "all", false); - })); + context.subscriptions.push( + vscode.commands.registerCommand("makefile.buildAll", async () => { + await make.buildTarget(make.TriggeredBy.buildAll, "all", false); + }) + ); - context.subscriptions.push(vscode.commands.registerCommand('makefile.buildCleanAll', async () => { - await make.buildTarget(make.TriggeredBy.buildCleanAll, "all", true); - })); + context.subscriptions.push( + vscode.commands.registerCommand("makefile.buildCleanAll", async () => { + await make.buildTarget(make.TriggeredBy.buildCleanAll, "all", true); + }) + ); - context.subscriptions.push(vscode.commands.registerCommand('makefile.setLaunchConfiguration', async () => { + context.subscriptions.push( + vscode.commands.registerCommand( + "makefile.setLaunchConfiguration", + async () => { await configuration.selectLaunchConfiguration(); - })); + } + ) + ); - context.subscriptions.push(vscode.commands.registerCommand('makefile.launchDebug', async () => { - await launcher.debugCurrentTarget(); - })); + context.subscriptions.push( + vscode.commands.registerCommand("makefile.launchDebug", async () => { + await launcher.debugCurrentTarget(); + }) + ); - context.subscriptions.push(vscode.commands.registerCommand('makefile.launchRun', async () => { - await launcher.runCurrentTarget(); - })); + context.subscriptions.push( + vscode.commands.registerCommand("makefile.launchRun", async () => { + await launcher.runCurrentTarget(); + }) + ); + /** Start of commands that shouldn't be exposed in package.json, they are used for command substitution in launch.json and tasks.json. */ + context.subscriptions.push( + vscode.commands.registerCommand("makefile.getLaunchTargetPath", () => { + telemetry.logEvent("getLaunchTargetPath"); + return launcher.getLaunchTargetPath(); + }) + ); - /** Start of commands that shouldn't be exposed in package.json, they are used for command substitution in launch.json and tasks.json. */ - context.subscriptions.push(vscode.commands.registerCommand('makefile.getLaunchTargetPath', () => { - telemetry.logEvent("getLaunchTargetPath"); - return launcher.getLaunchTargetPath(); - })); - - context.subscriptions.push(vscode.commands.registerCommand('makefile.launchTargetPath', () => { + context.subscriptions.push( + vscode.commands.registerCommand("makefile.launchTargetPath", () => { telemetry.logEvent("launchTargetPath"); return launcher.launchTargetPath(); - })); + }) + ); - context.subscriptions.push(vscode.commands.registerCommand('makefile.getLaunchTargetDirectory', () => { - telemetry.logEvent("getLaunchTargetDirectory"); - return launcher.getLaunchTargetDirectory(); - })); + context.subscriptions.push( + vscode.commands.registerCommand("makefile.getLaunchTargetDirectory", () => { + telemetry.logEvent("getLaunchTargetDirectory"); + return launcher.getLaunchTargetDirectory(); + }) + ); - context.subscriptions.push(vscode.commands.registerCommand('makefile.getLaunchTargetFileName', () => { + context.subscriptions.push( + vscode.commands.registerCommand("makefile.getLaunchTargetFileName", () => { telemetry.logEvent("getLaunchTargetFileName"); return launcher.getLaunchTargetFileName(); - })); + }) + ); - context.subscriptions.push(vscode.commands.registerCommand('makefile.launchTargetFileName', () => { + context.subscriptions.push( + vscode.commands.registerCommand("makefile.launchTargetFileName", () => { telemetry.logEvent("launchTargetFileName"); return launcher.launchTargetFileName(); - })); + }) + ); - context.subscriptions.push(vscode.commands.registerCommand('makefile.getLaunchTargetArgs', () => { - telemetry.logEvent("getLaunchTargetArgs"); - return launcher.getLaunchTargetArgs(); - })); + context.subscriptions.push( + vscode.commands.registerCommand("makefile.getLaunchTargetArgs", () => { + telemetry.logEvent("getLaunchTargetArgs"); + return launcher.getLaunchTargetArgs(); + }) + ); - context.subscriptions.push(vscode.commands.registerCommand('makefile.getLaunchTargetArgsConcat', () => { + context.subscriptions.push( + vscode.commands.registerCommand( + "makefile.getLaunchTargetArgsConcat", + () => { telemetry.logEvent("getLaunchTargetArgsConcat"); return launcher.getLaunchTargetArgsConcat(); - })); + } + ) + ); - context.subscriptions.push(vscode.commands.registerCommand('makefile.makeBaseDirectory', () => { - telemetry.logEvent("makeBaseDirectory"); - return configuration.makeBaseDirectory(); - })); - /** End of commands that shouldn't be exposed in package.json, they are used for command substitution in launch.json and tasks.json. */ + context.subscriptions.push( + vscode.commands.registerCommand("makefile.makeBaseDirectory", () => { + telemetry.logEvent("makeBaseDirectory"); + return configuration.makeBaseDirectory(); + }) + ); + /** End of commands that shouldn't be exposed in package.json, they are used for command substitution in launch.json and tasks.json. */ - context.subscriptions.push(vscode.commands.registerCommand('makefile.configure', async () => { - await make.configure(make.TriggeredBy.configure); - })); + context.subscriptions.push( + vscode.commands.registerCommand("makefile.configure", async () => { + await make.configure(make.TriggeredBy.configure); + }) + ); - context.subscriptions.push(vscode.commands.registerCommand('makefile.cleanConfigure', async () => { - await make.cleanConfigure(make.TriggeredBy.cleanConfigure); - })); + context.subscriptions.push( + vscode.commands.registerCommand("makefile.cleanConfigure", async () => { + await make.cleanConfigure(make.TriggeredBy.cleanConfigure); + }) + ); - context.subscriptions.push(vscode.commands.registerCommand('makefile.preConfigure', async () => { - await make.preConfigure(make.TriggeredBy.preconfigure); - })); + context.subscriptions.push( + vscode.commands.registerCommand("makefile.preConfigure", async () => { + await make.preConfigure(make.TriggeredBy.preconfigure); + }) + ); - context.subscriptions.push(vscode.commands.registerCommand('makefile.postConfigure', async () => { - await make.postConfigure(make.TriggeredBy.postConfigure); - })) + context.subscriptions.push( + vscode.commands.registerCommand("makefile.postConfigure", async () => { + await make.postConfigure(make.TriggeredBy.postConfigure); + }) + ); - // Reset state - useful for troubleshooting. - context.subscriptions.push(vscode.commands.registerCommand('makefile.resetState', (reload?: boolean) => { + // Reset state - useful for troubleshooting. + context.subscriptions.push( + vscode.commands.registerCommand( + "makefile.resetState", + (reload?: boolean) => { telemetry.logEvent("commandResetState"); extension.getState().reset(reload); - })); + } + ) + ); - context.subscriptions.push(vscode.commands.registerCommand('makefile.outline.configure', () => { - return vscode.commands.executeCommand("makefile.configure"); - })); + context.subscriptions.push( + vscode.commands.registerCommand("makefile.outline.configure", () => { + return vscode.commands.executeCommand("makefile.configure"); + }) + ); - context.subscriptions.push(vscode.commands.registerCommand('makefile.outline.cleanConfigure', () => { - return vscode.commands.executeCommand("makefile.cleanConfigure"); - })); + context.subscriptions.push( + vscode.commands.registerCommand("makefile.outline.cleanConfigure", () => { + return vscode.commands.executeCommand("makefile.cleanConfigure"); + }) + ); - context.subscriptions.push(vscode.commands.registerCommand('makefile.outline.preConfigure', () => { - return vscode.commands.executeCommand("makefile.preConfigure"); - })); + context.subscriptions.push( + vscode.commands.registerCommand("makefile.outline.preConfigure", () => { + return vscode.commands.executeCommand("makefile.preConfigure"); + }) + ); - context.subscriptions.push(vscode.commands.registerCommand('makefile.outline.postConfigure', () => { - return vscode.commands.executeCommand("makefile.postConfigure"); - })) + context.subscriptions.push( + vscode.commands.registerCommand("makefile.outline.postConfigure", () => { + return vscode.commands.executeCommand("makefile.postConfigure"); + }) + ); - context.subscriptions.push(vscode.commands.registerCommand('makefile.outline.setLaunchConfiguration', () => { - return vscode.commands.executeCommand("makefile.setLaunchConfiguration"); - })); + context.subscriptions.push( + vscode.commands.registerCommand( + "makefile.outline.setLaunchConfiguration", + () => { + return vscode.commands.executeCommand( + "makefile.setLaunchConfiguration" + ); + } + ) + ); - context.subscriptions.push(vscode.commands.registerCommand('makefile.outline.launchDebug', () => { - return vscode.commands.executeCommand("makefile.launchDebug"); - })); + context.subscriptions.push( + vscode.commands.registerCommand("makefile.outline.launchDebug", () => { + return vscode.commands.executeCommand("makefile.launchDebug"); + }) + ); - context.subscriptions.push(vscode.commands.registerCommand('makefile.outline.launchRun', () => { - return vscode.commands.executeCommand("makefile.launchRun"); - })); + context.subscriptions.push( + vscode.commands.registerCommand("makefile.outline.launchRun", () => { + return vscode.commands.executeCommand("makefile.launchRun"); + }) + ); - context.subscriptions.push(vscode.commands.registerCommand('makefile.outline.setBuildTarget', () => { - return vscode.commands.executeCommand("makefile.setBuildTarget"); - })); + context.subscriptions.push( + vscode.commands.registerCommand("makefile.outline.setBuildTarget", () => { + return vscode.commands.executeCommand("makefile.setBuildTarget"); + }) + ); - context.subscriptions.push(vscode.commands.registerCommand('makefile.outline.buildTarget', () => { - return vscode.commands.executeCommand("makefile.buildTarget"); - })); + context.subscriptions.push( + vscode.commands.registerCommand("makefile.outline.buildTarget", () => { + return vscode.commands.executeCommand("makefile.buildTarget"); + }) + ); - context.subscriptions.push(vscode.commands.registerCommand('makefile.outline.buildCleanTarget', () => { - return vscode.commands.executeCommand("makefile.buildCleanTarget"); - })); + context.subscriptions.push( + vscode.commands.registerCommand("makefile.outline.buildCleanTarget", () => { + return vscode.commands.executeCommand("makefile.buildCleanTarget"); + }) + ); - context.subscriptions.push(vscode.commands.registerCommand('makefile.outline.setBuildConfiguration', () => { + context.subscriptions.push( + vscode.commands.registerCommand( + "makefile.outline.setBuildConfiguration", + () => { return vscode.commands.executeCommand("makefile.setBuildConfiguration"); - })); + } + ) + ); - // Read from the workspace state before reading from settings, - // becase the latter may use state info in variable expansion. - configuration.initFromState(); - await configuration.initFromSettings(true); + // Read from the workspace state before reading from settings, + // becase the latter may use state info in variable expansion. + configuration.initFromState(); + await configuration.initFromSettings(true); - const openSettings = async (setting: string) => { - await vscode.commands.executeCommand("workbench.action.openSettings", setting); - await vscode.commands.executeCommand("workbench.action.openWorkspaceSettings"); - } + const openSettings = async (setting: string) => { + await vscode.commands.executeCommand( + "workbench.action.openSettings", + setting + ); + await vscode.commands.executeCommand( + "workbench.action.openWorkspaceSettings" + ); + }; - const openFile = async (fileUri: vscode.Uri) => { - await vscode.commands.executeCommand("vscode.open", fileUri); - await vscode.commands.executeCommand("workbench.files.action.showActiveFileInExplorer"); - } + const openFile = async (fileUri: vscode.Uri) => { + await vscode.commands.executeCommand("vscode.open", fileUri); + await vscode.commands.executeCommand( + "workbench.files.action.showActiveFileInExplorer" + ); + }; - context.subscriptions.push(vscode.commands.registerCommand('makefile.outline.openMakefilePathSetting', async () => { + context.subscriptions.push( + vscode.commands.registerCommand( + "makefile.outline.openMakefilePathSetting", + async () => { await openSettings("makefile.makefilePath"); - })); + } + ) + ); - context.subscriptions.push(vscode.commands.registerCommand('makefile.outline.openMakefileFile', async () => { + context.subscriptions.push( + vscode.commands.registerCommand( + "makefile.outline.openMakefileFile", + async () => { const makefile = configuration.getConfigurationMakefile(); if (makefile) { - if (util.checkFileExistsSync(makefile)) { - await openFile(vscode.Uri.file(makefile)); - } - else { - extension.updateMakefileFilePresent(false); - vscode.window.showErrorMessage(localize("makefile.outline.makefileFileNotFound", "The makefile file could not be opened.")); - } + if (util.checkFileExistsSync(makefile)) { + await openFile(vscode.Uri.file(makefile)); + } else { + extension.updateMakefileFilePresent(false); + vscode.window.showErrorMessage( + localize( + "makefile.outline.makefileFileNotFound", + "The makefile file could not be opened." + ) + ); + } } - })); + } + ) + ); - context.subscriptions.push(vscode.commands.registerCommand('makefile.outline.openMakePathSetting', async () => { + context.subscriptions.push( + vscode.commands.registerCommand( + "makefile.outline.openMakePathSetting", + async () => { await openSettings("makefile.makePath"); - })); + } + ) + ); - context.subscriptions.push(vscode.commands.registerCommand('makefile.outline.openBuildLogSetting', async () => { + context.subscriptions.push( + vscode.commands.registerCommand( + "makefile.outline.openBuildLogSetting", + async () => { await openSettings("makefile.buildLog"); - })); + } + ) + ); - context.subscriptions.push(vscode.commands.registerCommand('makefile.outline.openBuildLogFile', async () => { + context.subscriptions.push( + vscode.commands.registerCommand( + "makefile.outline.openBuildLogFile", + async () => { const buildLog = configuration.getBuildLog(); if (buildLog) { - if (util.checkFileExistsSync(buildLog)) { - await openFile(vscode.Uri.file(buildLog)); - } else { - extension.updateBuildLogPresent(false); - vscode.window.showErrorMessage(localize("makefile.outline.buildLogFileNotFound", "The build log file could not be opened.")); - } + if (util.checkFileExistsSync(buildLog)) { + await openFile(vscode.Uri.file(buildLog)); + } else { + extension.updateBuildLogPresent(false); + vscode.window.showErrorMessage( + localize( + "makefile.outline.buildLogFileNotFound", + "The build log file could not be opened." + ) + ); + } } - })); + } + ) + ); - // === Commands only for testing === - // commands that are not exposed via package.json and are used only for testing. - // TODO: In the future, we should refactor such that our tests can use already exposed commands, and/or refactor so - // that some of our tests that are more unit-like tests can be done with direct dependencies on the code. - if (process.env["MAKEFILE_TOOLS_TESTING"] === "1") { - context.subscriptions.push(vscode.commands.registerCommand('makefile.setBuildConfigurationByName', async (name: string) => { - await configuration.setConfigurationByName(name); - })); + // === Commands only for testing === + // commands that are not exposed via package.json and are used only for testing. + // TODO: In the future, we should refactor such that our tests can use already exposed commands, and/or refactor so + // that some of our tests that are more unit-like tests can be done with direct dependencies on the code. + if (process.env["MAKEFILE_TOOLS_TESTING"] === "1") { + context.subscriptions.push( + vscode.commands.registerCommand( + "makefile.setBuildConfigurationByName", + async (name: string) => { + await configuration.setConfigurationByName(name); + } + ) + ); - context.subscriptions.push(vscode.commands.registerCommand('makefile.setPreconfigureScriptByPath', async (path: string) => { - await configuration.setPreConfigureScript(path); - })); + context.subscriptions.push( + vscode.commands.registerCommand( + "makefile.setPreconfigureScriptByPath", + async (path: string) => { + await configuration.setPreConfigureScript(path); + } + ) + ); - context.subscriptions.push(vscode.commands.registerCommand('makefile.setTargetByName', async (name: string) => { - await configuration.setTargetByName(name); - })) + context.subscriptions.push( + vscode.commands.registerCommand( + "makefile.setTargetByName", + async (name: string) => { + await configuration.setTargetByName(name); + } + ) + ); - context.subscriptions.push(vscode.commands.registerCommand('makefile.setLaunchConfigurationByName', async (name: string) => { - await configuration.setLaunchConfigurationByName(name); - })); + context.subscriptions.push( + vscode.commands.registerCommand( + "makefile.setLaunchConfigurationByName", + async (name: string) => { + await configuration.setLaunchConfigurationByName(name); + } + ) + ); - context.subscriptions.push(vscode.commands.registerCommand('makefile.validateLaunchConfiguration', async () => { - return await launch.getLauncher().validateLaunchConfiguration(make.Operations.debug); - })); + context.subscriptions.push( + vscode.commands.registerCommand( + "makefile.validateLaunchConfiguration", + async () => { + return await launch + .getLauncher() + .validateLaunchConfiguration(make.Operations.debug); + } + ) + ); - context.subscriptions.push(vscode.commands.registerCommand('makefile.getCurrentLaunchConfiguration', async () => { - return configuration.getCurrentLaunchConfiguration(); - })) + context.subscriptions.push( + vscode.commands.registerCommand( + "makefile.getCurrentLaunchConfiguration", + async () => { + return configuration.getCurrentLaunchConfiguration(); + } + ) + ); - context.subscriptions.push(vscode.commands.registerCommand('makefile.prepareDebugAndRunCurrentTarget', async (launchConfiguration: configuration.LaunchConfiguration) => { - launch.getLauncher().prepareDebugCurrentTarget(launchConfiguration); - launch.getLauncher().prepareRunCurrentTarget(); - })); + context.subscriptions.push( + vscode.commands.registerCommand( + "makefile.prepareDebugAndRunCurrentTarget", + async (launchConfiguration: configuration.LaunchConfiguration) => { + launch.getLauncher().prepareDebugCurrentTarget(launchConfiguration); + launch.getLauncher().prepareRunCurrentTarget(); + } + ) + ); - context.subscriptions.push(vscode.commands.registerCommand('makefile.prepareBuildTarget', async (target: string) => { - make.prepareBuildTarget(target); - })); + context.subscriptions.push( + vscode.commands.registerCommand( + "makefile.prepareBuildTarget", + async (target: string) => { + make.prepareBuildTarget(target); + } + ) + ); - context.subscriptions.push(vscode.commands.registerCommand('makefile.testResetState', async () => { - await configuration.setCurrentLaunchConfiguration(undefined); - await configuration.setCurrentMakefileConfiguration("Default"); - configuration.setCurrentTarget(undefined); - configuration.initFromState(); - await configuration.initFromSettings(); - })); + context.subscriptions.push( + vscode.commands.registerCommand("makefile.testResetState", async () => { + await configuration.setCurrentLaunchConfiguration(undefined); + await configuration.setCurrentMakefileConfiguration("Default"); + configuration.setCurrentTarget(undefined); + configuration.initFromState(); + await configuration.initFromSettings(); + }) + ); - context.subscriptions.push(vscode.commands.registerCommand('makefile.getExpandedSettingValue', async (key: string, value: any) => { - await util.getExpandedSettingVal(key, value); - })); + context.subscriptions.push( + vscode.commands.registerCommand( + "makefile.getExpandedSettingValue", + async (key: string, value: any) => { + await util.getExpandedSettingVal(key, value); + } + ) + ); - context.subscriptions.push(vscode.commands.registerCommand('makefile.expandVariablesInSetting', async (key: string, value: string) => { - return util.expandVariablesInSetting(key, value); - })); - } - // === Commands only for testing === + context.subscriptions.push( + vscode.commands.registerCommand( + "makefile.expandVariablesInSetting", + async (key: string, value: string) => { + return util.expandVariablesInSetting(key, value); + } + ) + ); + } + // === Commands only for testing === - const parseCompilerArgsScript: string = util.parseCompilerArgsScriptFile(); + const parseCompilerArgsScript: string = util.parseCompilerArgsScriptFile(); - // The extension VSIX stripped the executable bit, so we need to set it. - // 0x755 means rwxr-xr-x (read and execute for everyone, write for owner). - await fs.chmod(parseCompilerArgsScript, 0o755); + // The extension VSIX stripped the executable bit, so we need to set it. + // 0x755 means rwxr-xr-x (read and execute for everyone, write for owner). + await fs.chmod(parseCompilerArgsScript, 0o755); - if (configuration.getConfigureOnOpen() && extension.getFullFeatureSet()) { - // Always clean configure on open - await make.cleanConfigure(make.TriggeredBy.cleanConfigureOnOpen); - } + if (configuration.getConfigureOnOpen() && extension.getFullFeatureSet()) { + // Always clean configure on open + await make.cleanConfigure(make.TriggeredBy.cleanConfigureOnOpen); + } - // Analyze settings for type validation and telemetry - let workspaceConfiguration: vscode.WorkspaceConfiguration = vscode.workspace.getConfiguration("makefile"); - let telemetryProperties: telemetry.Properties | null = {}; - try { - telemetryProperties = await telemetry.analyzeSettings(workspaceConfiguration, "makefile", - util.thisExtensionPackage().contributes.configuration.properties, - true, telemetryProperties); - } catch (e) { - telemetry.telemetryLogger(e.message); - } + // Analyze settings for type validation and telemetry + let workspaceConfiguration: vscode.WorkspaceConfiguration = + vscode.workspace.getConfiguration("makefile"); + let telemetryProperties: telemetry.Properties | null = {}; + try { + telemetryProperties = await telemetry.analyzeSettings( + workspaceConfiguration, + "makefile", + util.thisExtensionPackage().contributes.configuration.properties, + true, + telemetryProperties + ); + } catch (e) { + telemetry.telemetryLogger(e.message); + } - if (telemetryProperties && util.hasProperties(telemetryProperties)) { - telemetry.logEvent("settings", telemetryProperties); - } + if (telemetryProperties && util.hasProperties(telemetryProperties)) { + telemetry.logEvent("settings", telemetryProperties); + } } export async function deactivate(): Promise { - vscode.window.showInformationMessage(localize("extension.deactivated", "The extension {0} is de-activated.", "'vscode-makefile-tools'")); + vscode.window.showInformationMessage( + localize( + "extension.deactivated", + "The extension {0} is de-activated.", + "'vscode-makefile-tools'" + ) + ); - await telemetry.deactivate(); + await telemetry.deactivate(); - const items : any = [ - extension, - launcher, - statusBar - ]; + const items: any = [extension, launcher, statusBar]; - for (const item of items) { - if (item) { - item.dispose(); - } + for (const item of items) { + if (item) { + item.dispose(); } + } } diff --git a/src/launch.ts b/src/launch.ts index b0c0b7c..82772d2 100644 --- a/src/launch.ts +++ b/src/launch.ts @@ -3,368 +3,485 @@ // Launch support: debug and run in terminal -import * as configuration from './configuration'; -import * as extension from './extension'; -import * as logger from './logger'; -import * as make from './make'; -import * as path from 'path'; -import * as telemetry from './telemetry'; -import * as util from './util'; -import * as vscode from 'vscode'; +import * as configuration from "./configuration"; +import * as extension from "./extension"; +import * as logger from "./logger"; +import * as make from "./make"; +import * as path from "path"; +import * as telemetry from "./telemetry"; +import * as util from "./util"; +import * as vscode from "vscode"; -import * as nls from 'vscode-nls'; -nls.config({ messageFormat: nls.MessageFormat.bundle, bundleFormat: nls.BundleFormat.standalone })(); +import * as nls from "vscode-nls"; +nls.config({ + messageFormat: nls.MessageFormat.bundle, + bundleFormat: nls.BundleFormat.standalone, +})(); const localize: nls.LocalizeFunc = nls.loadMessageBundle(); export enum LaunchStatuses { - success = "success", - blocked = "blocked by (pre)configure or build", - noLaunchConfigurationSet = "no launch configuration set by the user", - launchTargetsListEmpty = "launch targets list empty", - buildFailed = "build failed", + success = "success", + blocked = "blocked by (pre)configure or build", + noLaunchConfigurationSet = "no launch configuration set by the user", + launchTargetsListEmpty = "launch targets list empty", + buildFailed = "build failed", } let launcher: Launcher; export class Launcher implements vscode.Disposable { - // Command property accessible from launch.json: - // the full path of the target binary currently set for launch - public getLaunchTargetPath(): string { - let launchConfiguration: configuration.LaunchConfiguration | undefined = configuration.getCurrentLaunchConfiguration(); - if (launchConfiguration) { - return launchConfiguration.binaryPath; - } else { - return ""; - } + // Command property accessible from launch.json: + // the full path of the target binary currently set for launch + public getLaunchTargetPath(): string { + let launchConfiguration: configuration.LaunchConfiguration | undefined = + configuration.getCurrentLaunchConfiguration(); + if (launchConfiguration) { + return launchConfiguration.binaryPath; + } else { + return ""; + } + } + + // Command property accessible from launch.json: + // calls getLaunchTargetPath after triggering a build of the current target, + // if makefile.buildBeforeLaunch allows it. + public async launchTargetPath(): Promise { + if (configuration.getBuildBeforeLaunch()) { + await make.buildTarget( + make.TriggeredBy.launch, + configuration.getCurrentTarget() || "" + ); } - // Command property accessible from launch.json: - // calls getLaunchTargetPath after triggering a build of the current target, - // if makefile.buildBeforeLaunch allows it. - public async launchTargetPath(): Promise { - if (configuration.getBuildBeforeLaunch()) { - await make.buildTarget(make.TriggeredBy.launch, configuration.getCurrentTarget() || ""); - } + return this.getLaunchTargetPath(); + } - return this.getLaunchTargetPath(); + // Command property accessible from launch.json: + // the full path from where the target binary is to be launched + public getLaunchTargetDirectory(): string { + let launchConfiguration: configuration.LaunchConfiguration | undefined = + configuration.getCurrentLaunchConfiguration(); + if (launchConfiguration) { + return launchConfiguration.cwd; + } else { + return util.getWorkspaceRoot(); + } + } + + // Command property accessible from launch.json: + // the file name of the current target binary, without path or extension. + public getLaunchTargetFileName(): string { + let launchConfiguration: configuration.LaunchConfiguration | undefined = + configuration.getCurrentLaunchConfiguration(); + if (launchConfiguration) { + return path.parse(launchConfiguration.binaryPath).name; + } else { + return ""; + } + } + + // Command property accessible from launch.json: + // calls getLaunchTargetFileName after triggering a build of the current target, + // if makefile.buildBeforeLaunch allows it. + public async launchTargetFileName(): Promise { + if (configuration.getBuildBeforeLaunch()) { + await make.buildTarget( + make.TriggeredBy.launch, + configuration.getCurrentTarget() || "" + ); } - // Command property accessible from launch.json: - // the full path from where the target binary is to be launched - public getLaunchTargetDirectory(): string { - let launchConfiguration: configuration.LaunchConfiguration | undefined = configuration.getCurrentLaunchConfiguration(); - if (launchConfiguration) { - return launchConfiguration.cwd; - } else { - return util.getWorkspaceRoot(); - } + return this.getLaunchTargetFileName(); + } + + // Command property accessible from launch.json: + // the arguments sent to the target binary, returned as array of string + // This is used by the debug/terminal VS Code APIs. + public getLaunchTargetArgs(): string[] { + let launchConfiguration: configuration.LaunchConfiguration | undefined = + configuration.getCurrentLaunchConfiguration(); + if (launchConfiguration) { + return launchConfiguration.binaryArgs; + } else { + return []; + } + } + + // Command property accessible from launch.json: + // the arguments sent to the target binary, returned as one simple string + // This is an alternative to define the arguments in launch.json, + // since the string array syntax is not working. + // This is not a perfect solution, it all depends on how the main entry point + // is parsing its given arguments. + // Example: for [CWD>tool arg1 arg2 arg3], the tool will receive + // 2 arguments: tool and "arg1 arg2 arg3" + // As opposed to the above case when the tool will receive + // 4 arguments: tool, arg1, arg2, arg3 + // TODO: investigate how we can define string array arguments + // for the target binary in launch.json + public getLaunchTargetArgsConcat(): string { + return this.getLaunchTargetArgs().join(" "); + } + + // Invoke a VS Code debugging session passing it all the information + // from the current launch configuration. + // Debugger (imperfect) guess logic: + // - VS for msvc toolset, lldb for clang toolset, gdb for anything else. + // - debugger path is assumed to be the same as the compiler path. + // Exceptions for miMode: + // - if the above logic results in a debugger that is missing, try the other one. + // This is needed either because the system might not be equipped + // with the preffered debugger that corresponds to the toolset in use, + // but also because there might be a compiler alias that is not properly identified + // (example: "cc" alias that points to clang but is not identified as clang, + // therefore requesting a gdb debugger which may be missing + // because there is no gcc toolset installed). + // TODO: implement proper detection of aliases and their commands. + // Exceptions for miDebuggerPath: + // - for MacOS, point to the lldb-mi debugger that is installed by CppTools + // - if CppTools extension is not installed, intentionally do not provide a miDebuggerPath On MAC, + // because the debugger knows how to find automatically the right lldb-mi when miMode is lldb and miDebuggerPath is undefined + // (this is true for systems older than Catalina). + // Additionally, cppvsdbg ignores miMode and miDebuggerPath. + public prepareDebugCurrentTarget( + currentLaunchConfiguration: configuration.LaunchConfiguration + ): vscode.DebugConfiguration { + let args: string[] = this.getLaunchTargetArgs(); + + let compilerPath: string | undefined = + extension.extension.getCompilerFullPath(); + let parsedObjPath: path.ParsedPath | undefined = compilerPath + ? path.parse(compilerPath) + : undefined; + let isClangCompiler: boolean | undefined = + parsedObjPath?.name.startsWith("clang"); + let isMsvcCompiler: boolean | undefined = + !isClangCompiler && parsedObjPath?.name.startsWith("cl"); + let dbg: string = isMsvcCompiler ? "cppvsdbg" : "cppdbg"; + + // Initial debugger guess + let guessMiDebuggerPath: string | undefined = + !isMsvcCompiler && parsedObjPath ? parsedObjPath.dir : undefined; + let guessMiMode: string | undefined; + if (parsedObjPath?.name.startsWith("clang")) { + guessMiMode = "lldb"; + } else if (!parsedObjPath?.name.startsWith("cl")) { + guessMiMode = "gdb"; } - // Command property accessible from launch.json: - // the file name of the current target binary, without path or extension. - public getLaunchTargetFileName(): string { - let launchConfiguration: configuration.LaunchConfiguration | undefined = configuration.getCurrentLaunchConfiguration(); - if (launchConfiguration) { - return path.parse(launchConfiguration.binaryPath).name; - } else { - return ""; - } + // If the first chosen debugger is not installed, try the other one. + if (guessMiDebuggerPath && guessMiMode) { + // if the guessMiDebuggerPath is already a file, then go with that. Otherwise, append the guessMiMode. + let debuggerPath: string = util.checkFileExistsSync(guessMiDebuggerPath) + ? guessMiDebuggerPath + : path.join(guessMiDebuggerPath, guessMiMode); + if (process.platform === "win32") { + // On mingw a file is not found if the extension is not part of the path + debuggerPath = debuggerPath + ".exe"; + } + + if (!util.checkFileExistsSync(debuggerPath)) { + guessMiMode = guessMiMode === "gdb" ? "lldb" : "gdb"; + } } - // Command property accessible from launch.json: - // calls getLaunchTargetFileName after triggering a build of the current target, - // if makefile.buildBeforeLaunch allows it. - public async launchTargetFileName(): Promise { - if (configuration.getBuildBeforeLaunch()) { - await make.buildTarget(make.TriggeredBy.launch, configuration.getCurrentTarget() || ""); - } + // Properties defined by makefile.launchConfigurations override makefile.defaultLaunchConfiguration + // and they both override the guessed values. + let defaultLaunchConfiguration: + | configuration.DefaultLaunchConfiguration + | undefined = configuration.getDefaultLaunchConfiguration(); + let miMode: string | undefined = + currentLaunchConfiguration.MIMode || + defaultLaunchConfiguration?.MIMode || + guessMiMode; + let miDebuggerPath: string | undefined = + currentLaunchConfiguration.miDebuggerPath || + defaultLaunchConfiguration?.miDebuggerPath || + guessMiDebuggerPath; - return this.getLaunchTargetFileName(); + // Exception for MAC-lldb, point to the lldb-mi installed by CppTools or set debugger path to undefined + // (more details in the comment at the beginning of this function). + if (miMode === "lldb" && process.platform === "darwin") { + const cpptoolsExtension: vscode.Extension | undefined = + vscode.extensions.getExtension("ms-vscode.cpptools"); + miDebuggerPath = cpptoolsExtension + ? path.join( + cpptoolsExtension.extensionPath, + "debugAdapters", + "lldb-mi", + "bin", + "lldb-mi" + ) + : undefined; + } else if (miDebuggerPath && miMode) { + // if the miDebuggerPath is already a file, rather than a directory, go with it. + // Otherwise, append the MiMode. + miDebuggerPath = util.checkFileExistsSync(miDebuggerPath) + ? miDebuggerPath + : path.join(miDebuggerPath, miMode); + if (process.platform === "win32") { + miDebuggerPath = miDebuggerPath + ".exe"; + } } - // Command property accessible from launch.json: - // the arguments sent to the target binary, returned as array of string - // This is used by the debug/terminal VS Code APIs. - public getLaunchTargetArgs(): string[] { - let launchConfiguration: configuration.LaunchConfiguration | undefined = configuration.getCurrentLaunchConfiguration(); - if (launchConfiguration) { - return launchConfiguration.binaryArgs; - } else { - return []; - } + let debugConfig: vscode.DebugConfiguration = { + type: dbg, + name: `Debug My Program`, + request: "launch", + cwd: this.getLaunchTargetDirectory(), + args, + env: util.mergeEnvironment(process.env as util.EnvironmentVariables), + program: this.getLaunchTargetPath(), + MIMode: miMode, + miDebuggerPath: miDebuggerPath, + console: "internalConsole", + internalConsoleOptions: "openOnSessionStart", + stopAtEntry: + currentLaunchConfiguration.stopAtEntry || + defaultLaunchConfiguration?.stopAtEntry, + symbolSearchPath: + currentLaunchConfiguration.symbolSearchPath || + defaultLaunchConfiguration?.symbolSearchPath, + }; + + logger.message( + "Created the following debug config:\n type = " + + debugConfig.type + + "\n cwd = " + + debugConfig.cwd + + " (= " + + this.getLaunchTargetDirectory() + + ")" + + "\n args = " + + args.join(" ") + + "\n program = " + + debugConfig.program + + " (= " + + this.getLaunchTargetPath() + + ")" + + "\n MIMode = " + + debugConfig.MIMode + + "\n miDebuggerPath = " + + debugConfig.miDebuggerPath + + "\n stopAtEntry = " + + debugConfig.stopAtEntry + + "\n symbolSearchPath = " + + debugConfig.symbolSearchPath + ); + + return debugConfig; + } + + async validateLaunchConfiguration(op: make.Operations): Promise { + // Cannot debug the project if it is currently building or (pre-)configuring. + if (make.blockedByOp(op)) { + return LaunchStatuses.blocked; } - // Command property accessible from launch.json: - // the arguments sent to the target binary, returned as one simple string - // This is an alternative to define the arguments in launch.json, - // since the string array syntax is not working. - // This is not a perfect solution, it all depends on how the main entry point - // is parsing its given arguments. - // Example: for [CWD>tool arg1 arg2 arg3], the tool will receive - // 2 arguments: tool and "arg1 arg2 arg3" - // As opposed to the above case when the tool will receive - // 4 arguments: tool, arg1, arg2, arg3 - // TODO: investigate how we can define string array arguments - // for the target binary in launch.json - public getLaunchTargetArgsConcat(): string { - return this.getLaunchTargetArgs().join(" "); - } - - // Invoke a VS Code debugging session passing it all the information - // from the current launch configuration. - // Debugger (imperfect) guess logic: - // - VS for msvc toolset, lldb for clang toolset, gdb for anything else. - // - debugger path is assumed to be the same as the compiler path. - // Exceptions for miMode: - // - if the above logic results in a debugger that is missing, try the other one. - // This is needed either because the system might not be equipped - // with the preffered debugger that corresponds to the toolset in use, - // but also because there might be a compiler alias that is not properly identified - // (example: "cc" alias that points to clang but is not identified as clang, - // therefore requesting a gdb debugger which may be missing - // because there is no gcc toolset installed). - // TODO: implement proper detection of aliases and their commands. - // Exceptions for miDebuggerPath: - // - for MacOS, point to the lldb-mi debugger that is installed by CppTools - // - if CppTools extension is not installed, intentionally do not provide a miDebuggerPath On MAC, - // because the debugger knows how to find automatically the right lldb-mi when miMode is lldb and miDebuggerPath is undefined - // (this is true for systems older than Catalina). - // Additionally, cppvsdbg ignores miMode and miDebuggerPath. - public prepareDebugCurrentTarget(currentLaunchConfiguration: configuration.LaunchConfiguration): vscode.DebugConfiguration { - let args: string[] = this.getLaunchTargetArgs(); - - let compilerPath : string | undefined = extension.extension.getCompilerFullPath(); - let parsedObjPath : path.ParsedPath | undefined = compilerPath ? path.parse(compilerPath) : undefined; - let isClangCompiler : boolean | undefined = parsedObjPath?.name.startsWith("clang"); - let isMsvcCompiler : boolean | undefined = !isClangCompiler && parsedObjPath?.name.startsWith("cl"); - let dbg: string = (isMsvcCompiler) ? "cppvsdbg" : "cppdbg"; - - // Initial debugger guess - let guessMiDebuggerPath : string | undefined = (!isMsvcCompiler && parsedObjPath) ? parsedObjPath.dir : undefined; - let guessMiMode: string | undefined; - if (parsedObjPath?.name.startsWith("clang")) { - guessMiMode = "lldb"; - } else if (!parsedObjPath?.name.startsWith("cl")) { - guessMiMode = "gdb"; - } - - // If the first chosen debugger is not installed, try the other one. - if (guessMiDebuggerPath && guessMiMode) { - // if the guessMiDebuggerPath is already a file, then go with that. Otherwise, append the guessMiMode. - let debuggerPath: string = util.checkFileExistsSync(guessMiDebuggerPath) ? guessMiDebuggerPath : path.join(guessMiDebuggerPath, guessMiMode); - if (process.platform === "win32") { - // On mingw a file is not found if the extension is not part of the path - debuggerPath = debuggerPath + ".exe"; + if (configuration.getBuildBeforeLaunch()) { + let currentBuildTarget: string = configuration.getCurrentTarget() || ""; + logger.message( + `Building current target before launch: "${currentBuildTarget}"` + ); + let buildSuccess: boolean = + (await make.buildTarget( + make.TriggeredBy.buildTarget, + currentBuildTarget, + false + )) === make.ConfigureBuildReturnCodeTypes.success; + if (!buildSuccess) { + logger.message(`Building target "${currentBuildTarget}" failed.`); + let noButton: string = localize("no", "No"); + let yesButton: string = localize("yes", "Yes"); + const message: string = localize( + "build.failed.continue.anyway", + "Build failed. Do you want to continue anyway?" + ); + const chosen: vscode.MessageItem | undefined = + await vscode.window.showErrorMessage( + message, + { + title: yesButton, + isCloseAffordance: false, + }, + { + title: noButton, + isCloseAffordance: true, } + ); - if (!util.checkFileExistsSync(debuggerPath)) { - guessMiMode = (guessMiMode === "gdb") ? "lldb" : "gdb"; - } + if (chosen === undefined || chosen.title === noButton) { + return LaunchStatuses.buildFailed; } - - // Properties defined by makefile.launchConfigurations override makefile.defaultLaunchConfiguration - // and they both override the guessed values. - let defaultLaunchConfiguration: configuration.DefaultLaunchConfiguration | undefined = configuration.getDefaultLaunchConfiguration(); - let miMode: string | undefined = currentLaunchConfiguration.MIMode || defaultLaunchConfiguration?.MIMode || guessMiMode; - let miDebuggerPath: string | undefined = currentLaunchConfiguration.miDebuggerPath || defaultLaunchConfiguration?.miDebuggerPath || guessMiDebuggerPath; - - // Exception for MAC-lldb, point to the lldb-mi installed by CppTools or set debugger path to undefined - // (more details in the comment at the beginning of this function). - if (miMode === "lldb" && process.platform === "darwin") { - const cpptoolsExtension: vscode.Extension | undefined = vscode.extensions.getExtension('ms-vscode.cpptools'); - miDebuggerPath = cpptoolsExtension ? path.join(cpptoolsExtension.extensionPath, "debugAdapters", "lldb-mi", "bin", "lldb-mi") : undefined; - } else if (miDebuggerPath && miMode) { - // if the miDebuggerPath is already a file, rather than a directory, go with it. - // Otherwise, append the MiMode. - miDebuggerPath = util.checkFileExistsSync(miDebuggerPath) ? miDebuggerPath : path.join(miDebuggerPath, miMode); - if (process.platform === "win32") { - miDebuggerPath = miDebuggerPath + ".exe"; - } - } - - let debugConfig: vscode.DebugConfiguration = { - type: dbg, - name: `Debug My Program`, - request: 'launch', - cwd: this.getLaunchTargetDirectory(), - args, - env: util.mergeEnvironment(process.env as util.EnvironmentVariables), - program: this.getLaunchTargetPath(), - MIMode: miMode, - miDebuggerPath: miDebuggerPath, - console: "internalConsole", - internalConsoleOptions: "openOnSessionStart", - stopAtEntry: currentLaunchConfiguration.stopAtEntry || defaultLaunchConfiguration?.stopAtEntry, - symbolSearchPath: currentLaunchConfiguration.symbolSearchPath || defaultLaunchConfiguration?.symbolSearchPath - }; - - logger.message("Created the following debug config:\n type = " + debugConfig.type + - "\n cwd = " + debugConfig.cwd + " (= " + this.getLaunchTargetDirectory() + ")" + - "\n args = " + args.join(" ") + - "\n program = " + debugConfig.program + " (= " + this.getLaunchTargetPath() + ")" + - "\n MIMode = " + debugConfig.MIMode + - "\n miDebuggerPath = " + debugConfig.miDebuggerPath + - "\n stopAtEntry = " + debugConfig.stopAtEntry + - "\n symbolSearchPath = " + debugConfig.symbolSearchPath); - - return debugConfig; + } } - async validateLaunchConfiguration(op: make.Operations): Promise { - // Cannot debug the project if it is currently building or (pre-)configuring. - if (make.blockedByOp(op)) { - return LaunchStatuses.blocked; - } + let currentLaunchConfiguration: + | configuration.LaunchConfiguration + | undefined = configuration.getCurrentLaunchConfiguration(); + if (!currentLaunchConfiguration) { + // If no launch configuration is set, give the user a chance to select one now from the quick pick + // (unless we know it's going to be empty). + if (configuration.getLaunchTargets().length === 0) { + vscode.window.showErrorMessage( + localize( + "cannot.op.no.launch.config.targets", + "Cannot {0} because there is no launch configuration set and the list of launch targets is empty. Double check the makefile configuration and the build target.", + `'${op}'` + ) + ); + return LaunchStatuses.launchTargetsListEmpty; + } else { + vscode.window.showErrorMessage( + localize( + "cannot.op.choose.launch.config", + "Cannot {0} because there is no launch configuration set. Choose one from the quick pick.", + `'${op}'` + ) + ); + await configuration.selectLaunchConfiguration(); - if (configuration.getBuildBeforeLaunch()) { - let currentBuildTarget: string = configuration.getCurrentTarget() || ""; - logger.message(`Building current target before launch: "${currentBuildTarget}"`); - let buildSuccess: boolean = (await make.buildTarget(make.TriggeredBy.buildTarget, currentBuildTarget, false)) === make.ConfigureBuildReturnCodeTypes.success; - if (!buildSuccess) { - logger.message(`Building target "${currentBuildTarget}" failed.`); - let noButton: string = localize("no", "No"); - let yesButton: string = localize("yes", "Yes"); - const message: string = localize("build.failed.continue.anyway", "Build failed. Do you want to continue anyway?"); - const chosen: vscode.MessageItem | undefined = await vscode.window.showErrorMessage(message, - { - title: yesButton, - isCloseAffordance: false, - }, - { - title: noButton, - isCloseAffordance: true - }); - - if (chosen === undefined || chosen.title === noButton) { - return LaunchStatuses.buildFailed; - } - } - } - - let currentLaunchConfiguration: configuration.LaunchConfiguration | undefined = configuration.getCurrentLaunchConfiguration(); + // Read again the current launch configuration. If a current launch configuration is stil not set + // (the user cancelled the quick pick or the parser found zero launch targets) message and fail. + currentLaunchConfiguration = + configuration.getCurrentLaunchConfiguration(); if (!currentLaunchConfiguration) { - // If no launch configuration is set, give the user a chance to select one now from the quick pick - // (unless we know it's going to be empty). - if (configuration.getLaunchTargets().length === 0) { - vscode.window.showErrorMessage(localize("cannot.op.no.launch.config.targets", - "Cannot {0} because there is no launch configuration set and the list of launch targets is empty. Double check the makefile configuration and the build target.", `'${op}'`)); - return LaunchStatuses.launchTargetsListEmpty; - } else { - vscode.window.showErrorMessage(localize("cannot.op.choose.launch.config", "Cannot {0} because there is no launch configuration set. Choose one from the quick pick.", `'${op}'`)); - await configuration.selectLaunchConfiguration(); - - // Read again the current launch configuration. If a current launch configuration is stil not set - // (the user cancelled the quick pick or the parser found zero launch targets) message and fail. - currentLaunchConfiguration = configuration.getCurrentLaunchConfiguration(); - if (!currentLaunchConfiguration) { - vscode.window.showErrorMessage(localize("cannot.op.without.launch.config", "Cannot {0} until you select an active launch configuration.", `'${op}'`)); - return LaunchStatuses.noLaunchConfigurationSet; - } - } + vscode.window.showErrorMessage( + localize( + "cannot.op.without.launch.config", + "Cannot {0} until you select an active launch configuration.", + `'${op}'` + ) + ); + return LaunchStatuses.noLaunchConfigurationSet; } - - return LaunchStatuses.success; + } } - public async debugCurrentTarget(): Promise { - let status: string = await this.validateLaunchConfiguration(make.Operations.debug); - let currentLaunchConfiguration: configuration.LaunchConfiguration | undefined; - if (status === LaunchStatuses.success) { - currentLaunchConfiguration = configuration.getCurrentLaunchConfiguration(); - } + return LaunchStatuses.success; + } - if (currentLaunchConfiguration) { - let debugConfig: vscode.DebugConfiguration = this.prepareDebugCurrentTarget(currentLaunchConfiguration); - let startFolder: vscode.WorkspaceFolder; - if (vscode.workspace.workspaceFolders) { - startFolder = vscode.workspace.workspaceFolders[0]; - await vscode.debug.startDebugging(startFolder, debugConfig); - } else { - await vscode.debug.startDebugging(undefined, debugConfig); - } - - if (!vscode.debug.activeDebugSession) { - status = "failed"; - } - } - - let telemetryProperties: telemetry.Properties = { - status: status - }; - telemetry.logEvent("debug", telemetryProperties); - - return vscode.debug.activeDebugSession; + public async debugCurrentTarget(): Promise { + let status: string = await this.validateLaunchConfiguration( + make.Operations.debug + ); + let currentLaunchConfiguration: + | configuration.LaunchConfiguration + | undefined; + if (status === LaunchStatuses.success) { + currentLaunchConfiguration = + configuration.getCurrentLaunchConfiguration(); } - private launchTerminal: vscode.Terminal | undefined; + if (currentLaunchConfiguration) { + let debugConfig: vscode.DebugConfiguration = + this.prepareDebugCurrentTarget(currentLaunchConfiguration); + let startFolder: vscode.WorkspaceFolder; + if (vscode.workspace.workspaceFolders) { + startFolder = vscode.workspace.workspaceFolders[0]; + await vscode.debug.startDebugging(startFolder, debugConfig); + } else { + await vscode.debug.startDebugging(undefined, debugConfig); + } - // Watch for the user closing our terminal - private readonly onTerminalClose = vscode.window.onDidCloseTerminal(term => { - if (term === this.launchTerminal) { - this.launchTerminal = undefined; - } - }); - - // Invoke a VS Code running terminal passing it all the information - // from the current launch configuration - public prepareRunCurrentTarget(): string { - // Add a pair of quotes just in case there is a space in the binary path - let terminalCommand: string = '"' + this.getLaunchTargetPath() + '" '; - terminalCommand += this.getLaunchTargetArgs().join(" "); - - // Log the message for high verbosity only because the output channel will become visible over the terminal, - // even if the terminal show() is called after the logger show(). - logger.message("Running command '" + terminalCommand + "' in the terminal from location '" + this.getLaunchTargetDirectory() + "'", "Debug"); - return terminalCommand; + if (!vscode.debug.activeDebugSession) { + status = "failed"; + } } - public async runCurrentTarget(): Promise { - const terminalOptions: vscode.TerminalOptions = { - name: 'Make/Launch', - }; + let telemetryProperties: telemetry.Properties = { + status: status, + }; + telemetry.logEvent("debug", telemetryProperties); - // Use cmd.exe on Windows - if (process.platform === 'win32') { - terminalOptions.shellPath = 'C:\\Windows\\System32\\cmd.exe'; - } + return vscode.debug.activeDebugSession; + } - terminalOptions.cwd = this.getLaunchTargetDirectory(); - terminalOptions.env = util.mergeEnvironment(process.env as util.EnvironmentVariables); + private launchTerminal: vscode.Terminal | undefined; - if (!this.launchTerminal) { - this.launchTerminal = vscode.window.createTerminal(terminalOptions); - } + // Watch for the user closing our terminal + private readonly onTerminalClose = vscode.window.onDidCloseTerminal( + (term) => { + if (term === this.launchTerminal) { + this.launchTerminal = undefined; + } + } + ); - let status: string = await this.validateLaunchConfiguration(make.Operations.run); - let currentLaunchConfiguration: configuration.LaunchConfiguration | undefined; - if (status === LaunchStatuses.success) { - currentLaunchConfiguration = configuration.getCurrentLaunchConfiguration(); - let terminalCommand: string = this.prepareRunCurrentTarget(); - this.launchTerminal.sendText(terminalCommand); + // Invoke a VS Code running terminal passing it all the information + // from the current launch configuration + public prepareRunCurrentTarget(): string { + // Add a pair of quotes just in case there is a space in the binary path + let terminalCommand: string = '"' + this.getLaunchTargetPath() + '" '; + terminalCommand += this.getLaunchTargetArgs().join(" "); - let telemetryProperties: telemetry.Properties = { - status: status - }; - telemetry.logEvent("run", telemetryProperties); - this.launchTerminal.show(); - } + // Log the message for high verbosity only because the output channel will become visible over the terminal, + // even if the terminal show() is called after the logger show(). + logger.message( + "Running command '" + + terminalCommand + + "' in the terminal from location '" + + this.getLaunchTargetDirectory() + + "'", + "Debug" + ); + return terminalCommand; + } - return this.launchTerminal; + public async runCurrentTarget(): Promise { + const terminalOptions: vscode.TerminalOptions = { + name: "Make/Launch", + }; + + // Use cmd.exe on Windows + if (process.platform === "win32") { + terminalOptions.shellPath = "C:\\Windows\\System32\\cmd.exe"; } - public dispose(): void { - if (this.launchTerminal) { - this.launchTerminal.dispose(); - } + terminalOptions.cwd = this.getLaunchTargetDirectory(); + terminalOptions.env = util.mergeEnvironment( + process.env as util.EnvironmentVariables + ); - this.onTerminalClose.dispose(); + if (!this.launchTerminal) { + this.launchTerminal = vscode.window.createTerminal(terminalOptions); } + + let status: string = await this.validateLaunchConfiguration( + make.Operations.run + ); + let currentLaunchConfiguration: + | configuration.LaunchConfiguration + | undefined; + if (status === LaunchStatuses.success) { + currentLaunchConfiguration = + configuration.getCurrentLaunchConfiguration(); + let terminalCommand: string = this.prepareRunCurrentTarget(); + this.launchTerminal.sendText(terminalCommand); + + let telemetryProperties: telemetry.Properties = { + status: status, + }; + telemetry.logEvent("run", telemetryProperties); + this.launchTerminal.show(); + } + + return this.launchTerminal; + } + + public dispose(): void { + if (this.launchTerminal) { + this.launchTerminal.dispose(); + } + + this.onTerminalClose.dispose(); + } } export function getLauncher(): Launcher { - if (launcher === undefined) { - launcher = new Launcher(); - } + if (launcher === undefined) { + launcher = new Launcher(); + } - return launcher; + return launcher; } diff --git a/src/logger.ts b/src/logger.ts index b8ec688..111f473 100644 --- a/src/logger.ts +++ b/src/logger.ts @@ -3,78 +3,78 @@ // Logging support -import * as fs from 'fs'; -import * as configuration from './configuration'; -import * as vscode from 'vscode'; +import * as fs from "fs"; +import * as configuration from "./configuration"; +import * as vscode from "vscode"; let makeOutputChannel: vscode.OutputChannel | undefined; function getOutputChannel(): vscode.OutputChannel { - if (!makeOutputChannel) { - makeOutputChannel = vscode.window.createOutputChannel("Makefile tools"); - } + if (!makeOutputChannel) { + makeOutputChannel = vscode.window.createOutputChannel("Makefile tools"); + } - return makeOutputChannel; + return makeOutputChannel; } // TODO: process verbosities with enums instead of strings. // This is a temporary hack. function loggingLevelApplies(messageVerbosity: string | undefined): boolean { - let projectVerbosity: string | undefined = configuration.getLoggingLevel(); + let projectVerbosity: string | undefined = configuration.getLoggingLevel(); - if (messageVerbosity === "Debug") { - return projectVerbosity === "Debug"; - } else if (messageVerbosity === "Verbose") { - return projectVerbosity === "Verbose" || projectVerbosity === "Debug"; - } + if (messageVerbosity === "Debug") { + return projectVerbosity === "Debug"; + } else if (messageVerbosity === "Verbose") { + return projectVerbosity === "Verbose" || projectVerbosity === "Debug"; + } - return true; + return true; } export function showOutputChannel(): void { - if (makeOutputChannel) { - makeOutputChannel.show(true); - } + if (makeOutputChannel) { + makeOutputChannel.show(true); + } } export function clearOutputChannel(): void { - if (makeOutputChannel) { - makeOutputChannel.clear(); - } + if (makeOutputChannel) { + makeOutputChannel.clear(); + } } //TODO: implement more verbosity levels for the output log export function message(message: string, loggingLevel?: string): void { - // Print the message only if the intended logging level matches the settings - // or if no loggingLevel restriction is provided. - if (!loggingLevelApplies(loggingLevel)) { - return; - } + // Print the message only if the intended logging level matches the settings + // or if no loggingLevel restriction is provided. + if (!loggingLevelApplies(loggingLevel)) { + return; + } - let channel: vscode.OutputChannel = getOutputChannel(); - channel.appendLine(message); + let channel: vscode.OutputChannel = getOutputChannel(); + channel.appendLine(message); - let extensionLog : string | undefined = configuration.getExtensionLog(); - if (extensionLog) { - fs.appendFileSync(extensionLog, message); - fs.appendFileSync(extensionLog, "\n"); - } + let extensionLog: string | undefined = configuration.getExtensionLog(); + if (extensionLog) { + fs.appendFileSync(extensionLog, message); + fs.appendFileSync(extensionLog, "\n"); + } } // This is used for a few scenarios where the message already has end of line incorporated. // Example: stdout/stderr of a child process read before the stream is closed. export function messageNoCR(message: string, loggingLevel?: string): void { - // Print the message only if the intended logging level matches the settings - // or if no loggingLevel restriction is provided. - if (!loggingLevelApplies(loggingLevel)) { - return; - } + // Print the message only if the intended logging level matches the settings + // or if no loggingLevel restriction is provided. + if (!loggingLevelApplies(loggingLevel)) { + return; + } - let channel: vscode.OutputChannel = getOutputChannel(); - channel.append(message); + let channel: vscode.OutputChannel = getOutputChannel(); + channel.append(message); - let extensionLog : string | undefined = configuration.getExtensionLog(); - if (extensionLog) { - fs.appendFileSync(extensionLog, message); - } + let extensionLog: string | undefined = configuration.getExtensionLog(); + if (extensionLog) { + fs.appendFileSync(extensionLog, message); + } } diff --git a/src/make.ts b/src/make.ts index 21c8d24..f5ef734 100644 --- a/src/make.ts +++ b/src/make.ts @@ -3,111 +3,143 @@ // Support for make operations -import * as configuration from './configuration'; -import * as cpp from 'vscode-cpptools'; -import * as cpptools from './cpptools'; -import {extension} from './extension'; -import * as fs from 'fs'; -import * as logger from './logger'; -import * as parser from './parser'; -import * as path from 'path'; -import * as util from './util'; -import * as telemetry from './telemetry'; -import * as vscode from 'vscode'; +import * as configuration from "./configuration"; +import * as cpp from "vscode-cpptools"; +import * as cpptools from "./cpptools"; +import { extension } from "./extension"; +import * as fs from "fs"; +import * as logger from "./logger"; +import * as parser from "./parser"; +import * as path from "path"; +import * as util from "./util"; +import * as telemetry from "./telemetry"; +import * as vscode from "vscode"; import { v4 as uuidv4 } from "uuid"; -import * as nls from 'vscode-nls'; -nls.config({ messageFormat: nls.MessageFormat.bundle, bundleFormat: nls.BundleFormat.standalone })(); +import * as nls from "vscode-nls"; +nls.config({ + messageFormat: nls.MessageFormat.bundle, + bundleFormat: nls.BundleFormat.standalone, +})(); const localize: nls.LocalizeFunc = nls.loadMessageBundle(); const recursiveString = localize("recursive", "(recursive)"); let isBuilding: boolean = false; -export function getIsBuilding(): boolean { return isBuilding; } +export function getIsBuilding(): boolean { + return isBuilding; +} export function setIsBuilding(building: boolean): void { - isBuilding = building; + isBuilding = building; } let isConfiguring: boolean = false; -export function getIsConfiguring(): boolean { return isConfiguring; } -export function setIsConfiguring(configuring: boolean): void { isConfiguring = configuring; } +export function getIsConfiguring(): boolean { + return isConfiguring; +} +export function setIsConfiguring(configuring: boolean): void { + isConfiguring = configuring; +} let configureIsInBackground: boolean = false; -export function getConfigureIsInBackground(): boolean { return configureIsInBackground; } -export function setConfigureIsInBackground(background: boolean): void { configureIsInBackground = background; } +export function getConfigureIsInBackground(): boolean { + return configureIsInBackground; +} +export function setConfigureIsInBackground(background: boolean): void { + configureIsInBackground = background; +} let configureIsClean: boolean = false; -export function getConfigureIsClean(): boolean { return configureIsClean; } -export function setConfigureIsClean(clean: boolean): void { configureIsClean = clean; } +export function getConfigureIsClean(): boolean { + return configureIsClean; +} +export function setConfigureIsClean(clean: boolean): void { + configureIsClean = clean; +} let isPreConfiguring: boolean = false; -export function getIsPreConfiguring(): boolean { return isPreConfiguring; } -export function setIsPreConfiguring(preConfiguring: boolean): void { isPreConfiguring = preConfiguring; } +export function getIsPreConfiguring(): boolean { + return isPreConfiguring; +} +export function setIsPreConfiguring(preConfiguring: boolean): void { + isPreConfiguring = preConfiguring; +} let isPostConfiguring: boolean = false; -export function getIsPostConfiguring(): boolean { return isPostConfiguring; }; -export function setIsPostConfiguring(postConfiguring: boolean): void { isPostConfiguring = postConfiguring; } +export function getIsPostConfiguring(): boolean { + return isPostConfiguring; +} +export function setIsPostConfiguring(postConfiguring: boolean): void { + isPostConfiguring = postConfiguring; +} // Leave positive error codes for make exit values export enum ConfigureBuildReturnCodeTypes { - success = 0, - blocked = -1, - cancelled = -2, - notFound = -3, - outOfDate = -4, - other = -5, - saveFailed = -6, - fullFeatureFalse = -7, - untrusted = -8, + success = 0, + blocked = -1, + cancelled = -2, + notFound = -3, + outOfDate = -4, + other = -5, + saveFailed = -6, + fullFeatureFalse = -7, + untrusted = -8, } export enum Operations { - preConfigure = "pre-configure", - postConfigure = "post-configure", - configure = "configure", - build = "build", - changeConfiguration = "change makefile configuration", - changeBuildTarget = "change build target", - changeLaunchTarget = "change launch target", - debug = "debug", - run = "run" + preConfigure = "pre-configure", + postConfigure = "post-configure", + configure = "configure", + build = "build", + changeConfiguration = "change makefile configuration", + changeBuildTarget = "change build target", + changeLaunchTarget = "change launch target", + debug = "debug", + run = "run", } export enum TriggeredBy { - buildTarget = "command pallette (buildTarget)", - buildCleanTarget = "command pallette (buildCleanTarget)", - buildAll = "command pallette (buildAll)", - buildCleanAll = "command pallette (buildCleanAll)", - preconfigure = "command pallette (preConfigure)", - alwaysPreconfigure = "settings (alwaysPreConfigure)", - postConfigure = "command pallette (postConfigure)", - alwaysPostConfigure = "settings (alwaysPostConfigure)", - configure = "command pallette (configure)", - configureOnOpen = "settings (configureOnOpen)", - cleanConfigureOnOpen = "configure dirty (on open), settings (configureOnOpen)", - cleanConfigure = "command pallette (clean configure)", - configureBeforeBuild = "configure dirty (before build), settings (configureAfterCommand)", - configureAfterConfigurationChange = "settings (configureAfterCommand), command pallette (setBuildConfiguration)", - configureAfterEditorFocusChange = "configure dirty (editor focus change), settings (configureOnEdit)", - configureBeforeTargetChange = "configure dirty (before target change), settings (configureAfterCommand)", - configureAfterTargetChange = "settings (configureAfterCommand), command pallette (setBuildTarget)", - configureBeforeLaunchTargetChange = "configureDirty (before launch target change), settings (configureAfterCommand)", - launch = "Launch (debug|run)", - tests = "Makefile Tools Regression Tests" + buildTarget = "command pallette (buildTarget)", + buildCleanTarget = "command pallette (buildCleanTarget)", + buildAll = "command pallette (buildAll)", + buildCleanAll = "command pallette (buildCleanAll)", + preconfigure = "command pallette (preConfigure)", + alwaysPreconfigure = "settings (alwaysPreConfigure)", + postConfigure = "command pallette (postConfigure)", + alwaysPostConfigure = "settings (alwaysPostConfigure)", + configure = "command pallette (configure)", + configureOnOpen = "settings (configureOnOpen)", + cleanConfigureOnOpen = "configure dirty (on open), settings (configureOnOpen)", + cleanConfigure = "command pallette (clean configure)", + configureBeforeBuild = "configure dirty (before build), settings (configureAfterCommand)", + configureAfterConfigurationChange = "settings (configureAfterCommand), command pallette (setBuildConfiguration)", + configureAfterEditorFocusChange = "configure dirty (editor focus change), settings (configureOnEdit)", + configureBeforeTargetChange = "configure dirty (before target change), settings (configureAfterCommand)", + configureAfterTargetChange = "settings (configureAfterCommand), command pallette (setBuildTarget)", + configureBeforeLaunchTargetChange = "configureDirty (before launch target change), settings (configureAfterCommand)", + launch = "Launch (debug|run)", + tests = "Makefile Tools Regression Tests", } -let fileIndex: Map = new Map(); -let workspaceBrowseConfiguration: cpp.WorkspaceBrowseConfiguration = { browsePath: [] }; +let fileIndex: Map = new Map< + string, + cpptools.SourceFileConfigurationItem +>(); +let workspaceBrowseConfiguration: cpp.WorkspaceBrowseConfiguration = { + browsePath: [], +}; export function getDeltaCustomConfigurationProvider(): cpptools.CustomConfigurationProvider { - let provider: cpptools.CustomConfigurationProvider = { - fileIndex: fileIndex, - workspaceBrowse: workspaceBrowseConfiguration - }; + let provider: cpptools.CustomConfigurationProvider = { + fileIndex: fileIndex, + workspaceBrowse: workspaceBrowseConfiguration, + }; - return provider; + return provider; } -export function setCustomConfigurationProvider(provider: cpptools.CustomConfigurationProvider): void { - fileIndex = provider.fileIndex; - workspaceBrowseConfiguration = provider.workspaceBrowse; +export function setCustomConfigurationProvider( + provider: cpptools.CustomConfigurationProvider +): void { + fileIndex = provider.fileIndex; + workspaceBrowseConfiguration = provider.workspaceBrowse; } // Identifies and logs whether an operation should be prevented from running. @@ -120,89 +152,111 @@ export function setCustomConfigurationProvider(provider: cpptools.CustomConfigur // Clicking the status bar buttons attempts to run the corresponding operation, // which triggers a popup and returns early if it should be blocked. Same for pallette commands. // In future we may enable/disable or change text depending on the blocking state. -export function blockedByOp(op: Operations, showPopup: boolean = true): Operations | undefined { - let blocker: Operations | undefined; +export function blockedByOp( + op: Operations, + showPopup: boolean = true +): Operations | undefined { + let blocker: Operations | undefined; - if (getIsPreConfiguring()) { - blocker = Operations.preConfigure; + if (getIsPreConfiguring()) { + blocker = Operations.preConfigure; + } + + if (getIsPostConfiguring()) { + blocker = Operations.postConfigure; + } + + if (getIsConfiguring()) { + // A configure in the background shouldn't block anything except another configure + if (getConfigureIsInBackground() && op !== Operations.configure) { + vscode.window.showInformationMessage( + localize( + "project.configuring.background.op.may.run.on.out.of.date.input", + "The project is configuring in the background and {0} may run on out-of-date input.", + op + ) + ); + } else { + blocker = Operations.configure; } + } - if (getIsPostConfiguring()) { - blocker = Operations.postConfigure; - } + if (getIsBuilding()) { + blocker = Operations.build; + } - if (getIsConfiguring()) { - // A configure in the background shouldn't block anything except another configure - if (getConfigureIsInBackground() && op !== Operations.configure) { - vscode.window.showInformationMessage(localize("project.configuring.background.op.may.run.on.out.of.date.input", - "The project is configuring in the background and {0} may run on out-of-date input.", op)); - } else { - blocker = Operations.configure; - } - } + if (blocker && showPopup) { + vscode.window.showErrorMessage( + localize( + "cannot.op.because.project.already.doing", + "Cannot {0} because the project is already doing a '{1}'.", + `'${op}'`, + blocker + ) + ); + } - if (getIsBuilding()) { - blocker = Operations.build; - } - - if (blocker && showPopup) { - vscode.window.showErrorMessage(localize("cannot.op.because.project.already.doing", "Cannot {0} because the project is already doing a '{1}'.", `'${op}'`, blocker)); - } - - return blocker; + return blocker; } -async function saveAll(): Promise { - if (configuration.getSaveBeforeBuildOrConfigure()) { - logger.message("Saving opened files before build."); - let saveSuccess: boolean = await vscode.workspace.saveAll(); - if (saveSuccess) { - return true; - } else { - logger.message("Saving opened files failed."); - let yesButton: string = localize("yes", "Yes"); - let noButton: string = localize("no", "No"); - const chosen: vscode.MessageItem | undefined = await vscode.window.showErrorMessage("Saving opened files failed. Do you want to continue anyway?", - { - title: yesButton, - isCloseAffordance: false, - }, - { - title: noButton, - isCloseAffordance: true - }); - - return chosen !== undefined && chosen.title === yesButton; - } +async function saveAll(): Promise { + if (configuration.getSaveBeforeBuildOrConfigure()) { + logger.message("Saving opened files before build."); + let saveSuccess: boolean = await vscode.workspace.saveAll(); + if (saveSuccess) { + return true; } else { - return true; + logger.message("Saving opened files failed."); + let yesButton: string = localize("yes", "Yes"); + let noButton: string = localize("no", "No"); + const chosen: vscode.MessageItem | undefined = + await vscode.window.showErrorMessage( + "Saving opened files failed. Do you want to continue anyway?", + { + title: yesButton, + isCloseAffordance: false, + }, + { + title: noButton, + isCloseAffordance: true, + } + ); + + return chosen !== undefined && chosen.title === yesButton; } + } else { + return true; + } } export function prepareBuildTarget(target: string): string[] { - let makeArgs: string[] = []; + let makeArgs: string[] = []; - // Prepend the target to the arguments given in the configurations json. - // If a clean build is desired, "clean" should precede the target. - if (target) { - makeArgs.push(target); - } + // Prepend the target to the arguments given in the configurations json. + // If a clean build is desired, "clean" should precede the target. + if (target) { + makeArgs.push(target); + } - makeArgs = makeArgs.concat(configuration.getConfigurationMakeArgs()); + makeArgs = makeArgs.concat(configuration.getConfigurationMakeArgs()); - logger.message(`Building target "${target}" with command: '${configuration.getConfigurationMakeCommand()} ${makeArgs.join(" ")}'`); - return makeArgs; + logger.message( + `Building target "${target}" with command: '${configuration.getConfigurationMakeCommand()} ${makeArgs.join( + " " + )}'` + ); + return makeArgs; } // Build targets allow list for telemetry function processTargetForTelemetry(target: string | undefined): string { - if (!target || target === "") { - return "(unset)"; - } else if (target === "all" || target === "clean") { - return target; - } + if (!target || target === "") { + return "(unset)"; + } else if (target === "all" || target === "clean") { + return target; + } - return "..."; // private undisclosed info + return "..."; // private undisclosed info } // PID of the process that may be running currently. @@ -210,654 +264,865 @@ function processTargetForTelemetry(target: string | undefined): string { // (make for configure, make for build or pre-configure cmd/bash). // TODO: improve the code regarding curPID and how util.spawnChildProcess is setting it in make.ts unit. let curPID: number = -1; -export function getCurPID(): number { return curPID; } -export function setCurPID(pid: number): void { curPID = pid; } +export function getCurPID(): number { + return curPID; +} +export function setCurPID(pid: number): void { + curPID = pid; +} const makefileBuildTaskName: string = "Makefile Tools Build Task"; -export async function buildTarget(triggeredBy: TriggeredBy, target: string, clean: boolean = false): Promise { - if (blockedByOp(Operations.build)) { - return ConfigureBuildReturnCodeTypes.blocked; +export async function buildTarget( + triggeredBy: TriggeredBy, + target: string, + clean: boolean = false +): Promise { + if (blockedByOp(Operations.build)) { + return ConfigureBuildReturnCodeTypes.blocked; + } + + if (!saveAll()) { + return ConfigureBuildReturnCodeTypes.saveFailed; + } + + // Same start time for build and an eventual configure. + let buildStartTime: number = Date.now(); + + // warn about an out of date configure state and configure if makefile.configureAfterCommand allows. + let configureExitCode: number | undefined; // used for telemetry + let configureElapsedTime: number | undefined; // used for telemetry + if (extension.getState().configureDirty) { + logger.message( + "The project needs to configure in order to build properly the current target." + ); + if (configuration.getConfigureAfterCommand()) { + configureExitCode = await configure(TriggeredBy.configureBeforeBuild); + if (configureExitCode !== ConfigureBuildReturnCodeTypes.success) { + logger.message("Attempting to run build after a failed configure."); + } + + configureElapsedTime = util.elapsedTimeSince(buildStartTime); } + } - if (!saveAll()) { - return ConfigureBuildReturnCodeTypes.saveFailed; + // Prepare a notification popup + let config: string | undefined = + configuration.getCurrentMakefileConfiguration(); + let configAndTarget: string = config; + if (target) { + target = target.trimLeft(); + if (target !== "") { + configAndTarget += "/" + target; } + } - // Same start time for build and an eventual configure. - let buildStartTime: number = Date.now(); + configAndTarget = `"${configAndTarget}"`; + let popupStr: string = localize( + "make.popupStr", + "Building ${0} the current makefile configuration ${0}", + clean ? "clean " : "", + configAndTarget + ); - // warn about an out of date configure state and configure if makefile.configureAfterCommand allows. - let configureExitCode: number | undefined; // used for telemetry - let configureElapsedTime: number | undefined; // used for telemetry - if (extension.getState().configureDirty) { - logger.message("The project needs to configure in order to build properly the current target."); - if (configuration.getConfigureAfterCommand()) { - configureExitCode = await configure(TriggeredBy.configureBeforeBuild); - if (configureExitCode !== ConfigureBuildReturnCodeTypes.success) { - logger.message("Attempting to run build after a failed configure."); - } + let cancelBuild: boolean = false; // when the build was cancelled by the user - configureElapsedTime = util.elapsedTimeSince(buildStartTime); - } - } + try { + return await vscode.window.withProgress( + { + location: vscode.ProgressLocation.Notification, + title: popupStr, + cancellable: true, + }, + async (progress, cancel) => { + cancel.onCancellationRequested(async () => { + progress.report({ + increment: 1, + message: localize("make.build.cancelling", "Cancelling..."), + }); + logger.message("The user is cancelling the build..."); + cancelBuild = true; - // Prepare a notification popup - let config: string | undefined = configuration.getCurrentMakefileConfiguration(); - let configAndTarget: string = config; - if (target) { - target = target.trimLeft(); - if (target !== "") { - configAndTarget += "/" + target; - } - } + // Kill the task that is used for building. + // This will take care of all processes that were spawned. + let myTask: vscode.TaskExecution | undefined = + vscode.tasks.taskExecutions.find((tsk) => { + if (tsk.task.name === makefileBuildTaskName) { + return tsk; + } + }); - configAndTarget = `"${configAndTarget}"`; - let popupStr: string = localize("make.popupStr", "Building ${0} the current makefile configuration ${0}", clean ? "clean " : "", configAndTarget); - - let cancelBuild: boolean = false; // when the build was cancelled by the user - - try { - return await vscode.window.withProgress({ - location: vscode.ProgressLocation.Notification, - title: popupStr, - cancellable: true, - }, - async (progress, cancel) => { - cancel.onCancellationRequested(async () => { - progress.report({increment: 1, message: localize("make.build.cancelling", "Cancelling...")}); - logger.message("The user is cancelling the build..."); - cancelBuild = true; - - // Kill the task that is used for building. - // This will take care of all processes that were spawned. - let myTask: vscode.TaskExecution | undefined = vscode.tasks.taskExecutions.find(tsk => { - if (tsk.task.name === makefileBuildTaskName) { - return tsk; - } - }); - - logger.message(`Killing task "${makefileBuildTaskName}".`); - myTask?.terminate(); - }); - - setIsBuilding(true); - - // If required by the "makefile.clearOutputBeforeBuild" setting, - // we need to clear the terminal output when "make"-ing target "clean" - // (but not when "make"-ing the following intended target, so that we see both together) - // or when "make"-ing the intended target in case of a not-clean build. - let clearOutput: boolean = configuration.getClearOutputBeforeBuild() || false; - - if (clean) { - // We don't need to track the return code for 'make "clean"'. - // We want to proceed with the 'make "target"' step anyway. - // The relevant return code for telemetry will be the second one. - // If the clean step fails, doBuildTarget will print an error message in the terminal. - await doBuildTarget(progress, "clean", clearOutput); - } - - let retc: number = await doBuildTarget(progress, target, clearOutput && !clean); - - // We need to know whether this build was cancelled by the user - // more than the real exit code of the make process in this circumstance. - if (cancelBuild) { - retc = ConfigureBuildReturnCodeTypes.cancelled; - } - - let buildElapsedTime: number = util.elapsedTimeSince(buildStartTime); - const telemetryProperties: telemetry.Properties = { - exitCode: retc?.toString() || "undefined", - target: processTargetForTelemetry(target), - triggeredBy: triggeredBy - }; - const telemetryMeasures: telemetry.Measures = { - buildTotalElapsedTime: buildElapsedTime - }; - - // Report if this build ran also a configure and how long it took. - if (configureExitCode !== undefined) { - telemetryProperties.configureExitCode = configureExitCode.toString(); - } - if (configureElapsedTime !== undefined) { - telemetryMeasures.configureElapsedTime = configureElapsedTime; - } - - telemetry.logEvent("build", telemetryProperties, telemetryMeasures); - - cancelBuild = false; - - return retc; - }, - ); - } finally { - setIsBuilding(false); - } -} - -export async function doBuildTarget(progress: vscode.Progress<{}>, target: string, clearTerminalOutput: boolean): Promise { - let makeArgs: string[] = prepareBuildTarget(target); - try { - const quotingStlye: vscode.ShellQuoting = vscode.ShellQuoting.Strong; - const quotingStyleName: string = "Strong"; - let myTaskCommand: vscode.ShellQuotedString = {value: configuration.getConfigurationMakeCommand(), quoting: quotingStlye}; - let myTaskArgs: vscode.ShellQuotedString[] = makeArgs.map(arg => { - return {value: arg, quoting: quotingStlye}; + logger.message(`Killing task "${makefileBuildTaskName}".`); + myTask?.terminate(); }); - const cwd: string = configuration.makeBaseDirectory(); - if (!util.checkDirectoryExistsSync(cwd)) { - logger.message(`Target "${target}" failed to build because CWD passed in does not exist (${cwd}).`); - return ConfigureBuildReturnCodeTypes.notFound; + setIsBuilding(true); + + // If required by the "makefile.clearOutputBeforeBuild" setting, + // we need to clear the terminal output when "make"-ing target "clean" + // (but not when "make"-ing the following intended target, so that we see both together) + // or when "make"-ing the intended target in case of a not-clean build. + let clearOutput: boolean = + configuration.getClearOutputBeforeBuild() || false; + + if (clean) { + // We don't need to track the return code for 'make "clean"'. + // We want to proceed with the 'make "target"' step anyway. + // The relevant return code for telemetry will be the second one. + // If the clean step fails, doBuildTarget will print an error message in the terminal. + await doBuildTarget(progress, "clean", clearOutput); } - let myTaskOptions: vscode.ShellExecutionOptions = {env: util.mergeEnvironment(process.env as util.EnvironmentVariables), cwd}; + let retc: number = await doBuildTarget( + progress, + target, + clearOutput && !clean + ); - let shellExec: vscode.ShellExecution = new vscode.ShellExecution(myTaskCommand, myTaskArgs, myTaskOptions); - let myTask: vscode.Task = new vscode.Task({type: "shell", group: "build", label: makefileBuildTaskName}, - vscode.TaskScope.Workspace, makefileBuildTaskName, "makefile", shellExec); - - myTask.problemMatchers = configuration.getConfigurationProblemMatchers(); - myTask.presentationOptions.clear = clearTerminalOutput; - myTask.presentationOptions.showReuseMessage = true; - - logger.message(`Executing task: "${myTask.name}" with quoting style "${quotingStyleName}"\n command name: ${myTaskCommand.value}\n command args ${makeArgs.join()}`, "Debug"); - await vscode.tasks.executeTask(myTask); - - const result: number = await(new Promise(resolve => { - let disposable: vscode.Disposable = vscode.tasks.onDidEndTaskProcess((e: vscode.TaskProcessEndEvent) => { - if (e.execution.task.name === makefileBuildTaskName) { - disposable.dispose(); - resolve(e.exitCode ?? ConfigureBuildReturnCodeTypes.other); - } - }); - })); - - if (result !== ConfigureBuildReturnCodeTypes.success) { - logger.message(`Target "${target}" failed to build.`); - } else { - logger.message(`Target "${target}" built successfully.`); + // We need to know whether this build was cancelled by the user + // more than the real exit code of the make process in this circumstance. + if (cancelBuild) { + retc = ConfigureBuildReturnCodeTypes.cancelled; } - return result; - } catch (error) { - // No need for notification popup, since the build result is visible already in the output channel - logger.message(error); - return ConfigureBuildReturnCodeTypes.notFound; + let buildElapsedTime: number = util.elapsedTimeSince(buildStartTime); + const telemetryProperties: telemetry.Properties = { + exitCode: retc?.toString() || "undefined", + target: processTargetForTelemetry(target), + triggeredBy: triggeredBy, + }; + const telemetryMeasures: telemetry.Measures = { + buildTotalElapsedTime: buildElapsedTime, + }; + + // Report if this build ran also a configure and how long it took. + if (configureExitCode !== undefined) { + telemetryProperties.configureExitCode = configureExitCode.toString(); + } + if (configureElapsedTime !== undefined) { + telemetryMeasures.configureElapsedTime = configureElapsedTime; + } + + telemetry.logEvent("build", telemetryProperties, telemetryMeasures); + + cancelBuild = false; + + return retc; + } + ); + } finally { + setIsBuilding(false); + } +} + +export async function doBuildTarget( + progress: vscode.Progress<{}>, + target: string, + clearTerminalOutput: boolean +): Promise { + let makeArgs: string[] = prepareBuildTarget(target); + try { + const quotingStlye: vscode.ShellQuoting = vscode.ShellQuoting.Strong; + const quotingStyleName: string = "Strong"; + let myTaskCommand: vscode.ShellQuotedString = { + value: configuration.getConfigurationMakeCommand(), + quoting: quotingStlye, + }; + let myTaskArgs: vscode.ShellQuotedString[] = makeArgs.map((arg) => { + return { value: arg, quoting: quotingStlye }; + }); + + const cwd: string = configuration.makeBaseDirectory(); + if (!util.checkDirectoryExistsSync(cwd)) { + logger.message( + `Target "${target}" failed to build because CWD passed in does not exist (${cwd}).` + ); + return ConfigureBuildReturnCodeTypes.notFound; } + + let myTaskOptions: vscode.ShellExecutionOptions = { + env: util.mergeEnvironment(process.env as util.EnvironmentVariables), + cwd, + }; + + let shellExec: vscode.ShellExecution = new vscode.ShellExecution( + myTaskCommand, + myTaskArgs, + myTaskOptions + ); + let myTask: vscode.Task = new vscode.Task( + { type: "shell", group: "build", label: makefileBuildTaskName }, + vscode.TaskScope.Workspace, + makefileBuildTaskName, + "makefile", + shellExec + ); + + myTask.problemMatchers = configuration.getConfigurationProblemMatchers(); + myTask.presentationOptions.clear = clearTerminalOutput; + myTask.presentationOptions.showReuseMessage = true; + + logger.message( + `Executing task: "${ + myTask.name + }" with quoting style "${quotingStyleName}"\n command name: ${ + myTaskCommand.value + }\n command args ${makeArgs.join()}`, + "Debug" + ); + await vscode.tasks.executeTask(myTask); + + const result: number = await new Promise((resolve) => { + let disposable: vscode.Disposable = vscode.tasks.onDidEndTaskProcess( + (e: vscode.TaskProcessEndEvent) => { + if (e.execution.task.name === makefileBuildTaskName) { + disposable.dispose(); + resolve(e.exitCode ?? ConfigureBuildReturnCodeTypes.other); + } + } + ); + }); + + if (result !== ConfigureBuildReturnCodeTypes.success) { + logger.message(`Target "${target}" failed to build.`); + } else { + logger.message(`Target "${target}" built successfully.`); + } + + return result; + } catch (error) { + // No need for notification popup, since the build result is visible already in the output channel + logger.message(error); + return ConfigureBuildReturnCodeTypes.notFound; + } } // Content to be parsed by various operations post configure (like finding all build/launch targets). // Represents the content of the provided makefile.buildLog or a fresh output of make --dry-run // (which is also written into makefile.configurationCachePath). let parseContent: string | undefined; -export function getParseContent(): string | undefined { return parseContent; } -export function setParseContent(content: string): void { parseContent = content; } +export function getParseContent(): string | undefined { + return parseContent; +} +export function setParseContent(content: string): void { + parseContent = content; +} // The source file of parseContent (build log or configuration dryrun cache). let parseFile: string | undefined; -export function getParseFile(): string | undefined { return parseFile; } -export function setParseFile(file: string): void { parseFile = file; } +export function getParseFile(): string | undefined { + return parseFile; +} +export function setParseFile(file: string): void { + parseFile = file; +} // Targets need to parse a dryrun make invocation that does not include a target name // (other than default empty "" or the standard "all"), otherwise it would produce // a subset of all the targets involved in the makefile (only the ones triggered // by building the current target). -export async function generateParseContent(progress: vscode.Progress<{}>, - cancel: vscode.CancellationToken, - forTargets: boolean = false, - recursive: boolean = false): Promise { - if (cancel.isCancellationRequested) { - return { - retc: ConfigureBuildReturnCodeTypes.cancelled, - elapsed: 0 - }; +export async function generateParseContent( + progress: vscode.Progress<{}>, + cancel: vscode.CancellationToken, + forTargets: boolean = false, + recursive: boolean = false +): Promise { + if (cancel.isCancellationRequested) { + return { + retc: ConfigureBuildReturnCodeTypes.cancelled, + elapsed: 0, + }; + } + + let startTime: number = Date.now(); + + // Rules for parse content and file: + // 1. makefile.buildLog provided by the user in settings + // 2. configuration cache (the previous dryrun output): makefile.configurationCachePath + // 3. the make dryrun output if (2) is missing + // We do not use buildLog for build targets analysis because + // we can afford to invoke make -pRrq (very quick even on large projects). + // We make sure to give the regression tests suite a build log that already contains + // targets information because we want to avoid invoking make for now. + let buildLog: string | undefined = configuration.getConfigurationBuildLog(); + if ( + buildLog && + (!forTargets || process.env["MAKEFILE_TOOLS_TESTING"] === "1") + ) { + parseContent = util.readFile(buildLog); + if (parseContent) { + parseFile = buildLog; + return { + retc: ConfigureBuildReturnCodeTypes.success, + elapsed: util.elapsedTimeSince(startTime), + }; } + } - let startTime: number = Date.now(); + const dryRunString = localize( + "make.generating.dryrun", + "Generating dry-run output" + ); + const forTargetsString = localize( + "make.generating.forTargets", + "(for targets specifically)" + ); + progress.report({ + increment: 1, + message: + dryRunString + + (recursive ? ` ${recursiveString}` : "") + + (forTargets ? ` ${forTargetsString}` : "" + "..."), + }); - // Rules for parse content and file: - // 1. makefile.buildLog provided by the user in settings - // 2. configuration cache (the previous dryrun output): makefile.configurationCachePath - // 3. the make dryrun output if (2) is missing - // We do not use buildLog for build targets analysis because - // we can afford to invoke make -pRrq (very quick even on large projects). - // We make sure to give the regression tests suite a build log that already contains - // targets information because we want to avoid invoking make for now. - let buildLog: string | undefined = configuration.getConfigurationBuildLog(); - if (buildLog && (!forTargets || process.env['MAKEFILE_TOOLS_TESTING'] === '1')) { - parseContent = util.readFile(buildLog); - if (parseContent) { - parseFile = buildLog; - return { - retc: ConfigureBuildReturnCodeTypes.success, - elapsed: util.elapsedTimeSince(startTime) - }; - } + // Continue with the make dryrun invocation + let makeArgs: string[] = []; + + // Prepend the target to the arguments given in the makefile.configurations object, + // unless we want to parse for the full set of available targets. + if (forTargets) { + makeArgs.push("all"); + } else { + let currentTarget: string | undefined = configuration.getCurrentTarget(); + if (currentTarget) { + makeArgs.push(currentTarget); } + } - const dryRunString = localize("make.generating.dryrun", "Generating dry-run output"); - const forTargetsString = localize("make.generating.forTargets", "(for targets specifically)"); - progress.report({ - increment: 1, message: dryRunString + - ((recursive) ? ` ${recursiveString}` : "") + - ((forTargets) ? ` ${forTargetsString}` : "" + - "...") + // Include all the make arguments defined in makefile.configurations.makeArgs + makeArgs = makeArgs.concat(configuration.getConfigurationMakeArgs()); + + // If we are analyzing build targets, we need the following switches: + // --print-data-base (which generates verbose output where we parse targets from). + // --no-builtin-variables and --no-builtin-rules (to reduce the size of the + // output produced by --print-data-base and also to obtain a list of targets + // that make sense, skipping over implicit targets like objects from sources + // or binaries from objects and libs). + // --question (to not execute anything, for us equivalent of dry-run + // but without printing commands, which contributes again to a smaller output). + // If we are analyzing compiler/linker commands for IntelliSense and launch targets, + // we use --dry-run and anything from makefile.dryrunSwitches. + const dryrunSwitches: string[] | undefined = + configuration.getDryrunSwitches(); + if (forTargets) { + makeArgs.push("--print-data-base"); + makeArgs.push("--no-builtin-variables"); + makeArgs.push("--no-builtin-rules"); + makeArgs.push("--question"); + logger.messageNoCR("Generating targets information with command: "); + } else { + makeArgs.push("--dry-run"); + + // If this is not a clean configure, remove --always-make from the arguments list. + // We need to have --always-make in makefile.dryrunSwitches and remove it for not clean configure + // (as opposed to not having --always-make in makefile.dryrunSwitches and adding it for clean configure) + // because we want to avoid having 2 dryrun switches settings (one for clean and one for not clean configure) + // and also because the user needs to be able to remove --always-make from any make --dry-run invocation, + // if it causes trouble. + dryrunSwitches?.forEach((sw) => { + if (getConfigureIsClean() || (sw !== "--always-make" && sw !== "-B")) { + makeArgs.push(sw); + } }); - // Continue with the make dryrun invocation - let makeArgs: string[] = []; + logger.messageNoCR( + `Generating ${ + getConfigureIsInBackground() ? "in the background a new " : "" + }configuration cache with command: ` + ); + } - // Prepend the target to the arguments given in the makefile.configurations object, - // unless we want to parse for the full set of available targets. - if (forTargets) { - makeArgs.push("all"); - } else { - let currentTarget: string | undefined = configuration.getCurrentTarget(); - if (currentTarget) { - makeArgs.push(currentTarget); - } + logger.message( + `'${configuration.getConfigurationMakeCommand()} ${makeArgs.join(" ")}'` + ); + + try { + let dryrunFile: string = forTargets ? "./targets.log" : "./dryrun.log"; + let extensionOutputFolder: string | undefined = + configuration.getExtensionOutputFolder(); + if (extensionOutputFolder) { + dryrunFile = path.join(extensionOutputFolder, dryrunFile); } + dryrunFile = util.resolvePathToRoot(dryrunFile); + logger.message(`Writing the dry-run output: ${dryrunFile}`); - // Include all the make arguments defined in makefile.configurations.makeArgs - makeArgs = makeArgs.concat(configuration.getConfigurationMakeArgs()); + const lineEnding: string = + process.platform === "win32" && process.env.MSYSTEM === undefined + ? "\r\n" + : "\n"; - // If we are analyzing build targets, we need the following switches: - // --print-data-base (which generates verbose output where we parse targets from). - // --no-builtin-variables and --no-builtin-rules (to reduce the size of the - // output produced by --print-data-base and also to obtain a list of targets - // that make sense, skipping over implicit targets like objects from sources - // or binaries from objects and libs). - // --question (to not execute anything, for us equivalent of dry-run - // but without printing commands, which contributes again to a smaller output). - // If we are analyzing compiler/linker commands for IntelliSense and launch targets, - // we use --dry-run and anything from makefile.dryrunSwitches. - const dryrunSwitches: string[] | undefined = configuration.getDryrunSwitches(); - if (forTargets) { - makeArgs.push("--print-data-base"); - makeArgs.push("--no-builtin-variables"); - makeArgs.push("--no-builtin-rules"); - makeArgs.push("--question"); - logger.messageNoCR("Generating targets information with command: "); - } else { - makeArgs.push("--dry-run"); + util.writeFile( + dryrunFile, + `${configuration.getConfigurationMakeCommand()} ${makeArgs.join( + " " + )}${lineEnding}` + ); - // If this is not a clean configure, remove --always-make from the arguments list. - // We need to have --always-make in makefile.dryrunSwitches and remove it for not clean configure - // (as opposed to not having --always-make in makefile.dryrunSwitches and adding it for clean configure) - // because we want to avoid having 2 dryrun switches settings (one for clean and one for not clean configure) - // and also because the user needs to be able to remove --always-make from any make --dry-run invocation, - // if it causes trouble. - dryrunSwitches?.forEach(sw => { - if (getConfigureIsClean() || (sw !== "--always-make" && sw !== "-B")) { - makeArgs.push(sw); - } - }); + let completeOutput: string = ""; + let stderrStr: string = ""; + let heartBeat: number = Date.now(); - logger.messageNoCR(`Generating ${getConfigureIsInBackground() ? "in the background a new " : ""}configuration cache with command: `); - } + let stdout: any = (result: string): void => { + const appendStr: string = `${result} ${lineEnding}`; + completeOutput += appendStr; + fs.appendFileSync(dryrunFile, appendStr); - logger.message(`'${configuration.getConfigurationMakeCommand()} ${makeArgs.join(" ")}'`); + progress.report({ + increment: 1, + message: + dryRunString + + (recursive ? ` ${recursiveString}` : "") + + (forTargets ? ` ${forTargetsString}` : "" + "..."), + }); - try { - let dryrunFile : string = forTargets ? "./targets.log" : "./dryrun.log"; - let extensionOutputFolder: string | undefined = configuration.getExtensionOutputFolder(); - if (extensionOutputFolder) { - dryrunFile = path.join(extensionOutputFolder, dryrunFile); - } - dryrunFile = util.resolvePathToRoot(dryrunFile); - logger.message(`Writing the dry-run output: ${dryrunFile}`); + heartBeat = Date.now(); + }; - const lineEnding: string = (process.platform === "win32" && process.env.MSYSTEM === undefined) ? "\r\n" : "\n"; + let stderr: any = (result: string): void => { + // We need this lineEnding to see more clearly the output coming from all these compilers and tools. + // But there is some unpredictability regarding how much these tools fragment their output, on various + // OSes and systems. To compare easily against a fix baseline, don't use lineEnding while running tests. + // So far this has been seen for stderr and not for stdout. + let appendStr: string = result; + if (process.env["MAKEFILE_TOOLS_TESTING"] !== "1") { + appendStr += lineEnding; + } + fs.appendFileSync(dryrunFile, appendStr); + stderrStr += appendStr; - util.writeFile(dryrunFile, `${configuration.getConfigurationMakeCommand()} ${makeArgs.join(" ")}${lineEnding}`); + // Sometimes there is useful information coming via the stderr + // (one example is even a bug of the make tool, because it reports + // "Entering directory" on stderr instead of stdout causing various issues). + completeOutput += appendStr; + }; - let completeOutput: string = ""; - let stderrStr: string = ""; - let heartBeat: number = Date.now(); + const heartBeatTimeout: number = 30; // half minute. TODO: make this a setting + let timeout: NodeJS.Timeout = setInterval(function (): void { + let elapsedHeartBit: number = util.elapsedTimeSince(heartBeat); + if (elapsedHeartBit > heartBeatTimeout) { + vscode.window.showWarningMessage( + "Dryrun timeout. See Makefile Tools Output Channel for details." + ); + logger.message( + "Dryrun timeout. Verify that the make command works properly " + + "in your development terminal (it could wait for stdin)." + ); + logger.message(`Double check the dryrun output log: ${dryrunFile}`); - let stdout: any = (result: string): void => { - const appendStr: string = `${result} ${lineEnding}`; - completeOutput += appendStr; - fs.appendFileSync(dryrunFile, appendStr); - - progress.report({ - increment: 1, - message: - dryRunString + - (recursive ? ` ${recursiveString}` : "") + - (forTargets ? ` ${forTargetsString}` : "" + "..."), - }); - - - heartBeat = Date.now(); - }; - - let stderr: any = (result: string): void => { - // We need this lineEnding to see more clearly the output coming from all these compilers and tools. - // But there is some unpredictability regarding how much these tools fragment their output, on various - // OSes and systems. To compare easily against a fix baseline, don't use lineEnding while running tests. - // So far this has been seen for stderr and not for stdout. - let appendStr: string = result; - if (process.env['MAKEFILE_TOOLS_TESTING'] !== '1') { - appendStr += lineEnding; - } - fs.appendFileSync(dryrunFile, appendStr); - stderrStr += appendStr; - - // Sometimes there is useful information coming via the stderr - // (one example is even a bug of the make tool, because it reports - // "Entering directory" on stderr instead of stdout causing various issues). - completeOutput += appendStr; - }; - - const heartBeatTimeout: number = 30; // half minute. TODO: make this a setting - let timeout: NodeJS.Timeout = setInterval(function (): void { - let elapsedHeartBit: number = util.elapsedTimeSince(heartBeat); - if (elapsedHeartBit > heartBeatTimeout) { - vscode.window.showWarningMessage("Dryrun timeout. See Makefile Tools Output Channel for details."); - logger.message("Dryrun timeout. Verify that the make command works properly " + - "in your development terminal (it could wait for stdin)."); - logger.message(`Double check the dryrun output log: ${dryrunFile}`); - - // It's enough to show this warning popup once. - clearInterval(timeout); - } - }, 5 * 1000); - - // The dry-run analysis should operate on english. - const result: util.SpawnProcessResult = await util.spawnChildProcess(configuration.getConfigurationMakeCommand(), makeArgs, util.getWorkspaceRoot(), true, true, stdout, stderr); + // It's enough to show this warning popup once. clearInterval(timeout); - let elapsedTime: number = util.elapsedTimeSince(startTime); - logger.message(`Generating dry-run elapsed time: ${elapsedTime}`); + } + }, 5 * 1000); - parseFile = dryrunFile; - parseContent = completeOutput; + // The dry-run analysis should operate on english. + const result: util.SpawnProcessResult = await util.spawnChildProcess( + configuration.getConfigurationMakeCommand(), + makeArgs, + util.getWorkspaceRoot(), + true, + true, + stdout, + stderr + ); + clearInterval(timeout); + let elapsedTime: number = util.elapsedTimeSince(startTime); + logger.message(`Generating dry-run elapsed time: ${elapsedTime}`); - // The error codes returned by the targets invocation (make -pRrq) mean something else - // (for example if targets are out of date). We can ignore the return code for this - // because it "can't fail". It represents only display of database and no targets are actually run. - // try syntax error - if (result.returnCode !== ConfigureBuildReturnCodeTypes.success && !forTargets) { - logger.message("The make dry-run command failed."); - logger.message("IntelliSense may work only partially or not at all."); - logger.message(stderrStr); + parseFile = dryrunFile; + parseContent = completeOutput; - // Report the standard dry-run error & guide only when the configure was not cancelled - // by the user (which causes retCode to be null). - // Also don't write the cache if this operation was cancelled - // because it may be incomplete and affect a future non clean configure. - if (result.returnCode !== null) { - util.reportDryRunError(dryrunFile); - } - } + // The error codes returned by the targets invocation (make -pRrq) mean something else + // (for example if targets are out of date). We can ignore the return code for this + // because it "can't fail". It represents only display of database and no targets are actually run. + // try syntax error + if ( + result.returnCode !== ConfigureBuildReturnCodeTypes.success && + !forTargets + ) { + logger.message("The make dry-run command failed."); + logger.message("IntelliSense may work only partially or not at all."); + logger.message(stderrStr); - curPID = -1; - return { - retc: result.returnCode, - elapsed: elapsedTime - }; - } catch (error) { - logger.message(error); - return { - retc: ConfigureBuildReturnCodeTypes.notFound, - elapsed: util.elapsedTimeSince(startTime) - }; + // Report the standard dry-run error & guide only when the configure was not cancelled + // by the user (which causes retCode to be null). + // Also don't write the cache if this operation was cancelled + // because it may be incomplete and affect a future non clean configure. + if (result.returnCode !== null) { + util.reportDryRunError(dryrunFile); + } } + + curPID = -1; + return { + retc: result.returnCode, + elapsed: elapsedTime, + }; + } catch (error) { + logger.message(error); + return { + retc: ConfigureBuildReturnCodeTypes.notFound, + elapsed: util.elapsedTimeSince(startTime), + }; + } } -export async function prePostConfigureHelper(titles: {configuringScript: string, cancelling: string}, configureScriptMethod: (progress: vscode.Progress<{}>) => Promise, setConfigureScriptState: (value: boolean) => void, logConfigureScriptTelemetry: (elapsedTime: number, exitCode: number) => void): Promise { - // No pre/post configure execution in untrusted workspaces. - // The check is needed also here in addition to disabling all UI and actions because, - // depending on settings, this can run automatically at project load. - if (!vscode.workspace.isTrusted) { - logger.message("No script can run in an untrusted workspace."); - return ConfigureBuildReturnCodeTypes.untrusted; - } +export async function prePostConfigureHelper( + titles: { configuringScript: string; cancelling: string }, + configureScriptMethod: (progress: vscode.Progress<{}>) => Promise, + setConfigureScriptState: (value: boolean) => void, + logConfigureScriptTelemetry: (elapsedTime: number, exitCode: number) => void +): Promise { + // No pre/post configure execution in untrusted workspaces. + // The check is needed also here in addition to disabling all UI and actions because, + // depending on settings, this can run automatically at project load. + if (!vscode.workspace.isTrusted) { + logger.message("No script can run in an untrusted workspace."); + return ConfigureBuildReturnCodeTypes.untrusted; + } - // check for being blocked by operations. - if (blockedByOp(Operations.preConfigure)) { - return ConfigureBuildReturnCodeTypes.blocked; - } + // check for being blocked by operations. + if (blockedByOp(Operations.preConfigure)) { + return ConfigureBuildReturnCodeTypes.blocked; + } - if (blockedByOp(Operations.postConfigure)) { - return ConfigureBuildReturnCodeTypes.blocked; - } + if (blockedByOp(Operations.postConfigure)) { + return ConfigureBuildReturnCodeTypes.blocked; + } - let configureScriptStartTime: number = Date.now(); + let configureScriptStartTime: number = Date.now(); - let cancelConfigureScript: boolean = false; + let cancelConfigureScript: boolean = false; - try { - return await vscode.window.withProgress({ - location: vscode.ProgressLocation.Notification, - title: titles.configuringScript, - cancellable: true - }, - async (progress, cancel) => { - cancel.onCancellationRequested(async () => { - progress.report({increment: 1, message: localize("make.prePostConfigure.cancelling", "Cancelling...")}); - cancelConfigureScript = true; + try { + return await vscode.window.withProgress( + { + location: vscode.ProgressLocation.Notification, + title: titles.configuringScript, + cancellable: true, + }, + async (progress, cancel) => { + cancel.onCancellationRequested(async () => { + progress.report({ + increment: 1, + message: localize( + "make.prePostConfigure.cancelling", + "Cancelling..." + ), + }); + cancelConfigureScript = true; - logger.message(`Attempting to kill the console process (PID = ${curPID}) and all its children subprocesses...`); + logger.message( + `Attempting to kill the console process (PID = ${curPID}) and all its children subprocesses...` + ); - await vscode.window.withProgress({ - location: vscode.ProgressLocation.Notification, - title: titles.cancelling, - cancellable: false, - }, - async (progress) => { - await util.killTree(progress, curPID); - }); - }); + await vscode.window.withProgress( + { + location: vscode.ProgressLocation.Notification, + title: titles.cancelling, + cancellable: false, + }, + async (progress) => { + await util.killTree(progress, curPID); + } + ); + }); - setConfigureScriptState(true); + setConfigureScriptState(true); - let retc: number = await configureScriptMethod(progress); + let retc: number = await configureScriptMethod(progress); - if (cancelConfigureScript) { - retc = ConfigureBuildReturnCodeTypes.cancelled; - } + if (cancelConfigureScript) { + retc = ConfigureBuildReturnCodeTypes.cancelled; + } - let configureScriptElapsedTime: number = util.elapsedTimeSince( - configureScriptStartTime - ); + let configureScriptElapsedTime: number = util.elapsedTimeSince( + configureScriptStartTime + ); - logConfigureScriptTelemetry(configureScriptElapsedTime, retc); + logConfigureScriptTelemetry(configureScriptElapsedTime, retc); - cancelConfigureScript = false; + cancelConfigureScript = false; - if (retc !== ConfigureBuildReturnCodeTypes.success) { - logger.showOutputChannel(); - } + if (retc !== ConfigureBuildReturnCodeTypes.success) { + logger.showOutputChannel(); + } - return retc; - }) - } finally { - setConfigureScriptState(false); - } + return retc; + } + ); + } finally { + setConfigureScriptState(false); + } } export async function preConfigure(triggeredBy: TriggeredBy): Promise { - let scriptFile: string | undefined = configuration.getPreConfigureScript(); - if (!scriptFile) { - vscode.window.showErrorMessage("Pre-configure failed: no script provided."); - logger.message("No pre-configure script is set in settings. " + - "Make sure a pre-configuration script path is defined with makefile.preConfigureScript."); - return ConfigureBuildReturnCodeTypes.notFound; - } + let scriptFile: string | undefined = configuration.getPreConfigureScript(); + if (!scriptFile) { + vscode.window.showErrorMessage("Pre-configure failed: no script provided."); + logger.message( + "No pre-configure script is set in settings. " + + "Make sure a pre-configuration script path is defined with makefile.preConfigureScript." + ); + return ConfigureBuildReturnCodeTypes.notFound; + } - if (!util.checkFileExistsSync(scriptFile)) { - vscode.window.showErrorMessage("Could not find pre-configure script."); - logger.message(`Could not find the given pre-configure script "${scriptFile}" on disk. `); - return ConfigureBuildReturnCodeTypes.notFound; - } + if (!util.checkFileExistsSync(scriptFile)) { + vscode.window.showErrorMessage("Could not find pre-configure script."); + logger.message( + `Could not find the given pre-configure script "${scriptFile}" on disk. ` + ); + return ConfigureBuildReturnCodeTypes.notFound; + } - // Assert that scriptFile is not undefined at this point since we've checked above. - return await prePostConfigureHelper( - { configuringScript: localize("make.preconfigure.title", "Pre-configuring ${0}", scriptFile), cancelling: localize("make.preconfigure.cancel.title", "Cancelling pre-configure")}, - (progress) => runPreConfigureScript(progress, scriptFile!), - (value) => setIsPreConfiguring(value), - (elapsedTime, exitCode) => { - const telemetryMeasures: telemetry.Measures = { - preConfigureElapsedTime: elapsedTime, - }; - const telemetryProperties: telemetry.Properties = { - exitCode: exitCode.toString(), - triggeredBy: triggeredBy, - }; - telemetry.logEvent( - "preConfigure", - telemetryProperties, - telemetryMeasures - ); - }); + // Assert that scriptFile is not undefined at this point since we've checked above. + return await prePostConfigureHelper( + { + configuringScript: localize( + "make.preconfigure.title", + "Pre-configuring ${0}", + scriptFile + ), + cancelling: localize( + "make.preconfigure.cancel.title", + "Cancelling pre-configure" + ), + }, + (progress) => runPreConfigureScript(progress, scriptFile!), + (value) => setIsPreConfiguring(value), + (elapsedTime, exitCode) => { + const telemetryMeasures: telemetry.Measures = { + preConfigureElapsedTime: elapsedTime, + }; + const telemetryProperties: telemetry.Properties = { + exitCode: exitCode.toString(), + triggeredBy: triggeredBy, + }; + telemetry.logEvent( + "preConfigure", + telemetryProperties, + telemetryMeasures + ); + } + ); } export async function postConfigure(triggeredBy: TriggeredBy): Promise { - let scriptFile: string | undefined = configuration.getPostConfigureScript(); - if (!scriptFile) { - vscode.window.showErrorMessage("Post-configure failed: no script provided."); - logger.message("No post-configure script is set in settings. " + - "Make sure a post-configuration script path is defined with makefile.postConfigureScript."); - return ConfigureBuildReturnCodeTypes.notFound; - } + let scriptFile: string | undefined = configuration.getPostConfigureScript(); + if (!scriptFile) { + vscode.window.showErrorMessage( + "Post-configure failed: no script provided." + ); + logger.message( + "No post-configure script is set in settings. " + + "Make sure a post-configuration script path is defined with makefile.postConfigureScript." + ); + return ConfigureBuildReturnCodeTypes.notFound; + } - if (!util.checkFileExistsSync(scriptFile)) { - vscode.window.showErrorMessage("Could not find post-configure script."); - logger.message(`Could not find the given post-configure script "${scriptFile}" on disk. `); - return ConfigureBuildReturnCodeTypes.notFound; - } + if (!util.checkFileExistsSync(scriptFile)) { + vscode.window.showErrorMessage("Could not find post-configure script."); + logger.message( + `Could not find the given post-configure script "${scriptFile}" on disk. ` + ); + return ConfigureBuildReturnCodeTypes.notFound; + } - // Assert that scriptFile is not undefined at this point since we've checked above. - return await prePostConfigureHelper( - { configuringScript: localize("make.postconfigure.title", "Post-configuring: ${0}", scriptFile), cancelling: localize("make.postconfigure.cancelling.title", "Cancelling post-configure")}, - (progress) => runPostConfigureScript(progress, scriptFile!), - (value) => setIsPostConfiguring(value), - (elapsedTime, exitCode) => { - const telemetryMeasures: telemetry.Measures = { - postConfigureElapsedTime: elapsedTime - }; - const telemetryProperties: telemetry.Properties = { - exitCode: exitCode.toString(), - triggeredBy: triggeredBy - }; - telemetry.logEvent("postConfigure", telemetryProperties, telemetryMeasures); - }); + // Assert that scriptFile is not undefined at this point since we've checked above. + return await prePostConfigureHelper( + { + configuringScript: localize( + "make.postconfigure.title", + "Post-configuring: ${0}", + scriptFile + ), + cancelling: localize( + "make.postconfigure.cancelling.title", + "Cancelling post-configure" + ), + }, + (progress) => runPostConfigureScript(progress, scriptFile!), + (value) => setIsPostConfiguring(value), + (elapsedTime, exitCode) => { + const telemetryMeasures: telemetry.Measures = { + postConfigureElapsedTime: elapsedTime, + }; + const telemetryProperties: telemetry.Properties = { + exitCode: exitCode.toString(), + triggeredBy: triggeredBy, + }; + telemetry.logEvent( + "postConfigure", + telemetryProperties, + telemetryMeasures + ); + } + ); } // Applies to the current process all the environment variables that resulted from the pre-configure step. // The input 'content' represents the output of a command that lists all the environment variables: // set on windows or printenv on linux/mac. -async function applyEnvironment(content: string | undefined) : Promise { - let lines: string[] = content?.split(/\r?\n/) || []; - lines.forEach(line => { - let eqPos: number = line.search("="); - // Sometimes we get a "" line and searching for = returns -1. Skip. - if (eqPos !== -1) { - let envVarName: string = line.substring(0, eqPos); - let envVarValue: string = line.substring(eqPos + 1, line.length); - process.env[envVarName] = envVarValue; - } - }); +async function applyEnvironment(content: string | undefined): Promise { + let lines: string[] = content?.split(/\r?\n/) || []; + lines.forEach((line) => { + let eqPos: number = line.search("="); + // Sometimes we get a "" line and searching for = returns -1. Skip. + if (eqPos !== -1) { + let envVarName: string = line.substring(0, eqPos); + let envVarValue: string = line.substring(eqPos + 1, line.length); + process.env[envVarName] = envVarValue; + } + }); } -export async function runPrePostConfigureScript(progress: vscode.Progress<{}>, scriptFile: string, scriptArgs: string[], loggingMessages: { success: string, successWithSomeError: string, failure: string}): Promise { - // Create a temporary wrapper for the user pre-configure script so that we collect - // in another temporary output file the environrment variables that were produced. - // generate a random guid to attach to the `wrapConfigureScript` to ensure we don't have races for the file. - // We split at the first dash to avoid having excessively long filenames. - const shortenedUniqueIdentifier = uuidv4().split("-")[0]; - let wrapScriptFile: string = path.join(util.tmpDir(), `wrapConfigureScript-${shortenedUniqueIdentifier}`); - let wrapScriptOutFile: string = wrapScriptFile + ".out"; - let wrapScriptContent: string; - if (process.platform === "win32") { - wrapScriptContent = `call "${scriptFile}"`; - wrapScriptContent += scriptArgs.length > 0 ? ` ${scriptArgs.join(" ").toString()}\r\n` : "\r\n"; - wrapScriptContent += `set > "${wrapScriptOutFile}"`; - wrapScriptFile += ".bat"; +export async function runPrePostConfigureScript( + progress: vscode.Progress<{}>, + scriptFile: string, + scriptArgs: string[], + loggingMessages: { + success: string; + successWithSomeError: string; + failure: string; + } +): Promise { + // Create a temporary wrapper for the user pre-configure script so that we collect + // in another temporary output file the environrment variables that were produced. + // generate a random guid to attach to the `wrapConfigureScript` to ensure we don't have races for the file. + // We split at the first dash to avoid having excessively long filenames. + const shortenedUniqueIdentifier = uuidv4().split("-")[0]; + let wrapScriptFile: string = path.join( + util.tmpDir(), + `wrapConfigureScript-${shortenedUniqueIdentifier}` + ); + let wrapScriptOutFile: string = wrapScriptFile + ".out"; + let wrapScriptContent: string; + if (process.platform === "win32") { + wrapScriptContent = `call "${scriptFile}"`; + wrapScriptContent += + scriptArgs.length > 0 + ? ` ${scriptArgs.join(" ").toString()}\r\n` + : "\r\n"; + wrapScriptContent += `set > "${wrapScriptOutFile}"`; + wrapScriptFile += ".bat"; + } else { + wrapScriptContent = `source '${scriptFile}' ${ + scriptArgs.length > 0 ? scriptArgs.join(" ").toString() : "" + }\n`; + wrapScriptContent += `printenv > '${wrapScriptOutFile}'`; + wrapScriptFile += ".sh"; + } + + util.writeFile(wrapScriptFile, wrapScriptContent); + + let concreteScriptArgs: string[] = []; + let runCommand: string; + if (process.platform === "win32") { + runCommand = "cmd"; + concreteScriptArgs.push("/c"); + concreteScriptArgs.push(`"${wrapScriptFile}"`); + } else { + runCommand = "/bin/bash"; + concreteScriptArgs.push("-c"); + concreteScriptArgs.push(`"source '${wrapScriptFile}'"`); + } + + try { + let stdout: any = (result: string): void => { + progress.report({ increment: 1, message: "..." }); + logger.messageNoCR(result, "Normal"); + }; + + let someErr: boolean = false; + let stderr: any = (result: string): void => { + someErr = true; + logger.messageNoCR(result, "Normal"); + }; + + // The preconfigure invocation should use the system locale. + const result: util.SpawnProcessResult = await util.spawnChildProcess( + runCommand, + concreteScriptArgs, + util.getWorkspaceRoot(), + false, + false, + stdout, + stderr + ); + if (result.returnCode === ConfigureBuildReturnCodeTypes.success) { + if (someErr) { + // Depending how the preconfigure scripts (and any inner called sub-scripts) are written, + // it may happen that the final error code returned by them to be succesful even if + // previous steps reported errors. + // Until a better error code analysis, simply warn wih a logger message and turn the successful + // return code into ConfigureBuildReurnCodeTypes.other, which would let us know in telemetry + // of this specific situation. + result.returnCode = ConfigureBuildReturnCodeTypes.other; + logger.message(loggingMessages.successWithSomeError); + } else { + logger.message(loggingMessages.success); + } } else { - wrapScriptContent = `source '${scriptFile}' ${scriptArgs.length > 0 ? scriptArgs.join(" ").toString() : ""}\n`; - wrapScriptContent += `printenv > '${wrapScriptOutFile}'`; - wrapScriptFile += ".sh"; + logger.message(loggingMessages.failure); } - util.writeFile(wrapScriptFile, wrapScriptContent); + // Apply the environment produced by running the pre-configure script. + await applyEnvironment(util.readFile(wrapScriptOutFile)); - let concreteScriptArgs: string[] = []; - let runCommand: string; - if (process.platform === 'win32') { - runCommand = "cmd"; - concreteScriptArgs.push("/c"); - concreteScriptArgs.push(`"${wrapScriptFile}"`); - } else { - runCommand = "/bin/bash"; - concreteScriptArgs.push("-c"); - concreteScriptArgs.push(`"source '${wrapScriptFile}'"`); - } - - try { - let stdout: any = (result: string): void => { - progress.report({increment: 1, message: "..."}); - logger.messageNoCR(result, "Normal"); - }; - - let someErr: boolean = false; - let stderr: any = (result: string): void => { - someErr = true; - logger.messageNoCR(result, "Normal"); - }; - - // The preconfigure invocation should use the system locale. - const result: util.SpawnProcessResult = await util.spawnChildProcess(runCommand, concreteScriptArgs, util.getWorkspaceRoot(), false, false, stdout, stderr); - if (result.returnCode === ConfigureBuildReturnCodeTypes.success) { - if (someErr) { - // Depending how the preconfigure scripts (and any inner called sub-scripts) are written, - // it may happen that the final error code returned by them to be succesful even if - // previous steps reported errors. - // Until a better error code analysis, simply warn wih a logger message and turn the successful - // return code into ConfigureBuildReurnCodeTypes.other, which would let us know in telemetry - // of this specific situation. - result.returnCode = ConfigureBuildReturnCodeTypes.other; - logger.message(loggingMessages.successWithSomeError); - } else { - logger.message(loggingMessages.success); - } - } else { - logger.message(loggingMessages.failure); - } - - // Apply the environment produced by running the pre-configure script. - await applyEnvironment(util.readFile(wrapScriptOutFile)); - - return result.returnCode; - } catch (error) { - logger.message(error); - return ConfigureBuildReturnCodeTypes.notFound; - } finally { - util.deleteFileSync(wrapScriptFile); - util.deleteFileSync(wrapScriptOutFile); - } + return result.returnCode; + } catch (error) { + logger.message(error); + return ConfigureBuildReturnCodeTypes.notFound; + } finally { + util.deleteFileSync(wrapScriptFile); + util.deleteFileSync(wrapScriptOutFile); + } } -export async function runPreConfigureScript(progress: vscode.Progress<{}>, scriptFile: string): Promise { - logger.message(`Pre-configuring...\nScript: "${configuration.getPreConfigureScript()}"`); +export async function runPreConfigureScript( + progress: vscode.Progress<{}>, + scriptFile: string +): Promise { + logger.message( + `Pre-configuring...\nScript: "${configuration.getPreConfigureScript()}"` + ); - const currentConfigPreConfigureArgs = configuration.getConfigurationPreConfigureArgs(); - return await runPrePostConfigureScript(progress, scriptFile, currentConfigPreConfigureArgs.length > 0 ? currentConfigPreConfigureArgs : configuration.getPreConfigureArgs(), { - success: "The pre-configure succeeded.", - successWithSomeError: "The pre-configure script returned success code " + - "but somewhere during the preconfigure process there were errors reported. " + - "Double check the preconfigure output in the Makefile Tools channel.", - failure: "The pre-configure script failed. This project may not configure successfully." - }); + const currentConfigPreConfigureArgs = + configuration.getConfigurationPreConfigureArgs(); + return await runPrePostConfigureScript( + progress, + scriptFile, + currentConfigPreConfigureArgs.length > 0 + ? currentConfigPreConfigureArgs + : configuration.getPreConfigureArgs(), + { + success: "The pre-configure succeeded.", + successWithSomeError: + "The pre-configure script returned success code " + + "but somewhere during the preconfigure process there were errors reported. " + + "Double check the preconfigure output in the Makefile Tools channel.", + failure: + "The pre-configure script failed. This project may not configure successfully.", + } + ); } -export async function runPostConfigureScript(progress: vscode.Progress<{}>, scriptFile: string): Promise { - logger.message(`Post-configuring...\nScript: "${configuration.getPostConfigureScript()}"`); +export async function runPostConfigureScript( + progress: vscode.Progress<{}>, + scriptFile: string +): Promise { + logger.message( + `Post-configuring...\nScript: "${configuration.getPostConfigureScript()}"` + ); - const currentConfigPostConfigureArgs = configuration.getConfigurationPostConfigureArgs(); - return await runPrePostConfigureScript(progress, scriptFile, currentConfigPostConfigureArgs.length > 0 ? currentConfigPostConfigureArgs : configuration.getPostConfigureArgs(), { + const currentConfigPostConfigureArgs = + configuration.getConfigurationPostConfigureArgs(); + return await runPrePostConfigureScript( + progress, + scriptFile, + currentConfigPostConfigureArgs.length > 0 + ? currentConfigPostConfigureArgs + : configuration.getPostConfigureArgs(), + { success: "The post-configure succeeded.", successWithSomeError: "The post-configure script returned success code " + @@ -865,39 +1130,48 @@ export async function runPostConfigureScript(progress: vscode.Progress<{}>, scri "Double check the postconfigure output in the Makefile Tools channel.", failure: "The post-configure script failed. This project may not configure successfully.", - }); + } + ); } interface ConfigurationCache { - buildTargets: string[]; - launchTargets: string[]; - customConfigurationProvider: { - workspaceBrowse: cpp.WorkspaceBrowseConfiguration; - fileIndex: [string, { - uri: string | vscode.Uri; - configuration: cpp.SourceFileConfiguration; - compileCommand: parser.CompileCommand; - }][]; - }; + buildTargets: string[]; + launchTargets: string[]; + customConfigurationProvider: { + workspaceBrowse: cpp.WorkspaceBrowseConfiguration; + fileIndex: [ + string, + { + uri: string | vscode.Uri; + configuration: cpp.SourceFileConfiguration; + compileCommand: parser.CompileCommand; + } + ][]; + }; } function isConfigurationEmpty(configurationCache: ConfigurationCache): boolean { - if (configurationCache.buildTargets.length === 0 && configurationCache.launchTargets.length === 0 && configurationCache.customConfigurationProvider.workspaceBrowse.browsePath.length === 0) { - return true; - } - return false; + if ( + configurationCache.buildTargets.length === 0 && + configurationCache.launchTargets.length === 0 && + configurationCache.customConfigurationProvider.workspaceBrowse.browsePath + .length === 0 + ) { + return true; + } + return false; } interface ConfigureSubphasesStatus { - loadFromCache?: ConfigureSubphaseStatus; - generateParseContent?: ConfigureSubphaseStatus; - preprocessParseContent?: ConfigureSubphaseStatus; - parseIntelliSense?: ConfigureSubphaseStatus; - parseLaunch?: ConfigureSubphaseStatus; - dryrunTargets?: ConfigureSubphaseStatus; - parseTargets?: ConfigureSubphaseStatus; + loadFromCache?: ConfigureSubphaseStatus; + generateParseContent?: ConfigureSubphaseStatus; + preprocessParseContent?: ConfigureSubphaseStatus; + parseIntelliSense?: ConfigureSubphaseStatus; + parseLaunch?: ConfigureSubphaseStatus; + dryrunTargets?: ConfigureSubphaseStatus; + parseTargets?: ConfigureSubphaseStatus; - recursiveConfigure?: ConfigureSubphasesStatus; + recursiveConfigure?: ConfigureSubphasesStatus; } // What makes a configure succesful or failed. @@ -914,32 +1188,35 @@ interface ConfigureSubphasesStatus { // Since this analyze helper is never called when configure is cancelled, // it means that the outcome of these 4 subphases does not affect the total return code. function analyzeConfigureSubphases(stats: ConfigureSubphasesStatus): number { - // Generate parse content is a critical phase. Either if it reads from a build log - // or invokes make --dry-run, a not found means there's nothing to parse. - // Same applies for the phase that computes the build targets, which always invokes make. - if (stats.generateParseContent?.retc === ConfigureBuildReturnCodeTypes.notFound || - stats.dryrunTargets?.retc === ConfigureBuildReturnCodeTypes.notFound) { - // But if a configure was successful from cache, return outOfDate and not failure. - return stats.loadFromCache?.retc === ConfigureBuildReturnCodeTypes.success ? - ConfigureBuildReturnCodeTypes.outOfDate : - ConfigureBuildReturnCodeTypes.notFound; - } + // Generate parse content is a critical phase. Either if it reads from a build log + // or invokes make --dry-run, a not found means there's nothing to parse. + // Same applies for the phase that computes the build targets, which always invokes make. + if ( + stats.generateParseContent?.retc === + ConfigureBuildReturnCodeTypes.notFound || + stats.dryrunTargets?.retc === ConfigureBuildReturnCodeTypes.notFound + ) { + // But if a configure was successful from cache, return outOfDate and not failure. + return stats.loadFromCache?.retc === ConfigureBuildReturnCodeTypes.success + ? ConfigureBuildReturnCodeTypes.outOfDate + : ConfigureBuildReturnCodeTypes.notFound; + } - // The outcome of a recursive configure invalidates any other previous returns. - if (stats.recursiveConfigure) { - return analyzeConfigureSubphases(stats.recursiveConfigure); - } + // The outcome of a recursive configure invalidates any other previous returns. + if (stats.recursiveConfigure) { + return analyzeConfigureSubphases(stats.recursiveConfigure); + } - return ConfigureBuildReturnCodeTypes.success; + return ConfigureBuildReturnCodeTypes.success; } interface ConfigureSubphaseStatus { - retc: ConfigureBuildReturnCodeTypes; - elapsed: number; + retc: ConfigureBuildReturnCodeTypes; + elapsed: number; } interface ConfigureSubphaseStatusItem { - name: string; - status: ConfigureSubphaseStatus; + name: string; + status: ConfigureSubphaseStatus; } // Process a list of possible undefined status properties and return an array @@ -947,35 +1224,34 @@ interface ConfigureSubphaseStatusItem { // The caller of "getRelevantConfigStats" sends "stats" of type "ConfigureSubphasesStatus" // but we need to declare it here as "any" to be able to index by prop (a string) below. function getRelevantConfigStats(stats: any): ConfigureSubphaseStatusItem[] { - let relevantStats: ConfigureSubphaseStatusItem[] = []; + let relevantStats: ConfigureSubphaseStatusItem[] = []; - let retCodeProps: string[] = Object.getOwnPropertyNames(stats); - retCodeProps.forEach(prop => { - if (prop.toString() === "recursiveConfigure") { - let recursiveRetCodes: ConfigureSubphaseStatusItem[] = getRelevantConfigStats(stats[prop]); - recursiveRetCodes.forEach(recursiveRetCode => { - relevantStats.push( - { - name: prop.toString() + "." + recursiveRetCode.name, - status: { - retc: recursiveRetCode.status.retc, - elapsed: recursiveRetCode.status.elapsed - } - }); - }); - } else { - relevantStats.push( - { - name: prop.toString(), - status: { - retc: stats[prop].retc, - elapsed: stats[prop].elapsed - } - }); - } - }); + let retCodeProps: string[] = Object.getOwnPropertyNames(stats); + retCodeProps.forEach((prop) => { + if (prop.toString() === "recursiveConfigure") { + let recursiveRetCodes: ConfigureSubphaseStatusItem[] = + getRelevantConfigStats(stats[prop]); + recursiveRetCodes.forEach((recursiveRetCode) => { + relevantStats.push({ + name: prop.toString() + "." + recursiveRetCode.name, + status: { + retc: recursiveRetCode.status.retc, + elapsed: recursiveRetCode.status.elapsed, + }, + }); + }); + } else { + relevantStats.push({ + name: prop.toString(), + status: { + retc: stats[prop].retc, + elapsed: stats[prop].elapsed, + }, + }); + } + }); - return relevantStats; + return relevantStats; } // A non clean configure loads first any pre-existing cache, so that the user @@ -987,34 +1263,50 @@ function getRelevantConfigStats(stats: any): ConfigureSubphaseStatusItem[] { // There is the downside that any files that are removed from the makefile // (thus disappearing from the log with commands) will still have IntelliSense loaded // until the next clean configure. -export async function configure(triggeredBy: TriggeredBy, updateTargets: boolean = true): Promise { +export async function configure( + triggeredBy: TriggeredBy, + updateTargets: boolean = true +): Promise { // Before running the --dry-run type of configure for the first time, even in a trusted workspace, // notify the user that it is possible some code to be executed even under "--dry-run" mode. // Note that this is a state variable and can be reseted via the command resetState, causing the popup to show again // even if a successfull --dry-run configure has been run in the past. - let ranDryRunInCodebaseLifetime: boolean = extension.getState().ranDryRunInCodebaseLifetime; + let ranDryRunInCodebaseLifetime: boolean = + extension.getState().ranDryRunInCodebaseLifetime; if (!ranDryRunInCodebaseLifetime && !configuration.getBuildLog()) { // The window popup message should be concise but more logging can be useful in the output channel. - logger.message("The Makefile Tools extension process of configuring your project is about to run 'make --dry-run' in order to parse the output for useful information. " + - "This is needed to calculate accurate IntelliSense and targets information. " + - "Although in general 'make --dry-run' only lists (without executing) the operations 'make' would do in the current context, " + - "it is still possible some code to be executed, like $(shell) syntax in the makefile or recursive invocations of the $(MAKE) variable."); - logger.message("If you don't feel comfortable allowing this configure process and 'make --dry-run' to be invoked by the extension, " + - "you can chose a recent full, clean, verbose and up-to-date build log as an alternative, via the setting 'makefile.buildLog'. "); + logger.message( + "The Makefile Tools extension process of configuring your project is about to run 'make --dry-run' in order to parse the output for useful information. " + + "This is needed to calculate accurate IntelliSense and targets information. " + + "Although in general 'make --dry-run' only lists (without executing) the operations 'make' would do in the current context, " + + "it is still possible some code to be executed, like $(shell) syntax in the makefile or recursive invocations of the $(MAKE) variable." + ); + logger.message( + "If you don't feel comfortable allowing this configure process and 'make --dry-run' to be invoked by the extension, " + + "you can chose a recent full, clean, verbose and up-to-date build log as an alternative, via the setting 'makefile.buildLog'. " + ); // Also, show the output channel for that message to be visible. logger.showOutputChannel(); - let yesButton: string = localize("yes.dont.show.again", "Yes (don't show again)"); + let yesButton: string = localize( + "yes.dont.show.again", + "Yes (don't show again)" + ); let noButton: string = localize("no", "No"); - const chosen: vscode.MessageItem | undefined = await vscode.window.showErrorMessage(localize("code.can.execute.dryrun.mode.continue", - "Configuring project. Code can still execute in --dry-run mode. Do you want to continue?"), - { - title: yesButton, - isCloseAffordance: false, - }, - { - title: noButton, - isCloseAffordance: true - }); + const chosen: vscode.MessageItem | undefined = + await vscode.window.showErrorMessage( + localize( + "code.can.execute.dryrun.mode.continue", + "Configuring project. Code can still execute in --dry-run mode. Do you want to continue?" + ), + { + title: yesButton, + isCloseAffordance: false, + }, + { + title: noButton, + isCloseAffordance: true, + } + ); // The 'code possibly executed under --dry-run' warning makes sense only for the configure operation. // This is when the user may not expect this to happen. Does not apply for a build or debug/launch and even for pre/post configure which, @@ -1037,7 +1329,8 @@ export async function configure(triggeredBy: TriggeredBy, updateTargets: boolean // Mark that this workspace had at least one attempt at configuring (of any kind: --dry-run or buildLog), before any chance of early return, // to accurately identify in telemetry whether this project configured successfully out of the box or not. - let ranConfigureInCodebaseLifetime: boolean = extension.getState().ranConfigureInCodebaseLifetime; + let ranConfigureInCodebaseLifetime: boolean = + extension.getState().ranConfigureInCodebaseLifetime; extension.getState().ranConfigureInCodebaseLifetime = true; // If `fullFeatureSet` is false and it wasn't a manual command invocation, return and `other` return value. @@ -1138,7 +1431,10 @@ export async function configure(triggeredBy: TriggeredBy, updateTargets: boolean await vscode.window.withProgress( { location: vscode.ProgressLocation.Notification, - title: localize("make.confiuring.cancel.title", "Cancelling configure"), + title: localize( + "make.confiuring.cancel.title", + "Cancelling configure" + ), cancellable: false, }, async (progress) => { @@ -1327,277 +1623,389 @@ export async function configure(triggeredBy: TriggeredBy, updateTargets: boolean } } -async function parseLaunchConfigurations(progress: vscode.Progress<{}>, cancel: vscode.CancellationToken, - dryRunOutput: string, recursive: boolean = false): Promise { +async function parseLaunchConfigurations( + progress: vscode.Progress<{}>, + cancel: vscode.CancellationToken, + dryRunOutput: string, + recursive: boolean = false +): Promise { + if (cancel.isCancellationRequested) { + return { + retc: ConfigureBuildReturnCodeTypes.cancelled, + elapsed: 0, + }; + } - if (cancel.isCancellationRequested) { - return { - retc: ConfigureBuildReturnCodeTypes.cancelled, - elapsed: 0 - }; + let startTime: number = Date.now(); + let launchConfigurations: configuration.LaunchConfiguration[] = []; + + let onStatus: any = (status: string): void => { + progress.report({ + increment: 1, + message: `${status}${recursive ? recursiveString : ""}...`, + }); + }; + + let onFoundLaunchConfiguration: any = ( + launchConfiguration: configuration.LaunchConfiguration + ): void => { + launchConfigurations.push(launchConfiguration); + }; + + let retc: number = await parser.parseLaunchConfigurations( + cancel, + dryRunOutput, + onStatus, + onFoundLaunchConfiguration + ); + if (retc === ConfigureBuildReturnCodeTypes.success) { + let launchConfigurationsStr: string[] = []; + launchConfigurations.forEach((config) => { + launchConfigurationsStr.push( + configuration.launchConfigurationToString(config) + ); + }); + + if (launchConfigurationsStr.length === 0) { + logger.message( + `No${getConfigureIsClean() ? "" : " new"}${ + getConfigureIsClean() ? "" : " new" + } launch configurations have been detected.` + ); + } else { + // Sort and remove duplicates that can be created in the following scenarios: + // - the same target binary invoked several times with the same arguments and from the same path + // - a target binary invoked once with no parameters is still a duplicate + // of the entry generated by the linker command which produced the binary + // - sometimes the same binary is linked more than once in the same location + // (example: instrumentation) but the launch configurations list need only one entry, + // corresponding to the final binary, not the intermediate ones. + launchConfigurationsStr = util.sortAndRemoveDuplicates( + launchConfigurationsStr + ); + + logger.message( + `Found the following ${launchConfigurationsStr.length}${ + getConfigureIsClean() ? "" : " new" + } launch targets defined in the makefile: ${launchConfigurationsStr.join( + ";" + )}` + ); } - let startTime: number = Date.now(); - let launchConfigurations: configuration.LaunchConfiguration[] = []; + if (getConfigureIsClean()) { + // If configure is clean, delete any old launch targets found previously. + configuration.setLaunchTargets(launchConfigurationsStr); + } else { + // If we're merging with a previous set of launch targets, + // remove duplicates because sometimes, depending how the makefiles are set up, + // a non --always-make dry-run may still log commands for up to date files. + // These would be found also in the previous list of launch targets. + configuration.setLaunchTargets( + util.sortAndRemoveDuplicates( + configuration.getLaunchTargets().concat(launchConfigurationsStr) + ) + ); + } - let onStatus: any = (status: string): void => { - progress.report({ increment: 1, message: `${status}${(recursive) ? recursiveString : ""}...` }); + logger.message( + `Complete list of launch targets: ${configuration + .getLaunchTargets() + .join(";")}` + ); + } + + return { + retc, + elapsed: util.elapsedTimeSince(startTime), + }; +} + +async function parseTargets( + progress: vscode.Progress<{}>, + cancel: vscode.CancellationToken, + dryRunOutput: string, + recursive: boolean = false +): Promise { + if (cancel.isCancellationRequested) { + return { + retc: ConfigureBuildReturnCodeTypes.cancelled, + elapsed: 0, }; + } - let onFoundLaunchConfiguration: any = (launchConfiguration: configuration.LaunchConfiguration): void => { - launchConfigurations.push(launchConfiguration); + let startTime: number = Date.now(); + let targets: string[] = []; + + let onStatus: any = (status: string): void => { + progress.report({ + increment: 1, + message: `${status}${recursive ? recursiveString : ""}`, + }); + }; + + let onFoundTarget: any = (target: string): void => { + targets.push(target); + }; + + let retc: number = await parser.parseTargets( + cancel, + dryRunOutput, + onStatus, + onFoundTarget + ); + if (retc === ConfigureBuildReturnCodeTypes.success) { + if (targets.length === 0) { + logger.message( + `No${ + getConfigureIsClean() ? "" : " new" + } build targets have been detected.` + ); + } else { + targets = targets.sort(); + logger.message( + `Found the following ${targets.length}${ + getConfigureIsClean() ? "" : " new" + } build targets defined in the makefile: ${targets.join(";")}` + ); + } + + if (getConfigureIsClean()) { + // If configure is clean, delete any old build targets found previously. + configuration.setBuildTargets(targets); + } else { + // If we're merging with a previous set of build targets, + // remove duplicates because sometimes, depending how the makefiles are set up, + // a non --always-make dry-run may still log commands for up to date files. + // These would be found also in the previous list of build targets. + configuration.setBuildTargets( + util.sortAndRemoveDuplicates( + configuration.getBuildTargets().concat(targets) + ) + ); + } + + logger.message( + `Complete list of build targets: ${configuration + .getBuildTargets() + .join(";")}` + ); + } + + return { + retc, + elapsed: util.elapsedTimeSince(startTime), + }; +} + +async function updateProvider( + progress: vscode.Progress<{}>, + cancel: vscode.CancellationToken, + dryRunOutput: string, + recursive: boolean = false +): Promise { + if (cancel.isCancellationRequested) { + return { + retc: ConfigureBuildReturnCodeTypes.cancelled, + elapsed: 0, }; + } - let retc: number = await parser.parseLaunchConfigurations(cancel, dryRunOutput, onStatus, onFoundLaunchConfiguration); - if (retc === ConfigureBuildReturnCodeTypes.success) { - let launchConfigurationsStr: string[] = []; - launchConfigurations.forEach(config => { - launchConfigurationsStr.push(configuration.launchConfigurationToString(config)); + let startTime: number = Date.now(); + logger.message( + `Updating the CppTools IntelliSense Configuration Provider.${ + recursive ? "(recursive)" : "" + }` + ); + + let onStatus: any = (status: string): void => { + progress.report({ + increment: 1, + message: `${status}${recursive ? recursiveString : ""} ...`, + }); + }; + + let onFoundCustomConfigProviderItem: any = ( + customConfigProviderItem: parser.CustomConfigProviderItem + ): void => { + // Configurations parsed from dryrun output or build log are saved temporarily in the delta file index + extension.buildCustomConfigurationProvider(customConfigProviderItem); + }; + + // Empty the cummulative browse path before we start a new parse for custom configuration. + // We can empty even if the configure is not clean, because the new browse paths will be appended + // to the previous browse paths. + extension.clearCummulativeBrowsePath(); + let retc: number = await parser.parseCustomConfigProvider( + cancel, + dryRunOutput, + onStatus, + onFoundCustomConfigProviderItem + ); + if (retc !== ConfigureBuildReturnCodeTypes.cancelled) { + // If this configure is clean, overwrite the final file index, otherwise merge with it. + let provider: cpptools.CustomConfigurationProvider = + getDeltaCustomConfigurationProvider(); + extension + .getCppConfigurationProvider() + .mergeCustomConfigurationProvider(provider); + + // Empty the 'delta' configurations. + provider.fileIndex.clear(); + provider.workspaceBrowse = { + browsePath: [], + compilerArgs: [], + compilerPath: undefined, + standard: undefined, + windowsSdkVersion: undefined, + }; + setCustomConfigurationProvider(provider); + + extension.updateCppToolsProvider(); + } + + return { + retc, + elapsed: util.elapsedTimeSince(startTime), + }; +} + +export async function preprocessDryRun( + progress: vscode.Progress<{}>, + cancel: vscode.CancellationToken, + dryrunOutput: string, + recursive: boolean = false +): Promise { + if (cancel.isCancellationRequested) { + return { + retc: ConfigureBuildReturnCodeTypes.cancelled, + elapsed: 0, + result: "", + }; + } + + let onStatus: any = (status: string): void => { + progress.report({ + increment: 1, + message: `${status}${recursive ? recursiveString : ""} ...`, + }); + }; + + return parser.preprocessDryRunOutput(cancel, dryrunOutput, onStatus); +} + +export async function loadConfigurationFromCache( + progress: vscode.Progress<{}>, + cancel: vscode.CancellationToken +): Promise { + if (cancel.isCancellationRequested) { + return { + retc: ConfigureBuildReturnCodeTypes.cancelled, + elapsed: 0, + }; + } + + let startTime: number = Date.now(); + let elapsedTime: number; + + await util.scheduleAsyncTask(async () => { + await extension.registerCppToolsProvider(); + }); + let cachePath: string | undefined = configuration.getConfigurationCachePath(); + if (cachePath) { + let content: string | undefined = util.readFile(cachePath); + if (content) { + try { + progress.report({ + increment: 1, + message: localize("make.configure.cache", "Configuring from cache"), + }); + logger.message(`Configuring from cache: ${cachePath}`); + let configurationCache: ConfigurationCache = { + buildTargets: [], + launchTargets: [], + customConfigurationProvider: { + workspaceBrowse: { + browsePath: [], + }, + fileIndex: [], + }, + }; + configurationCache = JSON.parse(content); + + // Trick to get proper URIs after reading from the cache. + // At the moment of writing into the cache, the URIs have + // the vscode.Uri.file(string) format. + // After saving and re-reading, we need the below, + // otherwise CppTools doesn't get anything. + await util.scheduleTask(() => { + configurationCache.customConfigurationProvider.fileIndex.forEach( + (i) => { + i[1].uri = vscode.Uri.file(i[0]); + } + ); }); - if (launchConfigurationsStr.length === 0) { - logger.message(`No${getConfigureIsClean() ? "" : " new"}${getConfigureIsClean() ? "" : " new"} launch configurations have been detected.`); - } else { - // Sort and remove duplicates that can be created in the following scenarios: - // - the same target binary invoked several times with the same arguments and from the same path - // - a target binary invoked once with no parameters is still a duplicate - // of the entry generated by the linker command which produced the binary - // - sometimes the same binary is linked more than once in the same location - // (example: instrumentation) but the launch configurations list need only one entry, - // corresponding to the final binary, not the intermediate ones. - launchConfigurationsStr = util.sortAndRemoveDuplicates(launchConfigurationsStr); + await util.scheduleTask(() => { + configuration.setBuildTargets(configurationCache.buildTargets); + configuration.setLaunchTargets(configurationCache.launchTargets); + }); - logger.message(`Found the following ${launchConfigurationsStr.length}${getConfigureIsClean() ? "" : " new"} launch targets defined in the makefile: ${launchConfigurationsStr.join(";")}`); - } - - if (getConfigureIsClean()) { - // If configure is clean, delete any old launch targets found previously. - configuration.setLaunchTargets(launchConfigurationsStr); - } else { - // If we're merging with a previous set of launch targets, - // remove duplicates because sometimes, depending how the makefiles are set up, - // a non --always-make dry-run may still log commands for up to date files. - // These would be found also in the previous list of launch targets. - configuration.setLaunchTargets(util.sortAndRemoveDuplicates(configuration.getLaunchTargets().concat(launchConfigurationsStr))); - } - - logger.message(`Complete list of launch targets: ${configuration.getLaunchTargets().join(";")}`); - } - - return { - retc, - elapsed: util.elapsedTimeSince(startTime) - }; -} - -async function parseTargets(progress: vscode.Progress<{}>, cancel: vscode.CancellationToken, - dryRunOutput: string, recursive: boolean = false): Promise { - - if (cancel.isCancellationRequested) { - return { - retc: ConfigureBuildReturnCodeTypes.cancelled, - elapsed: 0 - }; - } - - let startTime: number = Date.now(); - let targets: string[] = []; - - let onStatus: any = (status: string): void => { - progress.report({ increment: 1, message: `${status}${(recursive) ? recursiveString : ""}` }); - }; - - let onFoundTarget: any = (target: string): void => { - targets.push(target); - }; - - let retc: number = await parser.parseTargets(cancel, dryRunOutput, onStatus, onFoundTarget); - if (retc === ConfigureBuildReturnCodeTypes.success) { - if (targets.length === 0) { - logger.message(`No${getConfigureIsClean() ? "" : " new"} build targets have been detected.`); - } else { - targets = targets.sort(); - logger.message(`Found the following ${targets.length}${getConfigureIsClean() ? "" : " new"} build targets defined in the makefile: ${targets.join(";")}`); - } - - if (getConfigureIsClean()) { - // If configure is clean, delete any old build targets found previously. - configuration.setBuildTargets(targets); - } else { - // If we're merging with a previous set of build targets, - // remove duplicates because sometimes, depending how the makefiles are set up, - // a non --always-make dry-run may still log commands for up to date files. - // These would be found also in the previous list of build targets. - configuration.setBuildTargets(util.sortAndRemoveDuplicates(configuration.getBuildTargets().concat(targets))); - } - - logger.message(`Complete list of build targets: ${configuration.getBuildTargets().join(";")}`); - } - - return { - retc, - elapsed: util.elapsedTimeSince(startTime) - }; -} - -async function updateProvider(progress: vscode.Progress<{}>, cancel: vscode.CancellationToken, - dryRunOutput: string, recursive: boolean = false): Promise { - if (cancel.isCancellationRequested) { - return { - retc: ConfigureBuildReturnCodeTypes.cancelled, - elapsed: 0 - }; - } - - let startTime: number = Date.now(); - logger.message(`Updating the CppTools IntelliSense Configuration Provider.${(recursive) ? "(recursive)" : ""}`); - - let onStatus: any = (status: string): void => { - progress.report({ increment: 1, message: `${status}${(recursive) ? recursiveString : ""} ...` }); - }; - - let onFoundCustomConfigProviderItem: any = (customConfigProviderItem: parser.CustomConfigProviderItem): void => { - // Configurations parsed from dryrun output or build log are saved temporarily in the delta file index - extension.buildCustomConfigurationProvider(customConfigProviderItem); - }; - - // Empty the cummulative browse path before we start a new parse for custom configuration. - // We can empty even if the configure is not clean, because the new browse paths will be appended - // to the previous browse paths. - extension.clearCummulativeBrowsePath(); - let retc: number = await parser.parseCustomConfigProvider(cancel, dryRunOutput, onStatus, onFoundCustomConfigProviderItem); - if (retc !== ConfigureBuildReturnCodeTypes.cancelled) { - // If this configure is clean, overwrite the final file index, otherwise merge with it. - let provider: cpptools.CustomConfigurationProvider = getDeltaCustomConfigurationProvider(); - extension.getCppConfigurationProvider().mergeCustomConfigurationProvider(provider); - - // Empty the 'delta' configurations. - provider.fileIndex.clear(); - provider.workspaceBrowse = { - browsePath: [], - compilerArgs: [], - compilerPath: undefined, - standard: undefined, - windowsSdkVersion: undefined - }; - setCustomConfigurationProvider(provider); - - extension.updateCppToolsProvider(); - } - - return { - retc, - elapsed: util.elapsedTimeSince(startTime) - }; -} - -export async function preprocessDryRun(progress: vscode.Progress<{}>, cancel: vscode.CancellationToken, - dryrunOutput: string, recursive: boolean = false): Promise { - if (cancel.isCancellationRequested) { - return { - retc: ConfigureBuildReturnCodeTypes.cancelled, - elapsed: 0, - result: "" - }; - } - - let onStatus: any = (status: string): void => { - progress.report({ increment: 1, message: `${status}${(recursive) ? recursiveString : ""} ...` }); - }; - - return parser.preprocessDryRunOutput(cancel, dryrunOutput, onStatus); -} - -export async function loadConfigurationFromCache(progress: vscode.Progress<{}>, cancel: vscode.CancellationToken): Promise { - if (cancel.isCancellationRequested) { - return { - retc: ConfigureBuildReturnCodeTypes.cancelled, - elapsed: 0 - }; - } - - let startTime: number = Date.now(); - let elapsedTime: number; - - await util.scheduleAsyncTask(async () => {await extension.registerCppToolsProvider(); }); - let cachePath: string | undefined = configuration.getConfigurationCachePath(); - if (cachePath) { - let content: string | undefined = util.readFile(cachePath); - if (content) { - try { - progress.report({ increment: 1, message: localize("make.configure.cache", "Configuring from cache") }); - logger.message(`Configuring from cache: ${cachePath}`); - let configurationCache: ConfigurationCache = { - buildTargets: [], - launchTargets: [], - customConfigurationProvider: { - workspaceBrowse: { - browsePath: [] - }, - fileIndex: [] - } - }; - configurationCache = JSON.parse(content); - - // Trick to get proper URIs after reading from the cache. - // At the moment of writing into the cache, the URIs have - // the vscode.Uri.file(string) format. - // After saving and re-reading, we need the below, - // otherwise CppTools doesn't get anything. - await util.scheduleTask(() => { - configurationCache.customConfigurationProvider.fileIndex.forEach(i => { - i[1].uri = vscode.Uri.file(i[0]); - }); - }); - - await util.scheduleTask(() => { - configuration.setBuildTargets(configurationCache.buildTargets); - configuration.setLaunchTargets(configurationCache.launchTargets); - }); - - await util.scheduleTask(() => { - // The configurations saved in the cache are read directly into the final file index. - extension.getCppConfigurationProvider().setCustomConfigurationProvider({ - workspaceBrowse: configurationCache.customConfigurationProvider.workspaceBrowse, - // Trick to read a map from json - fileIndex: new Map(configurationCache.customConfigurationProvider.fileIndex) - }); - }); - - } catch (e) { - logger.message("An error occured while parsing the configuration cache."); - logger.message("Running clean configure instead."); - setConfigureIsInBackground(false); - setConfigureIsClean(true); - } - - elapsedTime = util.elapsedTimeSince(startTime); - logger.message(`Load configuration from cache elapsed time: ${elapsedTime}`); - - // Log all the files read from cache after elapsed time is calculated. - // IntelliSense should be available by now for all files. - // Don't await for this logging step. This may produce some interleaved output - // but it will still be readable. - await util.scheduleTask(() => { - extension.getCppConfigurationProvider().logConfigurationProviderComplete(); + await util.scheduleTask(() => { + // The configurations saved in the cache are read directly into the final file index. + extension + .getCppConfigurationProvider() + .setCustomConfigurationProvider({ + workspaceBrowse: + configurationCache.customConfigurationProvider.workspaceBrowse, + // Trick to read a map from json + fileIndex: new Map( + configurationCache.customConfigurationProvider.fileIndex + ), }); - } else { - return { - retc: ConfigureBuildReturnCodeTypes.notFound, - elapsed: 0 - }; - } - } else { - return { - retc: ConfigureBuildReturnCodeTypes.notFound, - elapsed: 0 - }; - } + }); + } catch (e) { + logger.message( + "An error occured while parsing the configuration cache." + ); + logger.message("Running clean configure instead."); + setConfigureIsInBackground(false); + setConfigureIsClean(true); + } + elapsedTime = util.elapsedTimeSince(startTime); + logger.message( + `Load configuration from cache elapsed time: ${elapsedTime}` + ); + + // Log all the files read from cache after elapsed time is calculated. + // IntelliSense should be available by now for all files. + // Don't await for this logging step. This may produce some interleaved output + // but it will still be readable. + await util.scheduleTask(() => { + extension + .getCppConfigurationProvider() + .logConfigurationProviderComplete(); + }); + } else { + return { + retc: ConfigureBuildReturnCodeTypes.notFound, + elapsed: 0, + }; + } + } else { return { - retc: cancel.isCancellationRequested ? ConfigureBuildReturnCodeTypes.cancelled : ConfigureBuildReturnCodeTypes.success, - elapsed: elapsedTime + retc: ConfigureBuildReturnCodeTypes.notFound, + elapsed: 0, }; + } + + return { + retc: cancel.isCancellationRequested + ? ConfigureBuildReturnCodeTypes.cancelled + : ConfigureBuildReturnCodeTypes.success, + elapsed: elapsedTime, + }; } // Update IntelliSense and launch targets with information parsed from a user given build log, @@ -1605,166 +2013,272 @@ export async function loadConfigurationFromCache(progress: vscode.Progress<{}>, // Sometimes the targets do not need an update (for example, when there has been // a change in the current build target), as requested through the boolean. // This saves unnecessary parsing which may be signifficant for very big code bases. -export async function doConfigure(progress: vscode.Progress<{}>, cancel: vscode.CancellationToken, updateTargets: boolean = true, recursiveDoConfigure: boolean = false): Promise { - let subphaseStats: ConfigureSubphasesStatus = {}; +export async function doConfigure( + progress: vscode.Progress<{}>, + cancel: vscode.CancellationToken, + updateTargets: boolean = true, + recursiveDoConfigure: boolean = false +): Promise { + let subphaseStats: ConfigureSubphasesStatus = {}; - // Configure does not start in the background (we have to load a configuration cache first). - setConfigureIsInBackground(false); + // Configure does not start in the background (we have to load a configuration cache first). + setConfigureIsInBackground(false); - // If available, load all the configure constructs via json from the cache file. - // If this doConfigure is in level 1 of recursion, avoid loading the configuration cache again - // since it's been done at recursion level 0. - // Also skip if there was at least one completed configure before in this VSCode session, - // regardless of any other failure error code, because at the end of that last configure, - // the extension saved this configuration content (that we can skip loading now) into the cache. - // The loading from cache is cheap, but logging it (for Verbose level) may interfere unnecessarily - // with the output channel, especially since that logging is not awaited for. - if (!recursiveDoConfigure && !extension.getCompletedConfigureInSession()) { - subphaseStats.loadFromCache = await loadConfigurationFromCache(progress, cancel); - if (subphaseStats.loadFromCache.retc === ConfigureBuildReturnCodeTypes.cancelled) { - return subphaseStats; - } else if (subphaseStats.loadFromCache.retc === ConfigureBuildReturnCodeTypes.success) { - // In case of success, the following configure steps should not block any other operation - // and can be performed in the background. - setConfigureIsInBackground(true); - } - } else { - logger.message("Loading configurations from cache is not necessary.", "Verbose"); + // If available, load all the configure constructs via json from the cache file. + // If this doConfigure is in level 1 of recursion, avoid loading the configuration cache again + // since it's been done at recursion level 0. + // Also skip if there was at least one completed configure before in this VSCode session, + // regardless of any other failure error code, because at the end of that last configure, + // the extension saved this configuration content (that we can skip loading now) into the cache. + // The loading from cache is cheap, but logging it (for Verbose level) may interfere unnecessarily + // with the output channel, especially since that logging is not awaited for. + if (!recursiveDoConfigure && !extension.getCompletedConfigureInSession()) { + subphaseStats.loadFromCache = await loadConfigurationFromCache( + progress, + cancel + ); + if ( + subphaseStats.loadFromCache.retc === + ConfigureBuildReturnCodeTypes.cancelled + ) { + return subphaseStats; + } else if ( + subphaseStats.loadFromCache.retc === ConfigureBuildReturnCodeTypes.success + ) { + // In case of success, the following configure steps should not block any other operation + // and can be performed in the background. + setConfigureIsInBackground(true); } + } else { + logger.message( + "Loading configurations from cache is not necessary.", + "Verbose" + ); + } - // This generates the dryrun output (saving it on disk) or reads an alternative build log. - // Timings for this sub-phase happen inside. - subphaseStats.generateParseContent = await generateParseContent(progress, cancel, false, recursiveDoConfigure); - if (subphaseStats.generateParseContent.retc === ConfigureBuildReturnCodeTypes.cancelled) { - return subphaseStats; - } - - // Some initial preprocessing required before any parsing is done. - logger.message(`Preprocessing: "${parseFile}"`); - let preprocessedDryrunOutput: string; - let preprocessedDryrunOutputResult: parser.PreprocessDryRunOutputReturnType = await preprocessDryRun(progress, cancel, parseContent || "", recursiveDoConfigure); - subphaseStats.preprocessParseContent = { - retc: preprocessedDryrunOutputResult.retc, - elapsed: preprocessedDryrunOutputResult.retc - }; - if (preprocessedDryrunOutputResult.result) { - preprocessedDryrunOutput = preprocessedDryrunOutputResult.result; - } else { - return subphaseStats; - } - logger.message(`Preprocess elapsed time: ${subphaseStats.preprocessParseContent.elapsed}`); - - // Configure IntelliSense - // Don't override retc1, since make invocations may fail with errors different than cancel - // and we still complete the configure process. - logger.message("Parsing for IntelliSense."); - subphaseStats.parseIntelliSense = await updateProvider(progress, cancel, preprocessedDryrunOutput, recursiveDoConfigure); - if (subphaseStats.parseIntelliSense.retc === ConfigureBuildReturnCodeTypes.cancelled) { - return subphaseStats; - } - logger.message(`Parsing for IntelliSense elapsed time: ${subphaseStats.parseIntelliSense.elapsed}`); - - // Configure launch targets as parsed from the makefile - // (and not as read from settings via makefile.launchConfigurations). - logger.message(`Parsing for launch targets.`); - subphaseStats.parseLaunch = await parseLaunchConfigurations(progress, cancel, preprocessedDryrunOutput, recursiveDoConfigure); - if (subphaseStats.parseLaunch.retc === ConfigureBuildReturnCodeTypes.cancelled) { - return subphaseStats; - } - logger.message(`Parsing for launch targets elapsed time: ${subphaseStats.parseLaunch.elapsed}`); - - // Verify if the current launch configuration is still part of the list and unset otherwise. - // By this point, configuration.getLaunchTargets() contains a complete list (old and new). - let currentLaunchConfiguration: configuration.LaunchConfiguration | undefined = configuration.getCurrentLaunchConfiguration(); - let currentLaunchConfigurationStr: string | undefined = currentLaunchConfiguration ? configuration.launchConfigurationToString(currentLaunchConfiguration) : ""; - if (currentLaunchConfigurationStr !== "" && currentLaunchConfiguration && - !configuration.getLaunchConfigurations().includes(currentLaunchConfiguration)) { - logger.message(`Current launch configuration ${currentLaunchConfigurationStr} is no longer present in the available list.`); - await configuration.setLaunchConfigurationByName(""); - } - - // Configure build targets only if necessary: - // if the caller considers we need a build target update - // or if the build target array hasn't been populated by now - // or if it contains only 'all' which we push automatically. - let buildTargets: string[] = configuration.getBuildTargets(); - if (updateTargets || buildTargets.length === 0 || - (buildTargets.length === 1 && buildTargets[0] === "all")) { - logger.message("Generating parse content for build targets."); - subphaseStats.dryrunTargets = await generateParseContent(progress, cancel, true, recursiveDoConfigure); - if (subphaseStats.dryrunTargets.retc === ConfigureBuildReturnCodeTypes.cancelled) { - return subphaseStats; - } - - logger.message(`Parsing for build targets from: "${parseFile}"`); - subphaseStats.parseTargets = await parseTargets(progress, cancel, parseContent || "", recursiveDoConfigure); - if (subphaseStats.parseTargets.retc === ConfigureBuildReturnCodeTypes.cancelled) { - return subphaseStats; - } - logger.message(`Parsing build targets elapsed time: ${subphaseStats.parseTargets.elapsed}`); - - // Verify if the current build target is still part of the list and unset otherwise. - // By this point, configuration.getBuildTargets() contains a comlete list (old and new). - buildTargets = configuration.getBuildTargets(); - let currentBuildTarget: string | undefined = configuration.getCurrentTarget(); - if (currentBuildTarget && currentBuildTarget !== "" && currentBuildTarget !== "all" && - !buildTargets.includes(currentBuildTarget)) { - logger.message(`Current build target ${currentBuildTarget} is no longer present in the available list.` + - ` Unsetting the current build target.`); - - // Setting a new target by name is not triggering a configure - // (only its caller setBuildTarget, invoked by its command or status bar button). - // But we do need to configure again after a build target change, - // so call doConfigure here and not configure. - // We don't need to alter yet any dirty or pending states, this being an 'inner' call of configure. - // We don't need to consider makefile.configureAfterCommand: even if set to false - // (which would result in changing a build target without a following configure - in the normal scenario) - // now we need to configure because this build target reset was done under the covers - // by the extension and as a result of a configure (which can only be triggered by the user - // if makefile.configureAfterCommand is set to false). - // Calling doConfigure here will not result in extra telemetry (just extra logging). - // The recursive call to doConfigure will fall still under the same progress bar and cancel button - // as the caller and its result will be included into the telemetry information reported by that. - // There can be only one level of recursivity because once the target is reset to empty, - // it is impossible to get into the state of having a target that is not found in the available list. - await configuration.setTargetByName(""); - logger.message("Automatically reconfiguring the project after a build target change."); - recursiveDoConfigure = true; - - // This one level recursive doConfigure will keep the same clean state as the caller - // since setConfigureIsClean runs before the caller configure and resets after - // the eventual recursive configure. - subphaseStats.recursiveConfigure = await doConfigure(progress, cancel, updateTargets, true); - } - } - - // Let the caller collect and log all information regarding the subphases return codes. - if (!recursiveDoConfigure) { - logger.message("Configure finished. The status for all the subphases that ran:"); - let subphases: ConfigureSubphaseStatusItem[] = getRelevantConfigStats(subphaseStats); - subphases.forEach(subphase => { - logger.message(`${subphase.name}: return code = ${subphase.status.retc}, ` + - `elapsed time = ${subphase.status.elapsed}`); - }); - } - - extension.getState().configureDirty = false; + // This generates the dryrun output (saving it on disk) or reads an alternative build log. + // Timings for this sub-phase happen inside. + subphaseStats.generateParseContent = await generateParseContent( + progress, + cancel, + false, + recursiveDoConfigure + ); + if ( + subphaseStats.generateParseContent.retc === + ConfigureBuildReturnCodeTypes.cancelled + ) { return subphaseStats; + } + + // Some initial preprocessing required before any parsing is done. + logger.message(`Preprocessing: "${parseFile}"`); + let preprocessedDryrunOutput: string; + let preprocessedDryrunOutputResult: parser.PreprocessDryRunOutputReturnType = + await preprocessDryRun( + progress, + cancel, + parseContent || "", + recursiveDoConfigure + ); + subphaseStats.preprocessParseContent = { + retc: preprocessedDryrunOutputResult.retc, + elapsed: preprocessedDryrunOutputResult.retc, + }; + if (preprocessedDryrunOutputResult.result) { + preprocessedDryrunOutput = preprocessedDryrunOutputResult.result; + } else { + return subphaseStats; + } + logger.message( + `Preprocess elapsed time: ${subphaseStats.preprocessParseContent.elapsed}` + ); + + // Configure IntelliSense + // Don't override retc1, since make invocations may fail with errors different than cancel + // and we still complete the configure process. + logger.message("Parsing for IntelliSense."); + subphaseStats.parseIntelliSense = await updateProvider( + progress, + cancel, + preprocessedDryrunOutput, + recursiveDoConfigure + ); + if ( + subphaseStats.parseIntelliSense.retc === + ConfigureBuildReturnCodeTypes.cancelled + ) { + return subphaseStats; + } + logger.message( + `Parsing for IntelliSense elapsed time: ${subphaseStats.parseIntelliSense.elapsed}` + ); + + // Configure launch targets as parsed from the makefile + // (and not as read from settings via makefile.launchConfigurations). + logger.message(`Parsing for launch targets.`); + subphaseStats.parseLaunch = await parseLaunchConfigurations( + progress, + cancel, + preprocessedDryrunOutput, + recursiveDoConfigure + ); + if ( + subphaseStats.parseLaunch.retc === ConfigureBuildReturnCodeTypes.cancelled + ) { + return subphaseStats; + } + logger.message( + `Parsing for launch targets elapsed time: ${subphaseStats.parseLaunch.elapsed}` + ); + + // Verify if the current launch configuration is still part of the list and unset otherwise. + // By this point, configuration.getLaunchTargets() contains a complete list (old and new). + let currentLaunchConfiguration: + | configuration.LaunchConfiguration + | undefined = configuration.getCurrentLaunchConfiguration(); + let currentLaunchConfigurationStr: string | undefined = + currentLaunchConfiguration + ? configuration.launchConfigurationToString(currentLaunchConfiguration) + : ""; + if ( + currentLaunchConfigurationStr !== "" && + currentLaunchConfiguration && + !configuration + .getLaunchConfigurations() + .includes(currentLaunchConfiguration) + ) { + logger.message( + `Current launch configuration ${currentLaunchConfigurationStr} is no longer present in the available list.` + ); + await configuration.setLaunchConfigurationByName(""); + } + + // Configure build targets only if necessary: + // if the caller considers we need a build target update + // or if the build target array hasn't been populated by now + // or if it contains only 'all' which we push automatically. + let buildTargets: string[] = configuration.getBuildTargets(); + if ( + updateTargets || + buildTargets.length === 0 || + (buildTargets.length === 1 && buildTargets[0] === "all") + ) { + logger.message("Generating parse content for build targets."); + subphaseStats.dryrunTargets = await generateParseContent( + progress, + cancel, + true, + recursiveDoConfigure + ); + if ( + subphaseStats.dryrunTargets.retc === + ConfigureBuildReturnCodeTypes.cancelled + ) { + return subphaseStats; + } + + logger.message(`Parsing for build targets from: "${parseFile}"`); + subphaseStats.parseTargets = await parseTargets( + progress, + cancel, + parseContent || "", + recursiveDoConfigure + ); + if ( + subphaseStats.parseTargets.retc === + ConfigureBuildReturnCodeTypes.cancelled + ) { + return subphaseStats; + } + logger.message( + `Parsing build targets elapsed time: ${subphaseStats.parseTargets.elapsed}` + ); + + // Verify if the current build target is still part of the list and unset otherwise. + // By this point, configuration.getBuildTargets() contains a comlete list (old and new). + buildTargets = configuration.getBuildTargets(); + let currentBuildTarget: string | undefined = + configuration.getCurrentTarget(); + if ( + currentBuildTarget && + currentBuildTarget !== "" && + currentBuildTarget !== "all" && + !buildTargets.includes(currentBuildTarget) + ) { + logger.message( + `Current build target ${currentBuildTarget} is no longer present in the available list.` + + ` Unsetting the current build target.` + ); + + // Setting a new target by name is not triggering a configure + // (only its caller setBuildTarget, invoked by its command or status bar button). + // But we do need to configure again after a build target change, + // so call doConfigure here and not configure. + // We don't need to alter yet any dirty or pending states, this being an 'inner' call of configure. + // We don't need to consider makefile.configureAfterCommand: even if set to false + // (which would result in changing a build target without a following configure - in the normal scenario) + // now we need to configure because this build target reset was done under the covers + // by the extension and as a result of a configure (which can only be triggered by the user + // if makefile.configureAfterCommand is set to false). + // Calling doConfigure here will not result in extra telemetry (just extra logging). + // The recursive call to doConfigure will fall still under the same progress bar and cancel button + // as the caller and its result will be included into the telemetry information reported by that. + // There can be only one level of recursivity because once the target is reset to empty, + // it is impossible to get into the state of having a target that is not found in the available list. + await configuration.setTargetByName(""); + logger.message( + "Automatically reconfiguring the project after a build target change." + ); + recursiveDoConfigure = true; + + // This one level recursive doConfigure will keep the same clean state as the caller + // since setConfigureIsClean runs before the caller configure and resets after + // the eventual recursive configure. + subphaseStats.recursiveConfigure = await doConfigure( + progress, + cancel, + updateTargets, + true + ); + } + } + + // Let the caller collect and log all information regarding the subphases return codes. + if (!recursiveDoConfigure) { + logger.message( + "Configure finished. The status for all the subphases that ran:" + ); + let subphases: ConfigureSubphaseStatusItem[] = + getRelevantConfigStats(subphaseStats); + subphases.forEach((subphase) => { + logger.message( + `${subphase.name}: return code = ${subphase.status.retc}, ` + + `elapsed time = ${subphase.status.elapsed}` + ); + }); + } + + extension.getState().configureDirty = false; + return subphaseStats; } // A clean configure = a non clean configure + empty the CppTools custom IntelliSense config provider. // In the case of a dry-run setting (not a build log) it also means adding --always-make to the make invocation. // Because we want to first read any existing cache and let the remaining heavy processing run in the background, // we don't delete the cache here. We just mark it to be later deleted by the non clean configure. -export async function cleanConfigure(triggeredBy: TriggeredBy, updateTargets: boolean = true): Promise { - // Even if the core configure process also checks for blocking operations, - // verify the same here as well, to make sure that we don't delete the caches - // only to return early from the core configure. - if (blockedByOp(Operations.configure)) { - return ConfigureBuildReturnCodeTypes.blocked; - } +export async function cleanConfigure( + triggeredBy: TriggeredBy, + updateTargets: boolean = true +): Promise { + // Even if the core configure process also checks for blocking operations, + // verify the same here as well, to make sure that we don't delete the caches + // only to return early from the core configure. + if (blockedByOp(Operations.configure)) { + return ConfigureBuildReturnCodeTypes.blocked; + } - setConfigureIsClean(true); + setConfigureIsClean(true); - return configure(triggeredBy, updateTargets); + return configure(triggeredBy, updateTargets); } diff --git a/src/parser.ts b/src/parser.ts index 84f7513..7a74db5 100644 --- a/src/parser.ts +++ b/src/parser.ts @@ -6,14 +6,14 @@ // (like some exceptions to the 'do not execute' rule // or dependencies on a real build) -import * as configuration from './configuration'; -import * as cpp from 'vscode-cpptools'; -import * as ext from './extension'; -import * as logger from './logger'; -import * as make from './make'; -import * as path from 'path'; -import * as util from './util'; -import * as vscode from 'vscode'; +import * as configuration from "./configuration"; +import * as cpp from "vscode-cpptools"; +import * as ext from "./extension"; +import * as logger from "./logger"; +import * as make from "./make"; +import * as path from "path"; +import * as util from "./util"; +import * as vscode from "vscode"; // List of compiler tools plus the most common aliases cc and c++ // ++ needs to be escaped for the regular expression in parseLineAsTool. @@ -23,240 +23,305 @@ import * as vscode from 'vscode'; // todo: any other scenarios of aliases and symlinks // that would make parseLineAsTool to not match the regular expression, // therefore wrongly skipping over compilation lines? -const compilers: string[] = ["ccache", "clang\\+\\+", "clang-cl", "clang-cpp", "clang", "gcc", "gpp", "cpp", "icc", "cc", "icl", "cl", "g\\+\\+", "c\\+\\+"]; -const linkers: string[] = ["ccache", "ilink", "link", "ld", "ccld", "gcc", "clang\\+\\+", "clang", "cc", "g\\+\\+", "c\\+\\+"]; +const compilers: string[] = [ + "ccache", + "clang\\+\\+", + "clang-cl", + "clang-cpp", + "clang", + "gcc", + "gpp", + "cpp", + "icc", + "cc", + "icl", + "cl", + "g\\+\\+", + "c\\+\\+", +]; +const linkers: string[] = [ + "ccache", + "ilink", + "link", + "ld", + "ccld", + "gcc", + "clang\\+\\+", + "clang", + "cc", + "g\\+\\+", + "c\\+\\+", +]; const sourceFileExtensions: string[] = ["cpp", "cc", "cxx", "c"]; const chunkSize: number = 100; -export async function parseTargets(cancel: vscode.CancellationToken, verboseLog: string, - statusCallback: (message: string) => void, - foundTargetCallback: (target: string) => void): Promise { - if (cancel.isCancellationRequested) { - return make.ConfigureBuildReturnCodeTypes.cancelled; +export async function parseTargets( + cancel: vscode.CancellationToken, + verboseLog: string, + statusCallback: (message: string) => void, + foundTargetCallback: (target: string) => void +): Promise { + if (cancel.isCancellationRequested) { + return make.ConfigureBuildReturnCodeTypes.cancelled; + } + + // Extract the text between "# Files" and "# Finished Make data base" lines + // There can be more than one matching section. + let regexpExtract: RegExp = + /(# Files\n*)([\s\S]*?)(\n# Finished Make data base)/gm; + let result: RegExpExecArray | null; + let extractedLog: string = ""; + + let matches: string[] = []; + let match: string[] | null; + result = await util.scheduleTask(() => regexpExtract.exec(verboseLog)); + + while (result) { + extractedLog = result[2]; + + // Skip lines starting with {#,.} or preceeded by "# Not a target" and extract the target. + // Additionally, if makefile.phonyOnlyTargets is true, include only targets + // succeeded by "# Phony target (prerequisite of .PHONY).". + let regexpTargetStr: string = + "^(?!\\n?[#\\.])(? regexpExtract.exec(verboseLog)); + if (match) { + let done: boolean = false; + let doParsingChunk: () => void = () => { + let chunkIndex: number = 0; - while (result) { - extractedLog = result[2]; + while (match && chunkIndex <= chunkSize) { + // Make sure we don't insert duplicates. + // They can be caused by the makefile syntax of defining variables for a target. + // That creates multiple lines with the same target name followed by :, + // which is the pattern parsed here. + if (!matches.includes(match[1])) { + matches.push(match[1]); + foundTargetCallback(match[1]); + } - // Skip lines starting with {#,.} or preceeded by "# Not a target" and extract the target. - // Additionally, if makefile.phonyOnlyTargets is true, include only targets - // succeeded by "# Phony target (prerequisite of .PHONY).". - let regexpTargetStr: string = "^(?!\\n?[#\\.])(? void) = () => { - let chunkIndex: number = 0; + result = await util.scheduleTask(() => regexpExtract.exec(verboseLog)); + } // while result - while (match && chunkIndex <= chunkSize) { - // Make sure we don't insert duplicates. - // They can be caused by the makefile syntax of defining variables for a target. - // That creates multiple lines with the same target name followed by :, - // which is the pattern parsed here. - if (!matches.includes(match[1])) { - matches.push(match[1]); - foundTargetCallback(match[1]); - } - - statusCallback("Parsing build targets..."); - match = regexpTarget.exec(extractedLog); - - if (!match) { - done = true; - } - - chunkIndex++; - } - }; - while (!done) { - if (cancel.isCancellationRequested) { - return make.ConfigureBuildReturnCodeTypes.cancelled; - } - - await util.scheduleTask(doParsingChunk); - } - } // if match - - result = await util.scheduleTask(() => regexpExtract.exec(verboseLog)); - } // while result - - return cancel.isCancellationRequested ? make.ConfigureBuildReturnCodeTypes.cancelled : make.ConfigureBuildReturnCodeTypes.success; + return cancel.isCancellationRequested + ? make.ConfigureBuildReturnCodeTypes.cancelled + : make.ConfigureBuildReturnCodeTypes.success; } export interface PreprocessDryRunOutputReturnType { - retc: number; - elapsed: number; - result?: string; + retc: number; + elapsed: number; + result?: string; } // Make various preprocessing transformations on the dry-run output // TODO: "cmd -c", "start cmd", "exit" -export async function preprocessDryRunOutput(cancel: vscode.CancellationToken, dryRunOutputStr: string, - statusCallback: (message: string) => void): Promise { - let preprocessedDryRunOutputStr: string = dryRunOutputStr; +export async function preprocessDryRunOutput( + cancel: vscode.CancellationToken, + dryRunOutputStr: string, + statusCallback: (message: string) => void +): Promise { + let preprocessedDryRunOutputStr: string = dryRunOutputStr; - if (cancel.isCancellationRequested) { - return { - retc: make.ConfigureBuildReturnCodeTypes.cancelled, - elapsed: 0 - }; + if (cancel.isCancellationRequested) { + return { + retc: make.ConfigureBuildReturnCodeTypes.cancelled, + elapsed: 0, + }; + } + + let startTime: number = Date.now(); + statusCallback("Preprocessing the dry-run output"); + + // Array of tasks required to be executed during the preprocess configure phase + let preprocessTasks: (() => void)[] = []; + + // Expand {REPO:VSCODE-MAKEFILE-TOOLS} to the full path of the root of the extension + // This is used for the pre-created dry-run logs consumed by the tests, + // in order to be able to have source files and includes for the test repro + // within the test subfolder of the extension repo, while still exercising full paths for parsing + // and not generating a different output with every new location where Makefile Tools is enlisted. + // A real user scenario wouldn't need this construct. + preprocessTasks.push(function (): void { + if (process.env["MAKEFILE_TOOLS_TESTING"] === "1") { + let extensionRootPath: string = path.resolve(__dirname, "../"); + preprocessedDryRunOutputStr = preprocessedDryRunOutputStr.replace( + /{REPO:VSCODE-MAKEFILE-TOOLS}/gm, + extensionRootPath + ); } + }); - let startTime: number = Date.now(); - statusCallback("Preprocessing the dry-run output"); + // Some compiler/linker commands are split on multiple lines. + // At the end of every intermediate line is at least a space, then a \ and end of line. + // Concatenate all these lines to see clearly each command on one line. + let regexp: RegExp = /\s+\\$\n/gm; + preprocessTasks.push(function (): void { + preprocessedDryRunOutputStr = preprocessedDryRunOutputStr.replace( + regexp, + " " + ); + }); - // Array of tasks required to be executed during the preprocess configure phase - let preprocessTasks: (() => void)[] = []; + // In case we parse a build log (as opposed to a dryrun log) for a project using libtool, + // capture the compiler commands reported by the libtool output. + // They may be redundant with the corresponding line from the dryrun (which is present in the build log as well) + // but in case of $ variables and commands invoked on the fly, the second time all are resolved/expanded + // and we can actually send good IntelliSense information for a good source code URL. + // For such a case, look at MONO (git clone https://github.com/mono/mono.git), for source code cordxtra.c + // Line with the original command, containing a 'test' command to determine on the fly the source code path. + // This line is present in the dryrun and also in the build log. Can't easily parse the correct source code path. + // /bin/bash ./libtool --tag=CC --mode=compile gcc -DHAVE_CONFIG_H -I./include -I./include -DGC_PTHREAD_START_STANDALONE -fexceptions -Wall -Wextra -Wpedantic -Wno-long-long -g -O2 -fno-strict-aliasing -MT cord/libcord_la-cordxtra.lo -MD -MP -MF cord/.deps/libcord_la-cordxtra.Tpo -c -o cord/libcord_la-cordxtra.lo `test -f 'cord/cordxtra.c' || echo './'`cord/cordxtra.c + // Line with the resolved command, from which the extension can parse a valid source code path. + // This line is present only in the build log, immediately following the above line. + // libtool: compile: gcc -DHAVE_CONFIG_H -I./include -I./include -DGC_PTHREAD_START_STANDALONE -fexceptions -Wall -Wextra -Wpedantic -Wno-long-long -g -O2 -fno-strict-aliasing -MT cord/libcord_la-cordxtra.lo -MD -MP -MF cord/.deps/libcord_la-cordxtra.Tpo -c cord/cordxtra.c -fPIC -DPIC -o cord/.libs/libcord_la-cordxtra.o + preprocessTasks.push(function (): void { + regexp = /libtool: compile:|libtool: link:/gm; + preprocessedDryRunOutputStr = preprocessedDryRunOutputStr.replace( + regexp, + "\nLIBTOOL_PATTERN\n" + ); + }); - // Expand {REPO:VSCODE-MAKEFILE-TOOLS} to the full path of the root of the extension - // This is used for the pre-created dry-run logs consumed by the tests, - // in order to be able to have source files and includes for the test repro - // within the test subfolder of the extension repo, while still exercising full paths for parsing - // and not generating a different output with every new location where Makefile Tools is enlisted. - // A real user scenario wouldn't need this construct. - preprocessTasks.push(function (): void { - if (process.env['MAKEFILE_TOOLS_TESTING'] === '1') { - let extensionRootPath: string = path.resolve(__dirname, "../"); - preprocessedDryRunOutputStr = preprocessedDryRunOutputStr.replace(/{REPO:VSCODE-MAKEFILE-TOOLS}/mg, extensionRootPath); - } - }); + // Process some more makefile output weirdness + // When --mode=compile or --mode=link are present in a line, we can ignore anything that is before + // and all that is after is a normal complete compiler or link command. + // Replace these patterns with end of line so that the parser will see only the right half. + preprocessTasks.push(function (): void { + regexp = /--mode=compile|--mode=link/gm; + preprocessedDryRunOutputStr = preprocessedDryRunOutputStr.replace( + regexp, + "\nLIBTOOL_PATTERN\n" + ); + }); - // Some compiler/linker commands are split on multiple lines. - // At the end of every intermediate line is at least a space, then a \ and end of line. - // Concatenate all these lines to see clearly each command on one line. - let regexp: RegExp = /\s+\\$\n/mg; - preprocessTasks.push(function (): void { - preprocessedDryRunOutputStr = preprocessedDryRunOutputStr.replace(regexp, " "); - }); + // Remove lines with $() since they come from unexpanded yet variables. The extension can't do anything yet + // about them anyway and also there will be a correspondent line in the dryrun with these variables expanded. + // Don't remove lines with $ without paranthesis, there are valid compilation lines that would be ignored otherwise. + preprocessTasks.push(function (): void { + regexp = /.*\$\(.*/gm; + preprocessedDryRunOutputStr = preprocessedDryRunOutputStr.replace( + regexp, + "" + ); + }); - // In case we parse a build log (as opposed to a dryrun log) for a project using libtool, - // capture the compiler commands reported by the libtool output. - // They may be redundant with the corresponding line from the dryrun (which is present in the build log as well) - // but in case of $ variables and commands invoked on the fly, the second time all are resolved/expanded - // and we can actually send good IntelliSense information for a good source code URL. - // For such a case, look at MONO (git clone https://github.com/mono/mono.git), for source code cordxtra.c - // Line with the original command, containing a 'test' command to determine on the fly the source code path. - // This line is present in the dryrun and also in the build log. Can't easily parse the correct source code path. - // /bin/bash ./libtool --tag=CC --mode=compile gcc -DHAVE_CONFIG_H -I./include -I./include -DGC_PTHREAD_START_STANDALONE -fexceptions -Wall -Wextra -Wpedantic -Wno-long-long -g -O2 -fno-strict-aliasing -MT cord/libcord_la-cordxtra.lo -MD -MP -MF cord/.deps/libcord_la-cordxtra.Tpo -c -o cord/libcord_la-cordxtra.lo `test -f 'cord/cordxtra.c' || echo './'`cord/cordxtra.c - // Line with the resolved command, from which the extension can parse a valid source code path. - // This line is present only in the build log, immediately following the above line. - // libtool: compile: gcc -DHAVE_CONFIG_H -I./include -I./include -DGC_PTHREAD_START_STANDALONE -fexceptions -Wall -Wextra -Wpedantic -Wno-long-long -g -O2 -fno-strict-aliasing -MT cord/libcord_la-cordxtra.lo -MD -MP -MF cord/.deps/libcord_la-cordxtra.Tpo -c cord/cordxtra.c -fPIC -DPIC -o cord/.libs/libcord_la-cordxtra.o - preprocessTasks.push(function (): void { - regexp = /libtool: compile:|libtool: link:/mg; - preprocessedDryRunOutputStr = preprocessedDryRunOutputStr.replace(regexp, "\nLIBTOOL_PATTERN\n"); - }); + // Extract the link command + // Keep the /link switch to the cl command because otherwise we will see compiling without /c + // and we will deduce some other output binary based on its /Fe or /Fo or first source given, + // instead of the output binary defined via the link operation (which will be parsed on the next line). + // TODO: address more accurately the overriding scenarios between output files defined via cl.exe + // and output files defined via cl.exe /link. + // For example, "cl.exe source.cpp /Fetest.exe /link /debug" still produces test.exe + // but cl.exe source.cpp /Fetest.exe /link /out:test2.exe produces only test2.exe. + // For now, ignore any output binary rules of cl while having the /link switch. + preprocessTasks.push(function (): void { + if (process.platform === "win32") { + preprocessedDryRunOutputStr = preprocessedDryRunOutputStr.replace( + / \/link /g, + "/link \n link.exe " + ); + } + }); - // Process some more makefile output weirdness - // When --mode=compile or --mode=link are present in a line, we can ignore anything that is before - // and all that is after is a normal complete compiler or link command. - // Replace these patterns with end of line so that the parser will see only the right half. - preprocessTasks.push(function (): void { - regexp = /--mode=compile|--mode=link/mg; - preprocessedDryRunOutputStr = preprocessedDryRunOutputStr.replace(regexp, "\nLIBTOOL_PATTERN\n"); - }); + // The splitting of multiple commands is better to be done at the end. + // Oherwise, this scenario interferes with the line ending '\' in some cases + // (see MAKE repo, ar.c compiler command, for example). + // Split multiple commands concatenated by '&&' + preprocessTasks.push(function (): void { + preprocessedDryRunOutputStr = preprocessedDryRunOutputStr.replace( + / && /g, + "\n" + ); + }); - // Remove lines with $() since they come from unexpanded yet variables. The extension can't do anything yet - // about them anyway and also there will be a correspondent line in the dryrun with these variables expanded. - // Don't remove lines with $ without paranthesis, there are valid compilation lines that would be ignored otherwise. - preprocessTasks.push(function (): void { - regexp = /.*\$\(.*/mg; - preprocessedDryRunOutputStr = preprocessedDryRunOutputStr.replace(regexp, ""); - }); + // Split multiple commands concatenated by ";" + preprocessTasks.push(function (): void { + preprocessedDryRunOutputStr = preprocessedDryRunOutputStr.replace( + /;/g, + "\n" + ); + }); - // Extract the link command - // Keep the /link switch to the cl command because otherwise we will see compiling without /c - // and we will deduce some other output binary based on its /Fe or /Fo or first source given, - // instead of the output binary defined via the link operation (which will be parsed on the next line). - // TODO: address more accurately the overriding scenarios between output files defined via cl.exe - // and output files defined via cl.exe /link. - // For example, "cl.exe source.cpp /Fetest.exe /link /debug" still produces test.exe - // but cl.exe source.cpp /Fetest.exe /link /out:test2.exe produces only test2.exe. - // For now, ignore any output binary rules of cl while having the /link switch. - preprocessTasks.push(function (): void { - if (process.platform === "win32") { - preprocessedDryRunOutputStr = preprocessedDryRunOutputStr.replace(/ \/link /g, "/link \n link.exe "); - } - }); - - // The splitting of multiple commands is better to be done at the end. - // Oherwise, this scenario interferes with the line ending '\' in some cases - // (see MAKE repo, ar.c compiler command, for example). - // Split multiple commands concatenated by '&&' - preprocessTasks.push(function (): void { - preprocessedDryRunOutputStr = preprocessedDryRunOutputStr.replace(/ && /g, "\n"); - }); - - // Split multiple commands concatenated by ";" - preprocessTasks.push(function (): void { - preprocessedDryRunOutputStr = preprocessedDryRunOutputStr.replace(/;/g, "\n"); - }); - - // Replace multiple "-" sequence because it hangs the regular expression engine. - // Strings with this pattern do not contain useful information to parse, they are safe to replace - // in our internal representation of the dryrun or build log. - // Replace with "- " instead of remove since this pattern does not cause hang or slow processing - // and so that we have a similar view of the preprocessed text. - preprocessTasks.push(function (): void { - regexp = /------/mg; - preprocessedDryRunOutputStr = preprocessedDryRunOutputStr.replace(regexp, "- - - - - - "); + // Replace multiple "-" sequence because it hangs the regular expression engine. + // Strings with this pattern do not contain useful information to parse, they are safe to replace + // in our internal representation of the dryrun or build log. + // Replace with "- " instead of remove since this pattern does not cause hang or slow processing + // and so that we have a similar view of the preprocessed text. + preprocessTasks.push(function (): void { + regexp = /------/gm; + preprocessedDryRunOutputStr = preprocessedDryRunOutputStr.replace( + regexp, + "- - - - - - " + ); }); // Loop through all the configure preprocess tasks, checking for cancel. - for (const func of preprocessTasks) { - await util.scheduleTask(func); + for (const func of preprocessTasks) { + await util.scheduleTask(func); - if (cancel.isCancellationRequested) { - return { - retc: make.ConfigureBuildReturnCodeTypes.cancelled, - elapsed: util.elapsedTimeSince(startTime) - }; - } - } - - return { - retc: make.ConfigureBuildReturnCodeTypes.success, + if (cancel.isCancellationRequested) { + return { + retc: make.ConfigureBuildReturnCodeTypes.cancelled, elapsed: util.elapsedTimeSince(startTime), - result: preprocessedDryRunOutputStr - }; + }; + } + } - // TODO: Insert preprocessed files content + return { + retc: make.ConfigureBuildReturnCodeTypes.success, + elapsed: util.elapsedTimeSince(startTime), + result: preprocessedDryRunOutputStr, + }; - // TODO: Wrappers (example: cl.cmd) + // TODO: Insert preprocessed files content + + // TODO: Wrappers (example: cl.cmd) } interface ToolInvocation { - // how the makefile invokes the tool: - // relative -to the makefile location- path, full path, explicit current directory or no path - // also including the file name, with or without extension - pathInMakefile: string; + // how the makefile invokes the tool: + // relative -to the makefile location- path, full path, explicit current directory or no path + // also including the file name, with or without extension + pathInMakefile: string; - // a full path formed from the given current path and the path in makefile - // plus the file name, with the extension appended (for windows) - fullPath: string; + // a full path formed from the given current path and the path in makefile + // plus the file name, with the extension appended (for windows) + fullPath: string; - // if found at the full path resolved above - found: boolean; + // if found at the full path resolved above + found: boolean; - // the arguments passed to the tool invocation - // define as string so that we deal with the separator properly later, via RegExp - arguments: string; + // the arguments passed to the tool invocation + // define as string so that we deal with the separator properly later, via RegExp + arguments: string; } // Helper that parses the given line as a tool invocation. @@ -270,103 +335,106 @@ interface ToolInvocation { // - quotes only around directory (file name outside quotes) // - path containing "toolName(no extension) " in the middle async function parseLineAsTool( - line: string, - toolNames: string[], - currentPath: string, - isCompilerOrLinker: boolean = true + line: string, + toolNames: string[], + currentPath: string, + isCompilerOrLinker: boolean = true ): Promise { - // To avoid hard-coding (and ever maintaining) in the tools list - // the various compilers/linkers that can have versions, prefixes or suffixes - // in their names, include a crafted regex around each tool name. - // Any number of prefix or suffix text, separated by '-'. - let versionedToolNames: string[] = []; - const prefixRegex: string = isCompilerOrLinker ? "(([a-zA-Z0-9-_.]*-)*" : ""; - const suffixRegex: string = isCompilerOrLinker ? "(-[a-zA-Z0-9-_.]*)*)" : ""; - toolNames.forEach(tool => { - // Check if the user defined this tool as to be excluded - if (!configuration.getExcludeCompilerNames()?.includes(tool)) { - versionedToolNames.push(`${prefixRegex}${tool}${suffixRegex}`); - } + // To avoid hard-coding (and ever maintaining) in the tools list + // the various compilers/linkers that can have versions, prefixes or suffixes + // in their names, include a crafted regex around each tool name. + // Any number of prefix or suffix text, separated by '-'. + let versionedToolNames: string[] = []; + const prefixRegex: string = isCompilerOrLinker ? "(([a-zA-Z0-9-_.]*-)*" : ""; + const suffixRegex: string = isCompilerOrLinker ? "(-[a-zA-Z0-9-_.]*)*)" : ""; + toolNames.forEach((tool) => { + // Check if the user defined this tool as to be excluded + if (!configuration.getExcludeCompilerNames()?.includes(tool)) { + versionedToolNames.push(`${prefixRegex}${tool}${suffixRegex}`); + } + }); + + // Add any additional tools specified by the user + // when we are looking at compilers or linkers, + // not when we parse for binary targets. + if (isCompilerOrLinker) { + configuration.getAdditionalCompilerNames()?.forEach((compiler) => { + if (!toolNames.includes(compiler)) { + versionedToolNames.push(`${prefixRegex}${compiler}${suffixRegex}`); + } }); + } - // Add any additional tools specified by the user - // when we are looking at compilers or linkers, - // not when we parse for binary targets. - if (isCompilerOrLinker) { - configuration.getAdditionalCompilerNames()?.forEach(compiler => { - if (!toolNames.includes(compiler)) { - versionedToolNames.push(`${prefixRegex}${compiler}${suffixRegex}`); - } - }); + // - any spaces/tabs before the tool invocation + // - with or without path (relative -to the makefile location- or full) + // - with or without extension (windows only) + // - with or without quotes + // - must have at least one space or tab after the tool invocation + let regexpStr: string = '^[\\s\\"]*(.*?)('; + if (process.platform === "win32") { + regexpStr += versionedToolNames.join("\\.exe|"); + + // ensure to append the extension for the last tool in the array since join didn't. + if (versionedToolNames.length > 0) { + regexpStr += "\\.exe"; } - // - any spaces/tabs before the tool invocation - // - with or without path (relative -to the makefile location- or full) - // - with or without extension (windows only) - // - with or without quotes - // - must have at least one space or tab after the tool invocation - let regexpStr: string = '^[\\s\\"]*(.*?)('; - if (process.platform === "win32") { - regexpStr += versionedToolNames.join('\\.exe|'); + regexpStr += "|"; + } - // ensure to append the extension for the last tool in the array since join didn't. - if (versionedToolNames.length > 0) { - regexpStr += ('\\.exe'); - } + regexpStr += versionedToolNames.join("|") + ')(\\s|\\"\\s)(.*)$'; - regexpStr += '|'; - } + let regexp: RegExp = RegExp(regexpStr, "mg"); + let match: RegExpExecArray | null = regexp.exec(line); - regexpStr += versionedToolNames.join('|') + ')(\\s|\\"\\s)(.*)$'; + if (!match) { + return undefined; + } - let regexp: RegExp = RegExp(regexpStr, "mg"); - let match: RegExpExecArray | null = regexp.exec(line); + let toolPathInMakefile: string = match[1]; + let toolNameInMakefile: string = match[2]; + if (process.platform === "win32" && !path.extname(toolNameInMakefile)) { + toolNameInMakefile += ".exe"; + } - if (!match) { - return undefined; - } + // Quotes are not needed either for the compiler path or the current path. + // checkFileExists works just fine without quotes, + // but makeFullPath gets confused sometimes for some quotes scenarios. + currentPath = util.removeQuotes(currentPath); + toolPathInMakefile = toolPathInMakefile.trimLeft(); + toolPathInMakefile = util.removeQuotes(toolPathInMakefile); - let toolPathInMakefile: string = match[1]; - let toolNameInMakefile: string = match[2]; - if (process.platform === "win32" && !path.extname(toolNameInMakefile)) { - toolNameInMakefile += ".exe"; - } + let toolFullPath: string = await util.makeFullPath( + toolPathInMakefile + toolNameInMakefile, + currentPath + ); + let toolFound: boolean = util.checkFileExistsSync(toolFullPath); - // Quotes are not needed either for the compiler path or the current path. - // checkFileExists works just fine without quotes, - // but makeFullPath gets confused sometimes for some quotes scenarios. - currentPath = util.removeQuotes(currentPath); - toolPathInMakefile = toolPathInMakefile.trimLeft(); - toolPathInMakefile = util.removeQuotes(toolPathInMakefile); + // Reject a regexp match that doesn't have a real path before the tool invocation, + // like for example link.exe /out:cl.exe being mistakenly parsed as a compiler command. + // Basically, only spaces and/or tabs and/or a valid path are allowed before the compiler name. + // There is no other easy way to eliminate that case via the regexp + // (it must accept a string before the tool). + // For now, we consider a path as valid if it can be found on disk. + // TODO: be able to recognize a string as a valid path even if it doesn't exist on disk, + // in case the project has a setup phase that is copying/installing stuff (like the toolset) + // and it does not have yet a build in place, therefore a path or file is not yet found on disk, + // even if it is valid. + // In other words, we allow the tool to not be found only if the makefile invokes it without any path, + // which opens the possibility of searching the tool through all the paths in the PATH environment variable. + // Note: when searching for execution targets in the makefile, if a binary was not previously built, + // the extension will not detect it for a launch configuration because of this following return. + if (toolPathInMakefile !== "" && !toolFound) { + return undefined; + } - let toolFullPath: string = await util.makeFullPath(toolPathInMakefile + toolNameInMakefile, currentPath); - let toolFound: boolean = util.checkFileExistsSync(toolFullPath); - - // Reject a regexp match that doesn't have a real path before the tool invocation, - // like for example link.exe /out:cl.exe being mistakenly parsed as a compiler command. - // Basically, only spaces and/or tabs and/or a valid path are allowed before the compiler name. - // There is no other easy way to eliminate that case via the regexp - // (it must accept a string before the tool). - // For now, we consider a path as valid if it can be found on disk. - // TODO: be able to recognize a string as a valid path even if it doesn't exist on disk, - // in case the project has a setup phase that is copying/installing stuff (like the toolset) - // and it does not have yet a build in place, therefore a path or file is not yet found on disk, - // even if it is valid. - // In other words, we allow the tool to not be found only if the makefile invokes it without any path, - // which opens the possibility of searching the tool through all the paths in the PATH environment variable. - // Note: when searching for execution targets in the makefile, if a binary was not previously built, - // the extension will not detect it for a launch configuration because of this following return. - if (toolPathInMakefile !== "" && !toolFound) { - return undefined; - } - - return { - // don't use join and neither paths/filenames processed above if we want to keep the exact text in the makefile - pathInMakefile: match[1] + match[2], - fullPath: toolFullPath, - arguments: match[match.length - 1], - found: toolFound - }; + return { + // don't use join and neither paths/filenames processed above if we want to keep the exact text in the makefile + pathInMakefile: match[1] + match[2], + fullPath: toolFullPath, + arguments: match[match.length - 1], + found: toolFound, + }; } // Helper to identify anything that looks like a compiler switch in the given command string. @@ -377,135 +445,161 @@ async function parseLineAsTool( // between two consecutive switches we let the shell parse it into arguments via a script invocation // (instead of us using other parser helpers in this module) to be in sync with how CppTools // expects the compiler arguments to be passed in. -async function parseAnySwitchFromToolArguments(args: string, excludeArgs: string[]): Promise { - // Identify the non value part of the switch: prefix, switch name - // and what may separate this from an eventual switch value - let switches: string[] = []; - let regExpStr: string = "(^|\\s+)(--|-" + - // On Win32 allow '/' as switch prefix as well, - // otherwise it conflicts with path character - (process.platform === "win32" ? "|\\/" : "") + - ")([a-zA-Z0-9_]+)"; - let regexp: RegExp = RegExp(regExpStr, "mg"); - let match1: RegExpExecArray | null; - let match2: RegExpExecArray | null; - let index1: number = -1; - let index2: number = -1; +async function parseAnySwitchFromToolArguments( + args: string, + excludeArgs: string[] +): Promise { + // Identify the non value part of the switch: prefix, switch name + // and what may separate this from an eventual switch value + let switches: string[] = []; + let regExpStr: string = + "(^|\\s+)(--|-" + + // On Win32 allow '/' as switch prefix as well, + // otherwise it conflicts with path character + (process.platform === "win32" ? "|\\/" : "") + + ")([a-zA-Z0-9_]+)"; + let regexp: RegExp = RegExp(regExpStr, "mg"); + let match1: RegExpExecArray | null; + let match2: RegExpExecArray | null; + let index1: number = -1; + let index2: number = -1; - // This contains all the compilation command fragments in between two different consecutive switches - // (except the ones we plan to ignore, specified by excludeArgs). - // Once this function is done concatenating into compilerArgRegions, - // we call the compiler args parsing script once for the whole list of regions - // (as opposed to invoking it for each fragment separately). - let compilerArgRegions: string = ""; + // This contains all the compilation command fragments in between two different consecutive switches + // (except the ones we plan to ignore, specified by excludeArgs). + // Once this function is done concatenating into compilerArgRegions, + // we call the compiler args parsing script once for the whole list of regions + // (as opposed to invoking it for each fragment separately). + let compilerArgRegions: string = ""; - // With every loop iteration we need 2 switch matches so that we analyze the text - // that is between them. If the current match is the last one, then we will analyze - // everything until the end of line. - match1 = regexp.exec(args); + // With every loop iteration we need 2 switch matches so that we analyze the text + // that is between them. If the current match is the last one, then we will analyze + // everything until the end of line. + match1 = regexp.exec(args); - // Even if we don't find any arguments that have a switch syntax, - // consider the whole command line to parse into arguments - // (this case is encountered when we call this helper while we parse launch targets). - if (!match1) { - compilerArgRegions = args; - } + // Even if we don't find any arguments that have a switch syntax, + // consider the whole command line to parse into arguments + // (this case is encountered when we call this helper while we parse launch targets). + if (!match1) { + compilerArgRegions = args; + } - while (match1) { - // Marks the beginning of the current switch (prefix + name). - // The exact switch prefix is needed when we call other parser helpers later - // and also CppTools expects the compiler arguments to be prefixed - // when received from the custom providers. - index1 = regexp.lastIndex - match1[0].length; + while (match1) { + // Marks the beginning of the current switch (prefix + name). + // The exact switch prefix is needed when we call other parser helpers later + // and also CppTools expects the compiler arguments to be prefixed + // when received from the custom providers. + index1 = regexp.lastIndex - match1[0].length; - // Marks the beginning of the next switch - match2 = regexp.exec(args); - if (match2) { - index2 = regexp.lastIndex - match2[0].length; - } else { - index2 = args.length; - } - - // The substring to analyze for the current switch. - // It doesn't help to look beyond the next switch match. - let partialArgs: string = args.substring(index1, index2); - let swi: string = match1[3]; - swi = swi.trim(); - - // Skip over any switches that we know we don't need - let exclude: boolean = false; - for (const arg of excludeArgs) { - if (swi.startsWith(arg)) { - exclude = true; - break; - } - } - - if (!exclude) { - compilerArgRegions += partialArgs; - } - - match1 = match2; - } - - let parseCompilerArgsScriptFile: string = util.parseCompilerArgsScriptFile(); - - if (process.platform === "win32") { - // There is a potential problem with the windows version of the script: - // A fragment like "-sw1,-sw2,-sw3" gets split by comma and a fragment like - // "-SwDef=Val" is split by equal. Opened GitHub issue - // https://github.com/microsoft/vscode-makefile-tools/issues/149. - // These scenarios don't happen on pure windows but can be encountered in classic linux - // scenarios run under MSYS/MINGW. - // Until a better fix is implemented for 149, use a temporary marker that we replace from and into. - compilerArgRegions = compilerArgRegions.replace(/\,/mg, "DONT_USE_COMMA_AS_SEPARATOR"); - compilerArgRegions = compilerArgRegions.replace(/\=/mg, "DONT_USE_EQUAL_AS_SEPARATOR"); - } - - let scriptArgs: string[] = []; - let runCommand: string; - if (process.platform === 'win32') { - runCommand = "cmd"; - scriptArgs.push("/c"); - scriptArgs.push(`""${parseCompilerArgsScriptFile}" ${compilerArgRegions}"`); + // Marks the beginning of the next switch + match2 = regexp.exec(args); + if (match2) { + index2 = regexp.lastIndex - match2[0].length; } else { - runCommand = "/bin/bash"; - scriptArgs.push("-c"); - scriptArgs.push(`"source '${parseCompilerArgsScriptFile}' ${compilerArgRegions}"`); + index2 = args.length; } - try { - let stdout: any = (result: string): void => { - if (process.platform === 'win32') { - // Restore the commas and equals that were hidden from the script invocation. - result = result.replace(/DONT_USE_COMMA_AS_SEPARATOR/mg, ","); - result = result.replace(/DONT_USE_EQUAL_AS_SEPARATOR/mg, "="); - } - let results: string[] = result.replace(/\r\n/mg, "\n").split("\n"); - // In case of concatenated separators, the shell sees different empty arguments - // which we can remove (most common is more spaces not being seen as a single space). - results.forEach(res => { - if (res !== "") { - switches.push(res.trim()); - } - }); - }; + // The substring to analyze for the current switch. + // It doesn't help to look beyond the next switch match. + let partialArgs: string = args.substring(index1, index2); + let swi: string = match1[3]; + swi = swi.trim(); - let stderr: any = (result: string): void => { - logger.message(`Error while running the compiler args parser script '${parseCompilerArgsScriptFile}' ` + - `for regions ("${compilerArgRegions})": "${result}"`, "Normal"); - }; + // Skip over any switches that we know we don't need + let exclude: boolean = false; + for (const arg of excludeArgs) { + if (swi.startsWith(arg)) { + exclude = true; + break; + } + } - // Running the compiler arguments parsing script can use the system locale. - const result: util.SpawnProcessResult = await util.spawnChildProcess(runCommand, scriptArgs, util.getWorkspaceRoot(), false, false, stdout, stderr); - if (result.returnCode !== 0) { - logger.message(`The compiler args parser script '${parseCompilerArgsScriptFile}' failed with error code ${result.returnCode} for regions (${compilerArgRegions})`, "Normal"); + if (!exclude) { + compilerArgRegions += partialArgs; + } + + match1 = match2; + } + + let parseCompilerArgsScriptFile: string = util.parseCompilerArgsScriptFile(); + + if (process.platform === "win32") { + // There is a potential problem with the windows version of the script: + // A fragment like "-sw1,-sw2,-sw3" gets split by comma and a fragment like + // "-SwDef=Val" is split by equal. Opened GitHub issue + // https://github.com/microsoft/vscode-makefile-tools/issues/149. + // These scenarios don't happen on pure windows but can be encountered in classic linux + // scenarios run under MSYS/MINGW. + // Until a better fix is implemented for 149, use a temporary marker that we replace from and into. + compilerArgRegions = compilerArgRegions.replace( + /\,/gm, + "DONT_USE_COMMA_AS_SEPARATOR" + ); + compilerArgRegions = compilerArgRegions.replace( + /\=/gm, + "DONT_USE_EQUAL_AS_SEPARATOR" + ); + } + + let scriptArgs: string[] = []; + let runCommand: string; + if (process.platform === "win32") { + runCommand = "cmd"; + scriptArgs.push("/c"); + scriptArgs.push(`""${parseCompilerArgsScriptFile}" ${compilerArgRegions}"`); + } else { + runCommand = "/bin/bash"; + scriptArgs.push("-c"); + scriptArgs.push( + `"source '${parseCompilerArgsScriptFile}' ${compilerArgRegions}"` + ); + } + + try { + let stdout: any = (result: string): void => { + if (process.platform === "win32") { + // Restore the commas and equals that were hidden from the script invocation. + result = result.replace(/DONT_USE_COMMA_AS_SEPARATOR/gm, ","); + result = result.replace(/DONT_USE_EQUAL_AS_SEPARATOR/gm, "="); + } + let results: string[] = result.replace(/\r\n/gm, "\n").split("\n"); + // In case of concatenated separators, the shell sees different empty arguments + // which we can remove (most common is more spaces not being seen as a single space). + results.forEach((res) => { + if (res !== "") { + switches.push(res.trim()); } - } catch (error) { - logger.message(error); - } + }); + }; - return switches; + let stderr: any = (result: string): void => { + logger.message( + `Error while running the compiler args parser script '${parseCompilerArgsScriptFile}' ` + + `for regions ("${compilerArgRegions})": "${result}"`, + "Normal" + ); + }; + + // Running the compiler arguments parsing script can use the system locale. + const result: util.SpawnProcessResult = await util.spawnChildProcess( + runCommand, + scriptArgs, + util.getWorkspaceRoot(), + false, + false, + stdout, + stderr + ); + if (result.returnCode !== 0) { + logger.message( + `The compiler args parser script '${parseCompilerArgsScriptFile}' failed with error code ${result.returnCode} for regions (${compilerArgRegions})`, + "Normal" + ); + } + } catch (error) { + logger.message(error); + } + + return switches; } // Helper that parses for a particular switch that can occur one or more times @@ -515,101 +609,145 @@ async function parseAnySwitchFromToolArguments(args: string, excludeArgs: string // removeSurroundingQuotes: needs to be false when called from parseAnySwitchFromToolArguments, // and true otherwise. We need to analyze more scenarios before setting in stone a particular algorithm // regarding the decision to remove or not to remove them. -function parseMultipleSwitchFromToolArguments(args: string, sw: string, removeSurroundingQuotes: boolean = true): string[] { - // - '-' or '/' or '--' as switch prefix - // - before each switch, we allow only for one or more spaces/tabs OR begining of line, - // to reject a case where a part of a path looks like a switch with its value - // (example: "drive:/dir/Ifolder" taking /Ifolder as include switch). - // - can be wrapped by a pair of ', before the switch prefix and after the switch value - // (example: '-DMY_DEFINE=SOMETHING' or '/I drive/folder/subfolder'). - // - one or none or more spaces/tabs or ':' or '=' between the switch and the value - // (examples): -Ipath, -I path, -I path, -std=gnu89 - // - the value can be wrapped by a pair of ", ' or `, even simmetrical combinations ('"..."') - // and should be able to not stop at space when inside the quote characters. - // (examples): -D'MY_DEFINE', -D "MY_DEFINE=SOME_VALUE", -I`drive:/folder with space/subfolder` - // - when the switch value contains a '=', the right half can be also quoted by ', ", ` or '"..."' - // and should be able to not stop at space when inside the quote characters. - // (example): -DMY_DEFINE='"SOME_VALUE"' +function parseMultipleSwitchFromToolArguments( + args: string, + sw: string, + removeSurroundingQuotes: boolean = true +): string[] { + // - '-' or '/' or '--' as switch prefix + // - before each switch, we allow only for one or more spaces/tabs OR begining of line, + // to reject a case where a part of a path looks like a switch with its value + // (example: "drive:/dir/Ifolder" taking /Ifolder as include switch). + // - can be wrapped by a pair of ', before the switch prefix and after the switch value + // (example: '-DMY_DEFINE=SOMETHING' or '/I drive/folder/subfolder'). + // - one or none or more spaces/tabs or ':' or '=' between the switch and the value + // (examples): -Ipath, -I path, -I path, -std=gnu89 + // - the value can be wrapped by a pair of ", ' or `, even simmetrical combinations ('"..."') + // and should be able to not stop at space when inside the quote characters. + // (examples): -D'MY_DEFINE', -D "MY_DEFINE=SOME_VALUE", -I`drive:/folder with space/subfolder` + // - when the switch value contains a '=', the right half can be also quoted by ', ", ` or '"..."' + // and should be able to not stop at space when inside the quote characters. + // (example): -DMY_DEFINE='"SOME_VALUE"' - function anythingBetweenQuotes(fullyQuoted: boolean): string { - // The basic pattern for anything between quotes accepts equally single quote, double quote or back tick. - // One pattern that is accepted is to wrap between escaped quotes and allow inside anything (including non-escaped quotes) except escaped quotes. - // Another pattern that is accepted is to wrap between non-escaped quotes and allow inside anything (including escaped quotes) except non-escaped quotes. - // One problem with the "..." pattern is that a simple "\" (or anything ending with \") will not know if the backslash is part of the inside of quote-quote - // or together with the following quote represents a \" and needs to look forward for another ending quote. - // If there is another quote somewhere else later in the command line (another -D or a file name wrapped in quotes) everything until that first upcoming quote - // will be included. - // Example that doesn't work: -DSLASH_DEFINE="\" -DSOME_OTHER_SWITCH "drive:\folder\file.extension" - // SLASH_DEFINE is equal to '\" -DSOME_OTHER_SWITCH ' - // Example that works: -DGIT_VERSION=" \" 1.2.3 \" " - // GIT_VERSION is equal to ' \" 1.2.3 \" ' - // Unfortunately, we also can't identify this to log in the output channel for later analysis of more makefile switch and quoting user scenarios. - // Fortunately, we didn't encounter the last scenario, only the first. - function anythingBetweenQuotesBasicPattern(quoteChar: string): string { - return '\\\\\\' + quoteChar + "((?!\\\\\\" + quoteChar + ").)*\\\\\\" + quoteChar + "|" + // \" anything(except \") \" - "\\" + quoteChar + "(\\\\\\" + quoteChar + "|[^\\" + quoteChar + "])*?[^\\\\\\" + quoteChar + "]?\\" + quoteChar; // " anything (except ") " + function anythingBetweenQuotes(fullyQuoted: boolean): string { + // The basic pattern for anything between quotes accepts equally single quote, double quote or back tick. + // One pattern that is accepted is to wrap between escaped quotes and allow inside anything (including non-escaped quotes) except escaped quotes. + // Another pattern that is accepted is to wrap between non-escaped quotes and allow inside anything (including escaped quotes) except non-escaped quotes. + // One problem with the "..." pattern is that a simple "\" (or anything ending with \") will not know if the backslash is part of the inside of quote-quote + // or together with the following quote represents a \" and needs to look forward for another ending quote. + // If there is another quote somewhere else later in the command line (another -D or a file name wrapped in quotes) everything until that first upcoming quote + // will be included. + // Example that doesn't work: -DSLASH_DEFINE="\" -DSOME_OTHER_SWITCH "drive:\folder\file.extension" + // SLASH_DEFINE is equal to '\" -DSOME_OTHER_SWITCH ' + // Example that works: -DGIT_VERSION=" \" 1.2.3 \" " + // GIT_VERSION is equal to ' \" 1.2.3 \" ' + // Unfortunately, we also can't identify this to log in the output channel for later analysis of more makefile switch and quoting user scenarios. + // Fortunately, we didn't encounter the last scenario, only the first. + function anythingBetweenQuotesBasicPattern(quoteChar: string): string { + return ( + "\\\\\\" + + quoteChar + + "((?!\\\\\\" + + quoteChar + + ").)*\\\\\\" + + quoteChar + + "|" + // \" anything(except \") \" + "\\" + + quoteChar + + "(\\\\\\" + + quoteChar + + "|[^\\" + + quoteChar + + "])*?[^\\\\\\" + + quoteChar + + "]?\\" + + quoteChar + ); // " anything (except ") " + } + + // If the switch is fully quoted with ', like ('-DMY_DEFINE="MyValue"'), don't allow single quotes + // inside the switch value. + // One example of what can be broken if we don't do this: gcc '-DDEF1=' '-DDef2=val2' + // in which case DEF1 would be seen as DEF1=' ' instead of empty = + let str: string = + anythingBetweenQuotesBasicPattern("`") + + "|" + + anythingBetweenQuotesBasicPattern('"') + + (fullyQuoted ? "" : "|" + anythingBetweenQuotesBasicPattern("'")); + return str; + } + + function mainPattern(fullyQuoted: boolean): string { + let pattern: string = + // prefix and switch name + "(" + + "\\/" + + sw + + "(:|=|\\s*)|-" + + sw + + "(:|=|\\s*)|--" + + sw + + "(:|=|\\s*)" + + ")" + + // switch value + "(" + + anythingBetweenQuotes(fullyQuoted) + + "|" + + // not fully quoted switch value scenarios + "(" + + // the left side (or whole value if no '=' is following) + "(" + + "[^\\s=]+" + // not quoted switch value component + ")" + + "(" + + "=" + // separator between switch value left side and right side + "(" + + anythingBetweenQuotes(fullyQuoted) + + "|" + + "[^\\s]+" + // not quoted right side of switch value + // equal is actually allowed (example gcc switch: -fmacro-prefix-map=./= ) + ")?" + // right side of '=' is optional, meaning we can define as nothing, like: -DMyDefine= + ")?" + // = is also optional (simple define) + ")" + + ")"; + + return pattern; + } + + let regexpStr: string = + "(" + + "^|\\s+" + + ")" + // start of line or any amount of space character + "(" + + "(" + + "\\'" + + mainPattern(true) + + "\\'" + + ")" + + "|" + // switch if fully quoted + "(" + + mainPattern(false) + + ")" + // switch if not fully quoted + ")"; + let regexp: RegExp = RegExp(regexpStr, "mg"); + let match: RegExpExecArray | null; + let results: string[] = []; + + match = regexp.exec(args); + while (match) { + let matchIndex: number = + match[2].startsWith("'") && match[2].endsWith("'") ? 8 : 26; + let result: string = match[matchIndex]; + if (result) { + if (removeSurroundingQuotes) { + result = util.removeSurroundingQuotes(result); } - - // If the switch is fully quoted with ', like ('-DMY_DEFINE="MyValue"'), don't allow single quotes - // inside the switch value. - // One example of what can be broken if we don't do this: gcc '-DDEF1=' '-DDef2=val2' - // in which case DEF1 would be seen as DEF1=' ' instead of empty = - let str: string = anythingBetweenQuotesBasicPattern("`") + '|' + anythingBetweenQuotesBasicPattern('"') + (fullyQuoted ? "" : '|' + anythingBetweenQuotesBasicPattern("'")); - return str; + results.push(result); } - - function mainPattern(fullyQuoted: boolean): string { - let pattern: string = - // prefix and switch name - '(' + - '\\/' + sw + '(:|=|\\s*)|-' + sw + '(:|=|\\s*)|--' + sw + '(:|=|\\s*)' + - ')' + - // switch value - '(' + - anythingBetweenQuotes(fullyQuoted) + '|' + - // not fully quoted switch value scenarios - '(' + - // the left side (or whole value if no '=' is following) - '(' + - '[^\\s=]+' + // not quoted switch value component - ')' + - '(' + - '=' + // separator between switch value left side and right side - '(' + - anythingBetweenQuotes(fullyQuoted) + '|' + - '[^\\s]+' + // not quoted right side of switch value - // equal is actually allowed (example gcc switch: -fmacro-prefix-map=./= ) - ')?' + // right side of '=' is optional, meaning we can define as nothing, like: -DMyDefine= - ')?' + // = is also optional (simple define) - ')' + - ')'; - - return pattern; - } - - let regexpStr: string = '(' + '^|\\s+' + ')' + // start of line or any amount of space character - '(' + - '(' + "\\'" + mainPattern(true) + "\\'" + ')' + "|" + // switch if fully quoted - '(' + mainPattern(false) + ')' + // switch if not fully quoted - ')'; - let regexp: RegExp = RegExp(regexpStr, "mg"); - let match: RegExpExecArray | null; - let results: string[] = []; - match = regexp.exec(args); - while (match) { - let matchIndex: number = (match[2].startsWith("'") && match[2].endsWith("'")) ? 8 : 26; - let result: string = match[matchIndex]; - if (result) { - if (removeSurroundingQuotes) { - result = util.removeSurroundingQuotes(result); - } - results.push(result); - } - match = regexp.exec(args); - } + } - return results; + return results; } // Helper that parses for any switch from a set that can occur one or more times @@ -625,42 +763,47 @@ function parseMultipleSwitchFromToolArguments(args: string, sw: string, removeSu // are reflected in the regexp here (especially around quoting scenarios and '='). // For now it's not critical because parseMultipleSwitchesFromToolArguments is called for target // architecture switches which don't have such complex scenarios. -function parseMultipleSwitchesFromToolArguments(args: string, simpleSwitches: string[], valueSwitches: string[]): string[] { - // - '-' or '/' or '--' as switch prefix - // - before each switch, we allow only for one or more spaces/tabs OR begining of line, - // to reject a case where a part of a path looks like a switch with its value - // - can be wrapped by a pair of ', before the switch prefix and after the switch value - // - the value can be wrapped by a pair of " - // - one or none or more spaces/tabs between the switch and the value - let regexpStr: string = '(^|\\s+)\\\'?('; - valueSwitches.forEach(sw => { - regexpStr += '\\/' + sw + '(:|=|\\s*)|-' + sw + '(:|=|\\s*)|--' + sw + '(:|=|\\s*)'; - // Make sure we don't append '|' after the last extension value - if (sw !== valueSwitches[valueSwitches.length - 1]) { - regexpStr += '|'; - } - }); - regexpStr += ')(\\".*?\\"|[^\\\'\\s]+)'; - regexpStr += '|((\\/|-|--)(' + simpleSwitches.join('|') + '))'; - regexpStr += '\\\'?'; - - let regexp: RegExp = RegExp(regexpStr, "mg"); - let match: RegExpExecArray | null; - let results: string[] = []; - - match = regexp.exec(args); - while (match) { - // If the current match is a simple switch, find it at index 15, otherwise at 12. - // In each scenario, only one will have a value while the other is undefined. - let result: string = match[12] || match[15]; - if (result) { - result = result.trim(); - results.push(result); - } - match = regexp.exec(args); +function parseMultipleSwitchesFromToolArguments( + args: string, + simpleSwitches: string[], + valueSwitches: string[] +): string[] { + // - '-' or '/' or '--' as switch prefix + // - before each switch, we allow only for one or more spaces/tabs OR begining of line, + // to reject a case where a part of a path looks like a switch with its value + // - can be wrapped by a pair of ', before the switch prefix and after the switch value + // - the value can be wrapped by a pair of " + // - one or none or more spaces/tabs between the switch and the value + let regexpStr: string = "(^|\\s+)\\'?("; + valueSwitches.forEach((sw) => { + regexpStr += + "\\/" + sw + "(:|=|\\s*)|-" + sw + "(:|=|\\s*)|--" + sw + "(:|=|\\s*)"; + // Make sure we don't append '|' after the last extension value + if (sw !== valueSwitches[valueSwitches.length - 1]) { + regexpStr += "|"; } + }); + regexpStr += ')(\\".*?\\"|[^\\\'\\s]+)'; + regexpStr += "|((\\/|-|--)(" + simpleSwitches.join("|") + "))"; + regexpStr += "\\'?"; - return results; + let regexp: RegExp = RegExp(regexpStr, "mg"); + let match: RegExpExecArray | null; + let results: string[] = []; + + match = regexp.exec(args); + while (match) { + // If the current match is a simple switch, find it at index 15, otherwise at 12. + // In each scenario, only one will have a value while the other is undefined. + let result: string = match[12] || match[15]; + if (result) { + result = result.trim(); + results.push(result); + } + match = regexp.exec(args); + } + + return results; } // Helper that parses for a particular switch that can occur once in the tool command line, @@ -675,29 +818,35 @@ function parseMultipleSwitchesFromToolArguments(args: string, simpleSwitches: st // are reflected in the regexp here (especially around quoting scenarios and '='). // For now it's not critical because parseSingleSwitchFromToolArguments is called for switches // that have simple value scenarios. -function parseSingleSwitchFromToolArguments(args: string, sw: string[]): string | undefined { - // - '-' or '/' or '--' as switch prefix - // - before the switch, we allow only for one or more spaces/tabs OR begining of line, - // to reject a case where a part of a path looks like a switch with its value - // - can be wrapped by a pair of ', before the switch prefix and after the switch value - // - the value can be wrapped by a pair of " - // - ':' or '=' or one/none/more spaces/tabs between the switch and the value - let regexpStr: string = '(^|\\s+)\\\'?(\\/|-|--)(' + sw.join("|") + ')(:|=|\\s*)(\\".*?\\"|[^\\\'\\s]+)\\\'?'; - let regexp: RegExp = RegExp(regexpStr, "mg"); - let match: RegExpExecArray | null; - let results: string[] = []; +function parseSingleSwitchFromToolArguments( + args: string, + sw: string[] +): string | undefined { + // - '-' or '/' or '--' as switch prefix + // - before the switch, we allow only for one or more spaces/tabs OR begining of line, + // to reject a case where a part of a path looks like a switch with its value + // - can be wrapped by a pair of ', before the switch prefix and after the switch value + // - the value can be wrapped by a pair of " + // - ':' or '=' or one/none/more spaces/tabs between the switch and the value + let regexpStr: string = + "(^|\\s+)\\'?(\\/|-|--)(" + + sw.join("|") + + ")(:|=|\\s*)(\\\".*?\\\"|[^\\'\\s]+)\\'?"; + let regexp: RegExp = RegExp(regexpStr, "mg"); + let match: RegExpExecArray | null; + let results: string[] = []; - match = regexp.exec(args); - while (match) { - let result: string = match[5]; - if (result) { - result = result.trim(); - results.push(result); - } - match = regexp.exec(args); + match = regexp.exec(args); + while (match) { + let result: string = match[5]; + if (result) { + result = result.trim(); + results.push(result); } + match = regexp.exec(args); + } - return results.pop(); + return results.pop(); } // Helper that answers whether a particular switch is passed to the tool. @@ -711,16 +860,17 @@ function parseSingleSwitchFromToolArguments(args: string, sw: string[]): string // TODO: detect sets of switches that cancel each other to return a more // accurate result in case of override (example: /TC and /TP) function isSwitchPassedInArguments(args: string, sw: string[]): boolean { - // - '-' or '/' or '--' as switch prefix - // - one or more spaces/tabs after - let regexpStr: string = '((\\s+)|^)(\\/|-|--)(' + sw.join("|") + ')((\\s+)|$)'; - let regexp: RegExp = RegExp(regexpStr, "mg"); + // - '-' or '/' or '--' as switch prefix + // - one or more spaces/tabs after + let regexpStr: string = + "((\\s+)|^)(\\/|-|--)(" + sw.join("|") + ")((\\s+)|$)"; + let regexp: RegExp = RegExp(regexpStr, "mg"); - if (regexp.exec(args)) { - return true; - } + if (regexp.exec(args)) { + return true; + } - return false; + return false; } // Helper that parses for files (of given extensions) that are given as arguments to a tool @@ -728,150 +878,198 @@ function isSwitchPassedInArguments(args: string, sw: string[]): boolean { // Attention to obj, pdb or exe files tied to /Fo, /Fd and /Fe // TODO: consider also ' besides " function parseFilesFromToolArguments(args: string, exts: string[]): string[] { - // no switch prefix and no association yet with a preceding switch - // one or more spaces/tabs before (or beginning of line) and after (or end of line) - // with or without quotes surrounding the argument - // - if surrounding quotes, don't allow another quote in between - // (todo: handle the scenario when quotes enclose just the directory path, without the file name) - let regexpStr: string = '('; - exts.forEach(ext => { - regexpStr += '\\".[^\\"]*?\\.' + ext + '\\"|'; - regexpStr += '\\S+\\.' + ext; - // Make sure we don't append '|' after the last extension value - if (ext !== exts[exts.length - 1]) { - regexpStr += '|'; - } - }); - regexpStr += ')(\\s+|$)'; + // no switch prefix and no association yet with a preceding switch + // one or more spaces/tabs before (or beginning of line) and after (or end of line) + // with or without quotes surrounding the argument + // - if surrounding quotes, don't allow another quote in between + // (todo: handle the scenario when quotes enclose just the directory path, without the file name) + let regexpStr: string = "("; + exts.forEach((ext) => { + regexpStr += '\\".[^\\"]*?\\.' + ext + '\\"|'; + regexpStr += "\\S+\\." + ext; + // Make sure we don't append '|' after the last extension value + if (ext !== exts[exts.length - 1]) { + regexpStr += "|"; + } + }); + regexpStr += ")(\\s+|$)"; - let regexp: RegExp = RegExp(regexpStr, "mg"); - let match: string[] | null; - let files: string[] = []; + let regexp: RegExp = RegExp(regexpStr, "mg"); + let match: string[] | null; + let files: string[] = []; - match = regexp.exec(args); - while (match) { - let result: string = match[1]; + match = regexp.exec(args); + while (match) { + let result: string = match[1]; - // It is quite common to encounter the following pattern: - // `test -f 'sourceFile.c' || echo './'`sourceFile.c - // or `test -f 'sourceFile.c' || echo '../../../libwally-core/src/'`sourceFile.c - // Until we implement the correct approach (to query live the test command) - // we can just ignore it and consider the second option of the OR - // (by removing the quotes while preserving the relative path). - // This is a short term workaround. - let idx: number = args.lastIndexOf(result); - let echo: string = "' || echo "; - let str: string = args.substring(idx - echo.length, idx); - if (str === echo) { - // not to use util.removeQuotes because that also removes double quotes " - result = result.replace(/\'/mg, ""); - result = result.replace(/\`/mg, ""); - } - - if (result) { - result = util.removeSurroundingQuotes(result); - - // Debug message to identify easier the scenarios where source files have inner quotes. - if (result.includes('"')) { - logger.message(`File argument that contains quotes: \`${result}\``, "Debug"); - } - - files.push(result); - } - match = regexp.exec(args); + // It is quite common to encounter the following pattern: + // `test -f 'sourceFile.c' || echo './'`sourceFile.c + // or `test -f 'sourceFile.c' || echo '../../../libwally-core/src/'`sourceFile.c + // Until we implement the correct approach (to query live the test command) + // we can just ignore it and consider the second option of the OR + // (by removing the quotes while preserving the relative path). + // This is a short term workaround. + let idx: number = args.lastIndexOf(result); + let echo: string = "' || echo "; + let str: string = args.substring(idx - echo.length, idx); + if (str === echo) { + // not to use util.removeQuotes because that also removes double quotes " + result = result.replace(/\'/gm, ""); + result = result.replace(/\`/gm, ""); } - return files; + if (result) { + result = util.removeSurroundingQuotes(result); + + // Debug message to identify easier the scenarios where source files have inner quotes. + if (result.includes('"')) { + logger.message( + `File argument that contains quotes: \`${result}\``, + "Debug" + ); + } + + files.push(result); + } + match = regexp.exec(args); + } + + return files; } // Helper that identifies system commands (cd, cd -, pushd, popd) and make.exe change directory switch (-C) // to calculate the effect on the current path, also remembering the transition in the history stack. // The current path is always the last one into the history. -async function currentPathAfterCommand(line: string, currentPathHistory: string[]): Promise { - line = line.trimLeft(); - line = line.trimRight(); +async function currentPathAfterCommand( + line: string, + currentPathHistory: string[] +): Promise { + line = line.trimLeft(); + line = line.trimRight(); - let lastCurrentPath: string = (currentPathHistory.length > 0) ? currentPathHistory[currentPathHistory.length - 1] : ""; - let newCurrentPath: string = ""; + let lastCurrentPath: string = + currentPathHistory.length > 0 + ? currentPathHistory[currentPathHistory.length - 1] + : ""; + let newCurrentPath: string = ""; - if (line.startsWith('cd -') && !configuration.getIgnoreDirectoryCommands()) { - // Swap the last two current paths in the history. - if (lastCurrentPath) { - currentPathHistory.pop(); - } - - let lastCurrentPath2: string = (currentPathHistory.length > 0) ? currentPathHistory.pop() || "" : lastCurrentPath; - - logger.message("Analyzing line: " + line, "Verbose"); - logger.message("CD- command: leaving directory " + lastCurrentPath + " and entering directory " + lastCurrentPath2, "Verbose"); - currentPathHistory.push(lastCurrentPath); - currentPathHistory.push(lastCurrentPath2); - } else if ((line.startsWith('popd') && !configuration.getIgnoreDirectoryCommands()) || - line.includes('Leaving directory')) { - let lastCurrentPath: string = (currentPathHistory.length > 0) ? currentPathHistory[currentPathHistory.length - 1] : ""; - currentPathHistory.pop(); - let lastCurrentPath2: string = (currentPathHistory.length > 0) ? currentPathHistory[currentPathHistory.length - 1] : ""; - logger.message("Analyzing line: " + line, "Verbose"); - logger.message("POPD command or end of MAKE -C: leaving directory " + lastCurrentPath + " and entering directory " + lastCurrentPath2, "Verbose"); - } else if (line.startsWith('cd') && !configuration.getIgnoreDirectoryCommands()) { - newCurrentPath = await util.makeFullPath(line.slice(3), lastCurrentPath); - - // For "cd-" (which toggles between the last 2 current paths), - // we must always keep one previous current path in the history. - // Don't pop if the history has only one path as of now, - // even if this wasn't a pushd. - if (currentPathHistory.length > 1) { - currentPathHistory = []; - currentPathHistory.push(lastCurrentPath); - } - - currentPathHistory.push(newCurrentPath); - logger.message("Analyzing line: " + line, "Verbose"); - logger.message("CD command: entering directory " + newCurrentPath, "Verbose"); - } else if (line.startsWith('pushd') && !configuration.getIgnoreDirectoryCommands()) { - newCurrentPath = await util.makeFullPath(line.slice(6), lastCurrentPath); - currentPathHistory.push(newCurrentPath); - logger.message("Analyzing line: " + line, "Verbose"); - logger.message("PUSHD command: entering directory " + newCurrentPath, "Verbose"); - } else if (line.includes('Entering directory')) { // equivalent to pushd - // The make switch print-directory wraps the folder in various ways. - let match: RegExpMatchArray | null = line.match("(.*)(Entering directory ['`\"])(.*)['`\"]"); - if (match) { - newCurrentPath = await util.makeFullPath(match[3], lastCurrentPath) || ""; - } else { - newCurrentPath = "Could not parse directory"; - } - - logger.message("Analyzing line: " + line, "Verbose"); - logger.message("MAKE -C: entering directory " + newCurrentPath, "Verbose"); - currentPathHistory.push(newCurrentPath); + if (line.startsWith("cd -") && !configuration.getIgnoreDirectoryCommands()) { + // Swap the last two current paths in the history. + if (lastCurrentPath) { + currentPathHistory.pop(); } - return currentPathHistory; + let lastCurrentPath2: string = + currentPathHistory.length > 0 + ? currentPathHistory.pop() || "" + : lastCurrentPath; + + logger.message("Analyzing line: " + line, "Verbose"); + logger.message( + "CD- command: leaving directory " + + lastCurrentPath + + " and entering directory " + + lastCurrentPath2, + "Verbose" + ); + currentPathHistory.push(lastCurrentPath); + currentPathHistory.push(lastCurrentPath2); + } else if ( + (line.startsWith("popd") && !configuration.getIgnoreDirectoryCommands()) || + line.includes("Leaving directory") + ) { + let lastCurrentPath: string = + currentPathHistory.length > 0 + ? currentPathHistory[currentPathHistory.length - 1] + : ""; + currentPathHistory.pop(); + let lastCurrentPath2: string = + currentPathHistory.length > 0 + ? currentPathHistory[currentPathHistory.length - 1] + : ""; + logger.message("Analyzing line: " + line, "Verbose"); + logger.message( + "POPD command or end of MAKE -C: leaving directory " + + lastCurrentPath + + " and entering directory " + + lastCurrentPath2, + "Verbose" + ); + } else if ( + line.startsWith("cd") && + !configuration.getIgnoreDirectoryCommands() + ) { + newCurrentPath = await util.makeFullPath(line.slice(3), lastCurrentPath); + + // For "cd-" (which toggles between the last 2 current paths), + // we must always keep one previous current path in the history. + // Don't pop if the history has only one path as of now, + // even if this wasn't a pushd. + if (currentPathHistory.length > 1) { + currentPathHistory = []; + currentPathHistory.push(lastCurrentPath); + } + + currentPathHistory.push(newCurrentPath); + logger.message("Analyzing line: " + line, "Verbose"); + logger.message( + "CD command: entering directory " + newCurrentPath, + "Verbose" + ); + } else if ( + line.startsWith("pushd") && + !configuration.getIgnoreDirectoryCommands() + ) { + newCurrentPath = await util.makeFullPath(line.slice(6), lastCurrentPath); + currentPathHistory.push(newCurrentPath); + logger.message("Analyzing line: " + line, "Verbose"); + logger.message( + "PUSHD command: entering directory " + newCurrentPath, + "Verbose" + ); + } else if (line.includes("Entering directory")) { + // equivalent to pushd + // The make switch print-directory wraps the folder in various ways. + let match: RegExpMatchArray | null = line.match( + "(.*)(Entering directory ['`\"])(.*)['`\"]" + ); + if (match) { + newCurrentPath = + (await util.makeFullPath(match[3], lastCurrentPath)) || ""; + } else { + newCurrentPath = "Could not parse directory"; + } + + logger.message("Analyzing line: " + line, "Verbose"); + logger.message("MAKE -C: entering directory " + newCurrentPath, "Verbose"); + currentPathHistory.push(newCurrentPath); + } + + return currentPathHistory; } // Structure used to describe a compilation command. Reference documentation is // hosted here https://clang.llvm.org/docs/JSONCompilationDatabase.html export interface CompileCommand { - directory: string; - file: string; - command: string; - arguments?: string[]; - output?: string; + directory: string; + file: string; + command: string; + arguments?: string[]; + output?: string; } export interface CustomConfigProviderItem { - defines: string[]; - includes: string[]; - forcedIncludes: string[]; - standard?: util.StandardVersion; - intelliSenseMode?: util.IntelliSenseMode; - compilerFullPath: string; - compilerArgs: string[]; - files: string[]; - windowsSDKVersion?: string; - currentPath: string; - line: string; + defines: string[]; + includes: string[]; + forcedIncludes: string[]; + standard?: util.StandardVersion; + intelliSenseMode?: util.IntelliSenseMode; + compilerFullPath: string; + compilerArgs: string[]; + files: string[]; + windowsSDKVersion?: string; + currentPath: string; + line: string; } // Parse the output of the make dry-run command in order to provide CppTools @@ -879,688 +1077,921 @@ export interface CustomConfigProviderItem { // as needed by CustomConfigurationProvider. In addition generate a // CompileCommand entry for every file with a compiler invocation to build // a compile_commands.json file. -export async function parseCustomConfigProvider(cancel: vscode.CancellationToken, dryRunOutputStr: string, - statusCallback: (message: string) => void, - onFoundCustomConfigProviderItem: (customConfigProviderItem: CustomConfigProviderItem) => void): Promise { - if (cancel.isCancellationRequested) { - return make.ConfigureBuildReturnCodeTypes.cancelled; - } +export async function parseCustomConfigProvider( + cancel: vscode.CancellationToken, + dryRunOutputStr: string, + statusCallback: (message: string) => void, + onFoundCustomConfigProviderItem: ( + customConfigProviderItem: CustomConfigProviderItem + ) => void +): Promise { + if (cancel.isCancellationRequested) { + return make.ConfigureBuildReturnCodeTypes.cancelled; + } - logger.message('Parsing dry-run output for CppTools Custom Configuration Provider.', "Normal"); + logger.message( + "Parsing dry-run output for CppTools Custom Configuration Provider.", + "Normal" + ); - // Current path starts with workspace root and can be modified - // with prompt commands like cd, cd-, pushd/popd or with -C make switch - let currentPath: string = util.getWorkspaceRoot(); - let currentPathHistory: string[] = [currentPath]; + // Current path starts with workspace root and can be modified + // with prompt commands like cd, cd-, pushd/popd or with -C make switch + let currentPath: string = util.getWorkspaceRoot(); + let currentPathHistory: string[] = [currentPath]; - // Read the dry-run output line by line, searching for compilers and directory changing commands - // to construct information for the CppTools custom configuration - let dryRunOutputLines: string[] = dryRunOutputStr.split("\n"); - let numberOfLines: number = dryRunOutputLines.length; - let index: number = 0; - let done: boolean = false; + // Read the dry-run output line by line, searching for compilers and directory changing commands + // to construct information for the CppTools custom configuration + let dryRunOutputLines: string[] = dryRunOutputStr.split("\n"); + let numberOfLines: number = dryRunOutputLines.length; + let index: number = 0; + let done: boolean = false; - async function doParsingChunk(): Promise { - let chunkIndex: number = 0; - while (index < numberOfLines && chunkIndex <= chunkSize) { - if (cancel.isCancellationRequested) { - break; - } + async function doParsingChunk(): Promise { + let chunkIndex: number = 0; + while (index < numberOfLines && chunkIndex <= chunkSize) { + if (cancel.isCancellationRequested) { + break; + } - let line: string = dryRunOutputLines[index]; + let line: string = dryRunOutputLines[index]; - statusCallback("Parsing for IntelliSense"); - currentPathHistory = await currentPathAfterCommand(line, currentPathHistory); - currentPath = currentPathHistory[currentPathHistory.length - 1]; + statusCallback("Parsing for IntelliSense"); + currentPathHistory = await currentPathAfterCommand( + line, + currentPathHistory + ); + currentPath = currentPathHistory[currentPathHistory.length - 1]; - let compilerTool: ToolInvocation | undefined = await parseLineAsTool(line, compilers, currentPath); + let compilerTool: ToolInvocation | undefined = await parseLineAsTool( + line, + compilers, + currentPath + ); - // If ccache wraps the compiler, parse again the remaining command line and we should obtain - // the real compiler name. - if (compilerTool && path.parse(compilerTool.pathInMakefile).name.endsWith("ccache")) { - line = line.replace(`${compilerTool.pathInMakefile}`, ""); - compilerTool = await parseLineAsTool(line, compilers, currentPath); - } + // If ccache wraps the compiler, parse again the remaining command line and we should obtain + // the real compiler name. + if ( + compilerTool && + path.parse(compilerTool.pathInMakefile).name.endsWith("ccache") + ) { + line = line.replace(`${compilerTool.pathInMakefile}`, ""); + compilerTool = await parseLineAsTool(line, compilers, currentPath); + } - if (compilerTool) { - logger.message("Found compiler command: " + line, "Verbose"); + if (compilerTool) { + logger.message("Found compiler command: " + line, "Verbose"); - // Compiler path is either what the makefile provides or found in the PATH environment variable or empty - let compilerFullPath: string = compilerTool.fullPath || ""; - if (!compilerTool.found) { - let toolBaseName: string = path.basename(compilerFullPath); - compilerFullPath = path.join(util.toolPathInEnv(toolBaseName) || "", toolBaseName); - } - - // Exclude switches that are being processed separately (I, FI, include, D, std) - // and switches that don't affect IntelliSense but are causing errors. - let compilerArgs: string[] = []; - compilerArgs = await parseAnySwitchFromToolArguments(compilerTool.arguments, ["I", "FI", "include", "D", "std", "MF"]); - - // Parse and log the includes, forced includes and the defines - let includes: string[] = parseMultipleSwitchFromToolArguments(compilerTool.arguments, 'I'); - includes = await util.makeFullPaths(includes, currentPath); - let forcedIncludes: string[] = parseMultipleSwitchFromToolArguments(compilerTool.arguments, 'FI'); - forcedIncludes = forcedIncludes.concat(parseMultipleSwitchFromToolArguments(compilerTool.arguments, 'include')); - forcedIncludes = await util.makeFullPaths(forcedIncludes, currentPath); - - let defines: string[] = parseMultipleSwitchFromToolArguments(compilerTool.arguments, 'D'); - - // Parse the IntelliSense mode - // how to deal with aliases and symlinks (CC, C++), which can point to any toolsets - let targetArchitecture: util.TargetArchitecture = getTargetArchitecture(compilerTool.arguments); - let intelliSenseMode: util.IntelliSenseMode = getIntelliSenseMode(ext.extension.getCppToolsVersion(), compilerFullPath, targetArchitecture); - - // For windows, parse the sdk version - let windowsSDKVersion: string | undefined = ""; - if (process.platform === "win32") { - windowsSDKVersion = process.env["WindowsSDKVersion"]; - } - - // Parse the source files - let files: string[] = parseFilesFromToolArguments(compilerTool.arguments, sourceFileExtensions); - files = await util.makeFullPaths(files, currentPath); - - // The language represented by this compilation command - let language: util.Language; - let hasC: boolean = files.filter(file => (file.endsWith(".c"))).length > 0; - let hasCpp: boolean = files.filter(file => (file.endsWith(".cpp"))).length > 0; - if (hasC && !hasCpp) { - language = "c"; - } else if (hasCpp && !hasC) { - language = "cpp"; - } - - // /TP and /TC (for cl.exe only) overwrite the meaning of the source files extensions - if (isSwitchPassedInArguments(compilerTool.arguments, ['TP'])) { - language = "cpp"; - } else if (isSwitchPassedInArguments(compilerTool.arguments, ['TC'])) { - language = "c"; - } - - // Parse the C/C++ standard as given in the compiler command line - let standardStr: string | undefined = parseSingleSwitchFromToolArguments(compilerTool.arguments, ["std"]); - - // If the command is compiling the same extension or uses -TC/-TP, send all the source files in one batch. - if (language) { - // More standard validation and defaults, in the context of the whole command. - let standard: util.StandardVersion | undefined = parseStandard(ext.extension.getCppToolsVersion(), standardStr, language); - - if (ext.extension) { - onFoundCustomConfigProviderItem({ defines, includes, forcedIncludes, standard, intelliSenseMode, compilerFullPath, compilerArgs, files, windowsSDKVersion, currentPath, line }); - } - } else { - // If the compiler command is mixing c and c++ source files, send a custom configuration for each of the source files separately, - // to be able to accurately validate and calculate the standard based on the correct language. - files.forEach(file => { - if (file.endsWith(".cpp")) { - language = "cpp"; - } else if (file.endsWith(".c")) { - language = "c"; - } - - // More standard validation and defaults, in the context of each source file. - let standard: util.StandardVersion | undefined = parseStandard(ext.extension.getCppToolsVersion(), standardStr, language); - - if (ext.extension) { - onFoundCustomConfigProviderItem({ defines, includes, forcedIncludes, standard, intelliSenseMode, compilerFullPath, compilerArgs, files: [file], windowsSDKVersion, currentPath, line }); - } - }); - } - } // if (compilerTool) { - - index++; - if (index === numberOfLines) { - done = true; - } - - chunkIndex++; - } // while loop - } // doParsingChunk function - - while (!done) { - if (cancel.isCancellationRequested) { - break; + // Compiler path is either what the makefile provides or found in the PATH environment variable or empty + let compilerFullPath: string = compilerTool.fullPath || ""; + if (!compilerTool.found) { + let toolBaseName: string = path.basename(compilerFullPath); + compilerFullPath = path.join( + util.toolPathInEnv(toolBaseName) || "", + toolBaseName + ); } - await util.scheduleAsyncTask(doParsingChunk); + // Exclude switches that are being processed separately (I, FI, include, D, std) + // and switches that don't affect IntelliSense but are causing errors. + let compilerArgs: string[] = []; + compilerArgs = await parseAnySwitchFromToolArguments( + compilerTool.arguments, + ["I", "FI", "include", "D", "std", "MF"] + ); + + // Parse and log the includes, forced includes and the defines + let includes: string[] = parseMultipleSwitchFromToolArguments( + compilerTool.arguments, + "I" + ); + includes = await util.makeFullPaths(includes, currentPath); + let forcedIncludes: string[] = parseMultipleSwitchFromToolArguments( + compilerTool.arguments, + "FI" + ); + forcedIncludes = forcedIncludes.concat( + parseMultipleSwitchFromToolArguments( + compilerTool.arguments, + "include" + ) + ); + forcedIncludes = await util.makeFullPaths(forcedIncludes, currentPath); + + let defines: string[] = parseMultipleSwitchFromToolArguments( + compilerTool.arguments, + "D" + ); + + // Parse the IntelliSense mode + // how to deal with aliases and symlinks (CC, C++), which can point to any toolsets + let targetArchitecture: util.TargetArchitecture = getTargetArchitecture( + compilerTool.arguments + ); + let intelliSenseMode: util.IntelliSenseMode = getIntelliSenseMode( + ext.extension.getCppToolsVersion(), + compilerFullPath, + targetArchitecture + ); + + // For windows, parse the sdk version + let windowsSDKVersion: string | undefined = ""; + if (process.platform === "win32") { + windowsSDKVersion = process.env["WindowsSDKVersion"]; + } + + // Parse the source files + let files: string[] = parseFilesFromToolArguments( + compilerTool.arguments, + sourceFileExtensions + ); + files = await util.makeFullPaths(files, currentPath); + + // The language represented by this compilation command + let language: util.Language; + let hasC: boolean = + files.filter((file) => file.endsWith(".c")).length > 0; + let hasCpp: boolean = + files.filter((file) => file.endsWith(".cpp")).length > 0; + if (hasC && !hasCpp) { + language = "c"; + } else if (hasCpp && !hasC) { + language = "cpp"; + } + + // /TP and /TC (for cl.exe only) overwrite the meaning of the source files extensions + if (isSwitchPassedInArguments(compilerTool.arguments, ["TP"])) { + language = "cpp"; + } else if (isSwitchPassedInArguments(compilerTool.arguments, ["TC"])) { + language = "c"; + } + + // Parse the C/C++ standard as given in the compiler command line + let standardStr: string | undefined = + parseSingleSwitchFromToolArguments(compilerTool.arguments, ["std"]); + + // If the command is compiling the same extension or uses -TC/-TP, send all the source files in one batch. + if (language) { + // More standard validation and defaults, in the context of the whole command. + let standard: util.StandardVersion | undefined = parseStandard( + ext.extension.getCppToolsVersion(), + standardStr, + language + ); + + if (ext.extension) { + onFoundCustomConfigProviderItem({ + defines, + includes, + forcedIncludes, + standard, + intelliSenseMode, + compilerFullPath, + compilerArgs, + files, + windowsSDKVersion, + currentPath, + line, + }); + } + } else { + // If the compiler command is mixing c and c++ source files, send a custom configuration for each of the source files separately, + // to be able to accurately validate and calculate the standard based on the correct language. + files.forEach((file) => { + if (file.endsWith(".cpp")) { + language = "cpp"; + } else if (file.endsWith(".c")) { + language = "c"; + } + + // More standard validation and defaults, in the context of each source file. + let standard: util.StandardVersion | undefined = parseStandard( + ext.extension.getCppToolsVersion(), + standardStr, + language + ); + + if (ext.extension) { + onFoundCustomConfigProviderItem({ + defines, + includes, + forcedIncludes, + standard, + intelliSenseMode, + compilerFullPath, + compilerArgs, + files: [file], + windowsSDKVersion, + currentPath, + line, + }); + } + }); + } + } // if (compilerTool) { + + index++; + if (index === numberOfLines) { + done = true; + } + + chunkIndex++; + } // while loop + } // doParsingChunk function + + while (!done) { + if (cancel.isCancellationRequested) { + break; } - return cancel.isCancellationRequested ? make.ConfigureBuildReturnCodeTypes.cancelled : make.ConfigureBuildReturnCodeTypes.success; + await util.scheduleAsyncTask(doParsingChunk); + } + + return cancel.isCancellationRequested + ? make.ConfigureBuildReturnCodeTypes.cancelled + : make.ConfigureBuildReturnCodeTypes.success; } // Target binaries arguments special handling function filterTargetBinaryArgs(args: string[]): string[] { - let processedArgs: string[] = []; + let processedArgs: string[] = []; - for (const arg of args) { - // Once we encounter a redirection character (pipe, stdout/stderr) remove it, - // together with all the arguments that are following, - // since they are not real parameters of the binary tool that is analyzed. - if (arg === '>' || arg === '1>' || arg === '2>' || arg === '|') { - break; - } - - processedArgs.push(arg); + for (const arg of args) { + // Once we encounter a redirection character (pipe, stdout/stderr) remove it, + // together with all the arguments that are following, + // since they are not real parameters of the binary tool that is analyzed. + if (arg === ">" || arg === "1>" || arg === "2>" || arg === "|") { + break; } - return processedArgs; + processedArgs.push(arg); + } + + return processedArgs; } // Parse the output of the make dry-run command in order to provide VS Code debugger // with information about binaries, their execution paths and arguments -export async function parseLaunchConfigurations(cancel: vscode.CancellationToken, dryRunOutputStr: string, - statusCallback: (message: string) => void, - onFoundLaunchConfiguration: (launchConfiguration: configuration.LaunchConfiguration) => void): Promise { - if (cancel.isCancellationRequested) { - return make.ConfigureBuildReturnCodeTypes.cancelled; - } +export async function parseLaunchConfigurations( + cancel: vscode.CancellationToken, + dryRunOutputStr: string, + statusCallback: (message: string) => void, + onFoundLaunchConfiguration: ( + launchConfiguration: configuration.LaunchConfiguration + ) => void +): Promise { + if (cancel.isCancellationRequested) { + return make.ConfigureBuildReturnCodeTypes.cancelled; + } - // Current path starts with workspace root and can be modified - // with prompt commands like cd, cd-, pushd/popd or with -C make switch - let currentPath: string = util.getWorkspaceRoot(); - let currentPathHistory: string[] = [currentPath]; + // Current path starts with workspace root and can be modified + // with prompt commands like cd, cd-, pushd/popd or with -C make switch + let currentPath: string = util.getWorkspaceRoot(); + let currentPathHistory: string[] = [currentPath]; - // array of full path executables built by this makefile - let targetBinaries: string[] = []; + // array of full path executables built by this makefile + let targetBinaries: string[] = []; - // The first pass of reading the dry-run output, line by line - // searching for compilers, linkers and directory changing commands - // to construct information for the launch configuration - let dryRunOutputLines: string[] = dryRunOutputStr.split("\n"); - let numberOfLines: number = dryRunOutputLines.length; - let index: number = 0; - let done: boolean = false; - let doLinkCommandsParsingChunk: (() => Promise) = async () => { - let chunkIndex: number = 0; - while (index < numberOfLines && chunkIndex <= chunkSize) { - if (cancel.isCancellationRequested) { - break; - } + // The first pass of reading the dry-run output, line by line + // searching for compilers, linkers and directory changing commands + // to construct information for the launch configuration + let dryRunOutputLines: string[] = dryRunOutputStr.split("\n"); + let numberOfLines: number = dryRunOutputLines.length; + let index: number = 0; + let done: boolean = false; + let doLinkCommandsParsingChunk: () => Promise = async () => { + let chunkIndex: number = 0; + while (index < numberOfLines && chunkIndex <= chunkSize) { + if (cancel.isCancellationRequested) { + break; + } - let line: string = dryRunOutputLines[index]; + let line: string = dryRunOutputLines[index]; - statusCallback("Parsing for launch targets: inspecting for link commands"); - currentPathHistory = await currentPathAfterCommand(line, currentPathHistory); - currentPath = currentPathHistory[currentPathHistory.length - 1]; + statusCallback( + "Parsing for launch targets: inspecting for link commands" + ); + currentPathHistory = await currentPathAfterCommand( + line, + currentPathHistory + ); + currentPath = currentPathHistory[currentPathHistory.length - 1]; - // A target binary is usually produced by the linker with the /out or /o switch, - // but there are several scenarios (for win32 Microsoft cl.exe) - // when the compiler is producing an output binary directly (via the /Fe switch) - // or indirectly (based on some naming default rules in the absence of /Fe) - let linkerTargetBinary: string | undefined; - let compilerTargetBinary: string | undefined; + // A target binary is usually produced by the linker with the /out or /o switch, + // but there are several scenarios (for win32 Microsoft cl.exe) + // when the compiler is producing an output binary directly (via the /Fe switch) + // or indirectly (based on some naming default rules in the absence of /Fe) + let linkerTargetBinary: string | undefined; + let compilerTargetBinary: string | undefined; - if (process.platform === "win32") { - let compilerTool: ToolInvocation | undefined = await parseLineAsTool(line, compilers, currentPath); - if (compilerTool) { - // If a cl.exe is not performing only an obj compilation, deduce the output executable if possible - // Note: no need to worry about the DLL case that this extension doesn't support yet - // since a compiler can produce implicitly only an executable. + if (process.platform === "win32") { + let compilerTool: ToolInvocation | undefined = await parseLineAsTool( + line, + compilers, + currentPath + ); + if (compilerTool) { + // If a cl.exe is not performing only an obj compilation, deduce the output executable if possible + // Note: no need to worry about the DLL case that this extension doesn't support yet + // since a compiler can produce implicitly only an executable. - if (path.basename(compilerTool.fullPath).startsWith("cl")) { - if (!isSwitchPassedInArguments(compilerTool.arguments, ["c", "P", "E", "EP"])) { - logger.message("Found compiler command:\n" + line, "Verbose"); + if (path.basename(compilerTool.fullPath).startsWith("cl")) { + if ( + !isSwitchPassedInArguments(compilerTool.arguments, [ + "c", + "P", + "E", + "EP", + ]) + ) { + logger.message("Found compiler command:\n" + line, "Verbose"); - // First read the value of the /Fe switch (for cl.exe) - compilerTargetBinary = parseSingleSwitchFromToolArguments(compilerTool.arguments, ["Fe"]); + // First read the value of the /Fe switch (for cl.exe) + compilerTargetBinary = parseSingleSwitchFromToolArguments( + compilerTool.arguments, + ["Fe"] + ); - // Then assume first object file base name (defined with /Fo) + exe - // The binary is produced in the same folder where the compiling operation takes place, - // and not in an eventual different obj path. - // Note: /Fo is not allowed on multiple sources compilations so there will be only one if found - if (!compilerTargetBinary) { - let objFile: string | undefined = parseSingleSwitchFromToolArguments(compilerTool.arguments, ["Fo"]); - if (objFile) { - let parsedObjPath: path.ParsedPath = path.parse(objFile); - compilerTargetBinary = parsedObjPath.name + ".exe"; - logger.message("The compiler command is not producing a target binary explicitly. Assuming " + - compilerTargetBinary + " from the first object passed in with /Fo", "Verbose"); - } - } else { - logger.message("Producing target binary with /Fe: " + compilerTargetBinary, "Verbose"); - } - - // Then assume first source file base name + exe. - // The binary is produced in the same folder where the compiling operation takes place, - // and not in an eventual different source path. - if (!compilerTargetBinary) { - let srcFiles: string[] | undefined = parseFilesFromToolArguments(compilerTool.arguments, sourceFileExtensions); - if (srcFiles.length >= 1) { - let parsedSourcePath: path.ParsedPath = path.parse(srcFiles[0]); - compilerTargetBinary = parsedSourcePath.name + ".exe"; - logger.message("The compiler command is not producing a target binary explicitly. Assuming " + - compilerTargetBinary + " from the first source file passed in", "Verbose"); - } - } - } - } - - if (compilerTargetBinary) { - compilerTargetBinary = await util.makeFullPath(compilerTargetBinary, currentPath); - } + // Then assume first object file base name (defined with /Fo) + exe + // The binary is produced in the same folder where the compiling operation takes place, + // and not in an eventual different obj path. + // Note: /Fo is not allowed on multiple sources compilations so there will be only one if found + if (!compilerTargetBinary) { + let objFile: string | undefined = + parseSingleSwitchFromToolArguments(compilerTool.arguments, [ + "Fo", + ]); + if (objFile) { + let parsedObjPath: path.ParsedPath = path.parse(objFile); + compilerTargetBinary = parsedObjPath.name + ".exe"; + logger.message( + "The compiler command is not producing a target binary explicitly. Assuming " + + compilerTargetBinary + + " from the first object passed in with /Fo", + "Verbose" + ); } - } + } else { + logger.message( + "Producing target binary with /Fe: " + compilerTargetBinary, + "Verbose" + ); + } - let linkerTool: ToolInvocation | undefined = await parseLineAsTool(line, linkers, currentPath); - if (linkerTool) { - // TODO: implement launch support for DLLs and LIBs, besides executables. - if (!isSwitchPassedInArguments(linkerTool.arguments, ["dll", "lib", "shared"])) { - // Gcc/Clang tools can also perform linking so don't parse any output binary - // if there are switches passed in to cause early stop of compilation: -c, -E, -S - // (-o will not point to an executable) - // Also, the ld switches -r and -Ur do not produce executables. - if (!isSwitchPassedInArguments(linkerTool.arguments, ["c", "E", "S", "r", "Ur"])) { - linkerTargetBinary = parseSingleSwitchFromToolArguments(linkerTool.arguments, ["out", "o"]); - logger.message("Found linker command: " + line, "Verbose"); - - if (!linkerTargetBinary) { - // For Microsoft link.exe, the default output binary takes the base name - // of the first file (obj, lib, etc...) that is passed to the linker. - // The binary is produced in the same folder where the linking operation takes place, - // and not in an eventual different obj/lib path. - if (process.platform === "win32" && path.basename(linkerTool.fullPath).startsWith("link")) { - let files: string[] = parseFilesFromToolArguments(linkerTool.arguments, ["obj", "lib"]); - if (files.length >= 1) { - let parsedPath: path.ParsedPath = path.parse(files[0]); - let targetBinaryFromFirstObjLib: string = parsedPath.name + ".exe"; - logger.message("The link command is not producing a target binary explicitly. Assuming " + - targetBinaryFromFirstObjLib + " based on first object passed in", "Verbose"); - linkerTargetBinary = targetBinaryFromFirstObjLib; - } - } else { - // The default output binary from a linking operation is usually a.out on linux/mac, - // produced in the same folder where the toolset is run. - logger.message("The link command is not producing a target binary explicitly. Assuming a.out", "Verbose"); - linkerTargetBinary = "a.out"; - } - } - } - - if (linkerTargetBinary) { - // Until we implement a more robust link target analysis - // (like query-ing for the executable attributes), - // we can safely assume that a ".la" file produced by libtool - // is a library and not an executable binary. - if (linkerTargetBinary.endsWith(".la") && dryRunOutputLines[index - 1] === "LIBTOOL_PATTERN") { - linkerTargetBinary = undefined; - } else { - linkerTargetBinary = util.removeSurroundingQuotes(linkerTargetBinary); - logger.message("Producing target binary: " + linkerTargetBinary, "Verbose"); - linkerTargetBinary = await util.makeFullPath(linkerTargetBinary, currentPath); - } - } + // Then assume first source file base name + exe. + // The binary is produced in the same folder where the compiling operation takes place, + // and not in an eventual different source path. + if (!compilerTargetBinary) { + let srcFiles: string[] | undefined = + parseFilesFromToolArguments( + compilerTool.arguments, + sourceFileExtensions + ); + if (srcFiles.length >= 1) { + let parsedSourcePath: path.ParsedPath = path.parse( + srcFiles[0] + ); + compilerTargetBinary = parsedSourcePath.name + ".exe"; + logger.message( + "The compiler command is not producing a target binary explicitly. Assuming " + + compilerTargetBinary + + " from the first source file passed in", + "Verbose" + ); } + } } + } - // It is not possible to have compilerTargetBinary and linkerTargetBinary both defined, - // because a dry-run output line cannot be a compilation and an explicit link at the same time. - // (cl.exe with /link switch is split into two lines - cl.exe and link.exe - during dry-run preprocessing). - // Also for gcc/clang, -o switch or the default output will be a .o in the presence of -c and an executable otherwise. - let targetBinary: string | undefined = linkerTargetBinary || compilerTargetBinary; - - // Some "$" (without following open paranthesis) are still left in the preprocessed output, - // because the configuraion provider parser may lose valid compilation lines otherwise. - // Additionally, for linker commands, ignore any dollar if present in the target binary name. - // We need to ignore the $ anywhere else in the linker command line so that we don't lose - // valid target binaries. - if (targetBinary && !targetBinary.includes("$")) { - targetBinaries.push(targetBinary); - - // Include limited launch configuration, when only the binary is known, - // in which case the execution path is defaulting to binary containing folder. - // It is more likely that an invocation would succeed from that location - // as opposed from any other (like the root) because of eventual dependencies - // that very likely to be built in the same place. - // and there are no args. - let launchConfiguration: configuration.LaunchConfiguration = { - binaryPath: targetBinary, - cwd: path.parse(targetBinary).dir, - binaryArgs: [] - }; - - logger.message("Adding launch configuration:\n" + configuration.launchConfigurationToString(launchConfiguration), "Verbose"); - onFoundLaunchConfiguration(launchConfiguration); - } - - index++; - if (index === numberOfLines) { - done = true; - } - - chunkIndex++; - } // while loop - }; // doLinkCommandsParsingChunk function - - while (!done) { - if (cancel.isCancellationRequested) { - return make.ConfigureBuildReturnCodeTypes.cancelled; + if (compilerTargetBinary) { + compilerTargetBinary = await util.makeFullPath( + compilerTargetBinary, + currentPath + ); + } } + } - await util.scheduleAsyncTask(doLinkCommandsParsingChunk); - } + let linkerTool: ToolInvocation | undefined = await parseLineAsTool( + line, + linkers, + currentPath + ); + if (linkerTool) { + // TODO: implement launch support for DLLs and LIBs, besides executables. + if ( + !isSwitchPassedInArguments(linkerTool.arguments, [ + "dll", + "lib", + "shared", + ]) + ) { + // Gcc/Clang tools can also perform linking so don't parse any output binary + // if there are switches passed in to cause early stop of compilation: -c, -E, -S + // (-o will not point to an executable) + // Also, the ld switches -r and -Ur do not produce executables. + if ( + !isSwitchPassedInArguments(linkerTool.arguments, [ + "c", + "E", + "S", + "r", + "Ur", + ]) + ) { + linkerTargetBinary = parseSingleSwitchFromToolArguments( + linkerTool.arguments, + ["out", "o"] + ); + logger.message("Found linker command: " + line, "Verbose"); - // If no binaries are found to be built, there is no point in parsing for invoking targets - if (targetBinaries.length === 0) { - return cancel.isCancellationRequested ? make.ConfigureBuildReturnCodeTypes.cancelled : make.ConfigureBuildReturnCodeTypes.success; - } + if (!linkerTargetBinary) { + // For Microsoft link.exe, the default output binary takes the base name + // of the first file (obj, lib, etc...) that is passed to the linker. + // The binary is produced in the same folder where the linking operation takes place, + // and not in an eventual different obj/lib path. + if ( + process.platform === "win32" && + path.basename(linkerTool.fullPath).startsWith("link") + ) { + let files: string[] = parseFilesFromToolArguments( + linkerTool.arguments, + ["obj", "lib"] + ); + if (files.length >= 1) { + let parsedPath: path.ParsedPath = path.parse(files[0]); + let targetBinaryFromFirstObjLib: string = + parsedPath.name + ".exe"; + logger.message( + "The link command is not producing a target binary explicitly. Assuming " + + targetBinaryFromFirstObjLib + + " based on first object passed in", + "Verbose" + ); + linkerTargetBinary = targetBinaryFromFirstObjLib; + } + } else { + // The default output binary from a linking operation is usually a.out on linux/mac, + // produced in the same folder where the toolset is run. + logger.message( + "The link command is not producing a target binary explicitly. Assuming a.out", + "Verbose" + ); + linkerTargetBinary = "a.out"; + } + } + } - // For each of the built binaries identified in the dry-run pass above, - // search the makefile for possible targets that are invoking them, - // to update the launch configuration with their name, full path, execution path and args. - // If a built binary is not having an execution target defined in the makefile, - // the launch configuration will be limited to the version having only with their name and path, - // workspace folder instead of another execution path and zero args. - // If this is not sufficient, the user can at any time write an execution target - // in the makefile or write a launch configuration in the settings json. - - // TODO: investigate the scenario when the binary is run relying on path environment variable - // and attention to on the fly environment changes made by make. - - // Reset the current path since we are going to analyze path transitions again - // with this second pass through the dry-run output lines, - // while building the launch custom provider data. - currentPath = util.getWorkspaceRoot(); - currentPathHistory = [currentPath]; - - // Since an executable can be called without its extension, - // on Windows only and only for extensions 'exe', - // create a new array with target binaries names - // to ensure we parse right these binaries invocation right. - let targetBinariesNames: string[] = []; - targetBinaries.forEach(target => { - let parsedPath: path.ParsedPath = path.parse(target); - if (!targetBinariesNames.includes(parsedPath.name)) { - if (process.platform === "win32" && parsedPath.ext === "exe") { - targetBinariesNames.push(util.escapeString(parsedPath.name)); + if (linkerTargetBinary) { + // Until we implement a more robust link target analysis + // (like query-ing for the executable attributes), + // we can safely assume that a ".la" file produced by libtool + // is a library and not an executable binary. + if ( + linkerTargetBinary.endsWith(".la") && + dryRunOutputLines[index - 1] === "LIBTOOL_PATTERN" + ) { + linkerTargetBinary = undefined; } else { - targetBinariesNames.push(util.escapeString(parsedPath.base)); + linkerTargetBinary = + util.removeSurroundingQuotes(linkerTargetBinary); + logger.message( + "Producing target binary: " + linkerTargetBinary, + "Verbose" + ); + linkerTargetBinary = await util.makeFullPath( + linkerTargetBinary, + currentPath + ); } + } } - }); + } - index = 0; - done = false; - let doBinaryInvocationsParsingChunk: (() => Promise) = async () => { - let chunkIndex: number = 0; - while (index < numberOfLines && chunkIndex <= chunkSize) { - if (cancel.isCancellationRequested) { - break; - } + // It is not possible to have compilerTargetBinary and linkerTargetBinary both defined, + // because a dry-run output line cannot be a compilation and an explicit link at the same time. + // (cl.exe with /link switch is split into two lines - cl.exe and link.exe - during dry-run preprocessing). + // Also for gcc/clang, -o switch or the default output will be a .o in the presence of -c and an executable otherwise. + let targetBinary: string | undefined = + linkerTargetBinary || compilerTargetBinary; - let line: string = dryRunOutputLines[index]; - // Some "$" (without following open paranthesis) are still left in the preprocessed output, - // because the configuraion provider parser may lose valid compilation lines otherwise. - // But the binary invocations parser should ignore any dollar because the extension can't resolve - // these anyway, wherever they are (current folder, binary name or arguments). - if (!line.includes("$")) { - statusCallback("Parsing for launch targets: inspecting built binary invocations"); - currentPathHistory = await currentPathAfterCommand(line, currentPathHistory); - currentPath = currentPathHistory[currentPathHistory.length - 1]; + // Some "$" (without following open paranthesis) are still left in the preprocessed output, + // because the configuraion provider parser may lose valid compilation lines otherwise. + // Additionally, for linker commands, ignore any dollar if present in the target binary name. + // We need to ignore the $ anywhere else in the linker command line so that we don't lose + // valid target binaries. + if (targetBinary && !targetBinary.includes("$")) { + targetBinaries.push(targetBinary); - // Currently, the target binary invocation will not be identified if the line does not start with it, - // because we need to be able to reject matches like "link.exe /out:mybinary.exe". - // See comment in parseLineAsTool about not understanding well what it is that prepends - // the target binary tool, unless we treat it as a path and verify its location on disk. - // Because of this limitation, the extension might not present to the user - // all the scenarios of arguments defined in the makefile for this target binary. - // TODO: identify and parse properly all the valid scenarios of invoking a taget binary in a makefile: - // - @if (not) exist binary binary arg1 arg2 arg3 - // (because an "@if exist" is not resolved by the dry-run and appears in the output) - // - cmd /c binary arg1 arg2 arg3 - // - start binary - let targetBinaryTool: ToolInvocation | undefined = await parseLineAsTool(line, targetBinariesNames, currentPath); + // Include limited launch configuration, when only the binary is known, + // in which case the execution path is defaulting to binary containing folder. + // It is more likely that an invocation would succeed from that location + // as opposed from any other (like the root) because of eventual dependencies + // that very likely to be built in the same place. + // and there are no args. + let launchConfiguration: configuration.LaunchConfiguration = { + binaryPath: targetBinary, + cwd: path.parse(targetBinary).dir, + binaryArgs: [], + }; - // If the found target binary invocation does not happen from a location - // where it was built previously, don't include it as a launch target. - // We can debug only what was built. Also, it's quite common to run - // tools from the path during the build and we shouldn't launch those. - if (targetBinaryTool) { - let foundTargetBinary: boolean = false; - targetBinaries.forEach(target => { - if (target === targetBinaryTool?.fullPath) { - foundTargetBinary = true; - } - }); + logger.message( + "Adding launch configuration:\n" + + configuration.launchConfigurationToString(launchConfiguration), + "Verbose" + ); + onFoundLaunchConfiguration(launchConfiguration); + } - if (!foundTargetBinary) { - targetBinaryTool = undefined; - } - } + index++; + if (index === numberOfLines) { + done = true; + } - if (targetBinaryTool) { - logger.message("Found binary execution command: " + line, "Verbose"); - // Include complete launch configuration: binary, execution path and args - // are known from parsing the dry-run - let splitArgs: string[] = targetBinaryTool.arguments ? await parseAnySwitchFromToolArguments(targetBinaryTool.arguments, []) : []; - if (splitArgs.length > 0) { - splitArgs = filterTargetBinaryArgs(splitArgs); - } + chunkIndex++; + } // while loop + }; // doLinkCommandsParsingChunk function - let launchConfiguration: configuration.LaunchConfiguration = { - binaryPath: targetBinaryTool.fullPath, - cwd: currentPath, - // TODO: consider optionally quoted arguments - binaryArgs: splitArgs - }; - - logger.message("Adding launch configuration:\n" + configuration.launchConfigurationToString(launchConfiguration), "Verbose"); - onFoundLaunchConfiguration(launchConfiguration); - } - } - - index++; - if (index === numberOfLines) { - done = true; - } - - chunkIndex++; - } // while loop - }; // doBinaryInvocationsParsingChunk function - - while (!done) { - if (cancel.isCancellationRequested) { - break; - } - - await util.scheduleAsyncTask(doBinaryInvocationsParsingChunk); + while (!done) { + if (cancel.isCancellationRequested) { + return make.ConfigureBuildReturnCodeTypes.cancelled; } - return cancel.isCancellationRequested ? make.ConfigureBuildReturnCodeTypes.cancelled : make.ConfigureBuildReturnCodeTypes.success; + await util.scheduleAsyncTask(doLinkCommandsParsingChunk); + } + + // If no binaries are found to be built, there is no point in parsing for invoking targets + if (targetBinaries.length === 0) { + return cancel.isCancellationRequested + ? make.ConfigureBuildReturnCodeTypes.cancelled + : make.ConfigureBuildReturnCodeTypes.success; + } + + // For each of the built binaries identified in the dry-run pass above, + // search the makefile for possible targets that are invoking them, + // to update the launch configuration with their name, full path, execution path and args. + // If a built binary is not having an execution target defined in the makefile, + // the launch configuration will be limited to the version having only with their name and path, + // workspace folder instead of another execution path and zero args. + // If this is not sufficient, the user can at any time write an execution target + // in the makefile or write a launch configuration in the settings json. + + // TODO: investigate the scenario when the binary is run relying on path environment variable + // and attention to on the fly environment changes made by make. + + // Reset the current path since we are going to analyze path transitions again + // with this second pass through the dry-run output lines, + // while building the launch custom provider data. + currentPath = util.getWorkspaceRoot(); + currentPathHistory = [currentPath]; + + // Since an executable can be called without its extension, + // on Windows only and only for extensions 'exe', + // create a new array with target binaries names + // to ensure we parse right these binaries invocation right. + let targetBinariesNames: string[] = []; + targetBinaries.forEach((target) => { + let parsedPath: path.ParsedPath = path.parse(target); + if (!targetBinariesNames.includes(parsedPath.name)) { + if (process.platform === "win32" && parsedPath.ext === "exe") { + targetBinariesNames.push(util.escapeString(parsedPath.name)); + } else { + targetBinariesNames.push(util.escapeString(parsedPath.base)); + } + } + }); + + index = 0; + done = false; + let doBinaryInvocationsParsingChunk: () => Promise = async () => { + let chunkIndex: number = 0; + while (index < numberOfLines && chunkIndex <= chunkSize) { + if (cancel.isCancellationRequested) { + break; + } + + let line: string = dryRunOutputLines[index]; + // Some "$" (without following open paranthesis) are still left in the preprocessed output, + // because the configuraion provider parser may lose valid compilation lines otherwise. + // But the binary invocations parser should ignore any dollar because the extension can't resolve + // these anyway, wherever they are (current folder, binary name or arguments). + if (!line.includes("$")) { + statusCallback( + "Parsing for launch targets: inspecting built binary invocations" + ); + currentPathHistory = await currentPathAfterCommand( + line, + currentPathHistory + ); + currentPath = currentPathHistory[currentPathHistory.length - 1]; + + // Currently, the target binary invocation will not be identified if the line does not start with it, + // because we need to be able to reject matches like "link.exe /out:mybinary.exe". + // See comment in parseLineAsTool about not understanding well what it is that prepends + // the target binary tool, unless we treat it as a path and verify its location on disk. + // Because of this limitation, the extension might not present to the user + // all the scenarios of arguments defined in the makefile for this target binary. + // TODO: identify and parse properly all the valid scenarios of invoking a taget binary in a makefile: + // - @if (not) exist binary binary arg1 arg2 arg3 + // (because an "@if exist" is not resolved by the dry-run and appears in the output) + // - cmd /c binary arg1 arg2 arg3 + // - start binary + let targetBinaryTool: ToolInvocation | undefined = + await parseLineAsTool(line, targetBinariesNames, currentPath); + + // If the found target binary invocation does not happen from a location + // where it was built previously, don't include it as a launch target. + // We can debug only what was built. Also, it's quite common to run + // tools from the path during the build and we shouldn't launch those. + if (targetBinaryTool) { + let foundTargetBinary: boolean = false; + targetBinaries.forEach((target) => { + if (target === targetBinaryTool?.fullPath) { + foundTargetBinary = true; + } + }); + + if (!foundTargetBinary) { + targetBinaryTool = undefined; + } + } + + if (targetBinaryTool) { + logger.message("Found binary execution command: " + line, "Verbose"); + // Include complete launch configuration: binary, execution path and args + // are known from parsing the dry-run + let splitArgs: string[] = targetBinaryTool.arguments + ? await parseAnySwitchFromToolArguments( + targetBinaryTool.arguments, + [] + ) + : []; + if (splitArgs.length > 0) { + splitArgs = filterTargetBinaryArgs(splitArgs); + } + + let launchConfiguration: configuration.LaunchConfiguration = { + binaryPath: targetBinaryTool.fullPath, + cwd: currentPath, + // TODO: consider optionally quoted arguments + binaryArgs: splitArgs, + }; + + logger.message( + "Adding launch configuration:\n" + + configuration.launchConfigurationToString(launchConfiguration), + "Verbose" + ); + onFoundLaunchConfiguration(launchConfiguration); + } + } + + index++; + if (index === numberOfLines) { + done = true; + } + + chunkIndex++; + } // while loop + }; // doBinaryInvocationsParsingChunk function + + while (!done) { + if (cancel.isCancellationRequested) { + break; + } + + await util.scheduleAsyncTask(doBinaryInvocationsParsingChunk); + } + + return cancel.isCancellationRequested + ? make.ConfigureBuildReturnCodeTypes.cancelled + : make.ConfigureBuildReturnCodeTypes.success; } /** * Determine the IntelliSenseMode based on hints from compiler path * and target architecture parsed from compiler flags. */ -function getIntelliSenseMode(cppVersion: cpp.Version | undefined, compilerPath: string, targetArch: util.TargetArchitecture): util.IntelliSenseMode { - if (cppVersion && cppVersion >= cpp.Version.v5 && targetArch === undefined) { - // IntelliSenseMode is optional for CppTools v5+ and is determined by CppTools. - return undefined; - } +function getIntelliSenseMode( + cppVersion: cpp.Version | undefined, + compilerPath: string, + targetArch: util.TargetArchitecture +): util.IntelliSenseMode { + if (cppVersion && cppVersion >= cpp.Version.v5 && targetArch === undefined) { + // IntelliSenseMode is optional for CppTools v5+ and is determined by CppTools. + return undefined; + } - const canUseArm: boolean = (cppVersion !== undefined && cppVersion >= cpp.Version.v4); - const compilerName: string = path.basename(compilerPath || "").toLocaleLowerCase(); - if (compilerName === 'cl.exe') { - const clArch: string = path.basename(path.dirname(compilerPath)).toLocaleLowerCase(); - switch (clArch) { - case 'arm64': - return canUseArm ? 'msvc-arm64' : 'msvc-x64'; - case 'arm': - return canUseArm ? 'msvc-arm' : 'msvc-x86'; - case 'x86': - return 'msvc-x86'; - case 'x64': - default: - return 'msvc-x64'; - } - } else if (compilerName.indexOf('armclang') >= 0) { - switch (targetArch) { - case 'arm64': - return canUseArm ? 'clang-arm64' : 'clang-x64'; - case 'arm': - default: - return canUseArm ? 'clang-arm' : 'clang-x86'; - } - } else if (compilerName.indexOf('clang') >= 0) { - switch (targetArch) { - case 'arm64': - return canUseArm ? 'clang-arm64' : 'clang-x64'; - case 'arm': - return canUseArm ? 'clang-arm' : 'clang-x86'; - case 'x86': - return 'clang-x86'; - case 'x64': - default: - return 'clang-x64'; - } - } else if (compilerName.indexOf('aarch64') >= 0) { - // Compiler with 'aarch64' in its name may also have 'arm', so check for - // aarch64 compilers before checking for ARM specific compilers. - return canUseArm ? 'gcc-arm64' : 'gcc-x64'; - } else if (compilerName.indexOf('arm') >= 0) { - return canUseArm ? 'gcc-arm' : 'gcc-x86'; - } else if (compilerName.indexOf('gcc') >= 0 || compilerName.indexOf('g++') >= 0) { - switch (targetArch) { - case 'x86': - return 'gcc-x86'; - case 'x64': - default: - return 'gcc-x64'; - } - } else { - // unknown compiler; pick platform defaults. - if (process.platform === 'win32') { - return 'msvc-x64'; - } else if (process.platform === 'darwin') { - return 'clang-x64'; - } else { - return 'gcc-x64'; - } + const canUseArm: boolean = + cppVersion !== undefined && cppVersion >= cpp.Version.v4; + const compilerName: string = path + .basename(compilerPath || "") + .toLocaleLowerCase(); + if (compilerName === "cl.exe") { + const clArch: string = path + .basename(path.dirname(compilerPath)) + .toLocaleLowerCase(); + switch (clArch) { + case "arm64": + return canUseArm ? "msvc-arm64" : "msvc-x64"; + case "arm": + return canUseArm ? "msvc-arm" : "msvc-x86"; + case "x86": + return "msvc-x86"; + case "x64": + default: + return "msvc-x64"; } + } else if (compilerName.indexOf("armclang") >= 0) { + switch (targetArch) { + case "arm64": + return canUseArm ? "clang-arm64" : "clang-x64"; + case "arm": + default: + return canUseArm ? "clang-arm" : "clang-x86"; + } + } else if (compilerName.indexOf("clang") >= 0) { + switch (targetArch) { + case "arm64": + return canUseArm ? "clang-arm64" : "clang-x64"; + case "arm": + return canUseArm ? "clang-arm" : "clang-x86"; + case "x86": + return "clang-x86"; + case "x64": + default: + return "clang-x64"; + } + } else if (compilerName.indexOf("aarch64") >= 0) { + // Compiler with 'aarch64' in its name may also have 'arm', so check for + // aarch64 compilers before checking for ARM specific compilers. + return canUseArm ? "gcc-arm64" : "gcc-x64"; + } else if (compilerName.indexOf("arm") >= 0) { + return canUseArm ? "gcc-arm" : "gcc-x86"; + } else if ( + compilerName.indexOf("gcc") >= 0 || + compilerName.indexOf("g++") >= 0 + ) { + switch (targetArch) { + case "x86": + return "gcc-x86"; + case "x64": + default: + return "gcc-x64"; + } + } else { + // unknown compiler; pick platform defaults. + if (process.platform === "win32") { + return "msvc-x64"; + } else if (process.platform === "darwin") { + return "clang-x64"; + } else { + return "gcc-x64"; + } + } } /** * Determine the target architecture from the compiler flags present in the given compilation command. */ function getTargetArchitecture(compilerArgs: string): util.TargetArchitecture { - // Go through all the possible target architecture switches. - // For each switch, apply a set of rules to identify the target arch. - // The last switch wins. - let possibleArchs: string[] = parseMultipleSwitchesFromToolArguments(compilerArgs, ["m32", "m64"], ["arch", "march", "target"]); - let targetArch: util.TargetArchitecture; // this starts as undefined + // Go through all the possible target architecture switches. + // For each switch, apply a set of rules to identify the target arch. + // The last switch wins. + let possibleArchs: string[] = parseMultipleSwitchesFromToolArguments( + compilerArgs, + ["m32", "m64"], + ["arch", "march", "target"] + ); + let targetArch: util.TargetArchitecture; // this starts as undefined - possibleArchs.forEach(arch => { - if (arch === "m32") { - targetArch = "x86"; - } else if (arch === "m64") { - targetArch = "x64"; - } else if (arch === "i686") { - targetArch = "x86"; - } else if (arch === "amd64" || arch === "x86_64") { - targetArch = "x64"; - } else if (arch === "aarch64" || arch === "armv8-a" || arch === "armv8.") { - targetArch = "arm64"; - } else if (arch === "arm" || arch === "armv8-r" || arch === "armv8-m") { - targetArch = "arm"; - } else { - // Check if ARM version is 7 or earlier. - const verStr: string | undefined = arch?.substr(4, 1); - if (verStr) { - const verNum: number = +verStr; - if (verNum <= 7) { - targetArch = "arm"; - } - } - } - }); - - return targetArch; -} - -export function parseStandard(cppVersion: cpp.Version | undefined, std: string | undefined, language: util.Language): util.StandardVersion | undefined { - let canUseGnu: boolean = (cppVersion !== undefined && cppVersion >= cpp.Version.v4); - let canUseCxx23: boolean = (cppVersion !== undefined && cppVersion >= cpp.Version.v6); - let standard: util.StandardVersion | undefined; - if (cppVersion && cppVersion >= cpp.Version.v5 && std === undefined) { - // C/C++ standard is optional for CppTools v5+ and is determined by CppTools. - return undefined; - } - - if (!std) { - // Standard defaults when no std switch is given - if (language === "c") { - return "c11"; - } else if (language === "cpp") { - return "c++17"; - } - } else if (language === "cpp") { - standard = parseCppStandard(std, canUseGnu, canUseCxx23); - if (!standard) { - logger.message(`Unknown C++ standard control flag: ${std}`, "Normal"); - } - } else if (language === "c") { - standard = parseCStandard(std, canUseGnu); - if (!standard) { - logger.message(`Unknown C standard control flag: ${std}`, "Normal"); - } - } else if (language === undefined) { - standard = parseCppStandard(std, canUseGnu, canUseCxx23); - if (!standard) { - standard = parseCStandard(std, canUseGnu); - } - if (!standard) { - logger.message(`Unknown standard control flag: ${std}`, "Normal"); - } + possibleArchs.forEach((arch) => { + if (arch === "m32") { + targetArch = "x86"; + } else if (arch === "m64") { + targetArch = "x64"; + } else if (arch === "i686") { + targetArch = "x86"; + } else if (arch === "amd64" || arch === "x86_64") { + targetArch = "x64"; + } else if (arch === "aarch64" || arch === "armv8-a" || arch === "armv8.") { + targetArch = "arm64"; + } else if (arch === "arm" || arch === "armv8-r" || arch === "armv8-m") { + targetArch = "arm"; } else { - logger.message("Unknown language", "Normal"); - } - - return standard; -} - -function parseCppStandard(std: string, canUseGnu: boolean, canUseCxx23: boolean): util.StandardVersion | undefined { - const isGnu: boolean = canUseGnu && std.startsWith('gnu'); - if (std.endsWith('++23') || std.endsWith('++2b') || std.endsWith('++latest')) { - if (canUseCxx23) { - return isGnu ? 'gnu++23' : 'c++23'; - } else { - return isGnu ? 'gnu++20' : 'c++20'; + // Check if ARM version is 7 or earlier. + const verStr: string | undefined = arch?.substr(4, 1); + if (verStr) { + const verNum: number = +verStr; + if (verNum <= 7) { + targetArch = "arm"; } - } else if (std.endsWith('++2a') || std.endsWith('++20')) { - return isGnu ? 'gnu++20' : 'c++20'; - } else if (std.endsWith('++17') || std.endsWith('++1z')) { - return isGnu ? 'gnu++17' : 'c++17'; - } else if (std.endsWith('++14') || std.endsWith('++1y')) { - return isGnu ? 'gnu++14' : 'c++14'; - } else if (std.endsWith('++11') || std.endsWith('++0x')) { - return isGnu ? 'gnu++11' : 'c++11'; - } else if (std.endsWith('++03')) { - return isGnu ? 'gnu++03' : 'c++03'; - } else if (std.endsWith('++98')) { - return isGnu ? 'gnu++98' : 'c++98'; - } else { - return undefined; - } - } - - function parseCStandard(std: string, canUseGnu: boolean): util.StandardVersion | undefined { - // GNU options from: https://gcc.gnu.org/onlinedocs/gcc/C-Dialect-Options.html#C-Dialect-Options - const isGnu: boolean = canUseGnu && std.startsWith('gnu'); - if (/(c|gnu)(90|89|iso9899:(1990|199409))/.test(std)) { - return isGnu ? 'gnu89' : 'c89'; - } else if (/(c|gnu)(99|9x|iso9899:(1999|199x))/.test(std)) { - return isGnu ? 'gnu99' : 'c99'; - } else if (/(c|gnu)(11|1x|iso9899:2011)/.test(std)) { - return isGnu ? 'gnu11' : 'c11'; - } else if (/(c|gnu)(17|18|iso9899:(2017|2018))/.test(std)) { - if (canUseGnu) { - // cpptools supports 'c17' in same version it supports GNU std. - return isGnu ? 'gnu17' : 'c17'; - } else { - return 'c11'; } - } else { - return undefined; } + }); + + return targetArch; +} + +export function parseStandard( + cppVersion: cpp.Version | undefined, + std: string | undefined, + language: util.Language +): util.StandardVersion | undefined { + let canUseGnu: boolean = + cppVersion !== undefined && cppVersion >= cpp.Version.v4; + let canUseCxx23: boolean = + cppVersion !== undefined && cppVersion >= cpp.Version.v6; + let standard: util.StandardVersion | undefined; + if (cppVersion && cppVersion >= cpp.Version.v5 && std === undefined) { + // C/C++ standard is optional for CppTools v5+ and is determined by CppTools. + return undefined; } + + if (!std) { + // Standard defaults when no std switch is given + if (language === "c") { + return "c11"; + } else if (language === "cpp") { + return "c++17"; + } + } else if (language === "cpp") { + standard = parseCppStandard(std, canUseGnu, canUseCxx23); + if (!standard) { + logger.message(`Unknown C++ standard control flag: ${std}`, "Normal"); + } + } else if (language === "c") { + standard = parseCStandard(std, canUseGnu); + if (!standard) { + logger.message(`Unknown C standard control flag: ${std}`, "Normal"); + } + } else if (language === undefined) { + standard = parseCppStandard(std, canUseGnu, canUseCxx23); + if (!standard) { + standard = parseCStandard(std, canUseGnu); + } + if (!standard) { + logger.message(`Unknown standard control flag: ${std}`, "Normal"); + } + } else { + logger.message("Unknown language", "Normal"); + } + + return standard; +} + +function parseCppStandard( + std: string, + canUseGnu: boolean, + canUseCxx23: boolean +): util.StandardVersion | undefined { + const isGnu: boolean = canUseGnu && std.startsWith("gnu"); + if ( + std.endsWith("++23") || + std.endsWith("++2b") || + std.endsWith("++latest") + ) { + if (canUseCxx23) { + return isGnu ? "gnu++23" : "c++23"; + } else { + return isGnu ? "gnu++20" : "c++20"; + } + } else if (std.endsWith("++2a") || std.endsWith("++20")) { + return isGnu ? "gnu++20" : "c++20"; + } else if (std.endsWith("++17") || std.endsWith("++1z")) { + return isGnu ? "gnu++17" : "c++17"; + } else if (std.endsWith("++14") || std.endsWith("++1y")) { + return isGnu ? "gnu++14" : "c++14"; + } else if (std.endsWith("++11") || std.endsWith("++0x")) { + return isGnu ? "gnu++11" : "c++11"; + } else if (std.endsWith("++03")) { + return isGnu ? "gnu++03" : "c++03"; + } else if (std.endsWith("++98")) { + return isGnu ? "gnu++98" : "c++98"; + } else { + return undefined; + } +} + +function parseCStandard( + std: string, + canUseGnu: boolean +): util.StandardVersion | undefined { + // GNU options from: https://gcc.gnu.org/onlinedocs/gcc/C-Dialect-Options.html#C-Dialect-Options + const isGnu: boolean = canUseGnu && std.startsWith("gnu"); + if (/(c|gnu)(90|89|iso9899:(1990|199409))/.test(std)) { + return isGnu ? "gnu89" : "c89"; + } else if (/(c|gnu)(99|9x|iso9899:(1999|199x))/.test(std)) { + return isGnu ? "gnu99" : "c99"; + } else if (/(c|gnu)(11|1x|iso9899:2011)/.test(std)) { + return isGnu ? "gnu11" : "c11"; + } else if (/(c|gnu)(17|18|iso9899:(2017|2018))/.test(std)) { + if (canUseGnu) { + // cpptools supports 'c17' in same version it supports GNU std. + return isGnu ? "gnu17" : "c17"; + } else { + return "c11"; + } + } else { + return undefined; + } +} diff --git a/src/state.ts b/src/state.ts index 8f91548..888ac98 100644 --- a/src/state.ts +++ b/src/state.ts @@ -3,7 +3,7 @@ // state.ts -import * as vscode from 'vscode'; +import * as vscode from "vscode"; // Class for the management of all the workspace state variables export class StateManager { @@ -20,26 +20,26 @@ export class StateManager { // The project build configuration (one of the entries in the array of makefile.configurations // or a default). get buildConfiguration(): string | undefined { - return this._get('buildConfiguration'); + return this._get("buildConfiguration"); } set buildConfiguration(v: string | undefined) { - this._update('buildConfiguration', v); + this._update("buildConfiguration", v); } // The project build target (one of the targets defined in the makefile). get buildTarget(): string | undefined { - return this._get('buildTarget'); + return this._get("buildTarget"); } set buildTarget(v: string | undefined) { - this._update('buildTarget', v); + this._update("buildTarget", v); } // The project launch configuration (one of the entries in the array of makefile.launchConfigurations). get launchConfiguration(): string | undefined { - return this._get('launchConfiguration'); + return this._get("launchConfiguration"); } set launchConfiguration(v: string | undefined) { - this._update('launchConfiguration', v); + this._update("launchConfiguration", v); } // Whether this project had any configure attempt before @@ -47,10 +47,10 @@ export class StateManager { // Sent as telemetry information and useful to know // how many projects are able to configure out of the box. get ranConfigureInCodebaseLifetime(): boolean { - return this._get('ranConfigureInCodebaseLifetime') || false; + return this._get("ranConfigureInCodebaseLifetime") || false; } set ranConfigureInCodebaseLifetime(v: boolean) { - this._update('ranConfigureInCodebaseLifetime', v); + this._update("ranConfigureInCodebaseLifetime", v); } // Whether this project had any --dry-run specific configure attempt before @@ -59,10 +59,10 @@ export class StateManager { // that some makefile code could still execute even in --dry-run mode. // Once the user decides 'Yes(don't show again)' the popup is not shown. get ranDryRunInCodebaseLifetime(): boolean { - return this._get('ranDryRunInCodebaseLifetime') || false; + return this._get("ranDryRunInCodebaseLifetime") || false; } set ranDryRunInCodebaseLifetime(v: boolean) { - this._update('ranDryRunInCodebaseLifetime', v); + this._update("ranDryRunInCodebaseLifetime", v); } // If the project needs a clean configure as a result @@ -70,15 +70,15 @@ export class StateManager { // (makefile configuration change, build target change, // settings or makefiles edits) get configureDirty(): boolean { - let dirty: boolean | undefined = this._get('configureDirty'); + let dirty: boolean | undefined = this._get("configureDirty"); if (dirty === undefined) { - dirty = true; + dirty = true; } return dirty; } set configureDirty(v: boolean) { - this._update('configureDirty', v); + this._update("configureDirty", v); } // Reset all the variables saved in the workspace state. @@ -91,7 +91,7 @@ export class StateManager { this.configureDirty = false; if (reloadWindow) { - vscode.commands.executeCommand('workbench.action.reloadWindow'); + vscode.commands.executeCommand("workbench.action.reloadWindow"); } } } diff --git a/src/telemetry.ts b/src/telemetry.ts index 10d0d04..cf35ae4 100644 --- a/src/telemetry.ts +++ b/src/telemetry.ts @@ -3,109 +3,123 @@ // Telemetry.ts -import * as configuration from './configuration'; -import * as logger from './logger'; -import * as util from './util'; -import TelemetryReporter from '@vscode/extension-telemetry'; +import * as configuration from "./configuration"; +import * as logger from "./logger"; +import * as util from "./util"; +import TelemetryReporter from "@vscode/extension-telemetry"; import * as vscode from "vscode"; export type Properties = { [key: string]: string }; export type Measures = { [key: string]: number }; interface IPackageInfo { - name: string; - version: string; - aiKey: string; + name: string; + version: string; + aiKey: string; } let telemetryReporter: TelemetryReporter | null; export function activate(): void { - try { - // Don't create the telemetry object (which will result in no information being sent) - // when running Makefile Tools tests. - if (process.env['MAKEFILE_TOOLS_TESTING'] !== '1') { - telemetryReporter = createReporter(); - } - } catch (e) { - // can't really do much about this + try { + // Don't create the telemetry object (which will result in no information being sent) + // when running Makefile Tools tests. + if (process.env["MAKEFILE_TOOLS_TESTING"] !== "1") { + telemetryReporter = createReporter(); } + } catch (e) { + // can't really do much about this + } } export async function deactivate(): Promise { - if (telemetryReporter) { - await telemetryReporter.dispose(); - } + if (telemetryReporter) { + await telemetryReporter.dispose(); + } } export function telemetryLogger(str: string, loggingLevel?: string) { - if (vscode.env.isTelemetryEnabled) { - logger.message(str, loggingLevel); - } + if (vscode.env.isTelemetryEnabled) { + logger.message(str, loggingLevel); + } } -export function logEvent(eventName: string, properties?: Properties, measures?: Measures): void { - // We don't want to log telemetry in testing. - if (telemetryReporter && process.env["MAKEFILE_TOOLS_TESTING"] !== "1") { - try { - telemetryReporter.sendTelemetryEvent(eventName, properties, measures); - } catch (e) { - telemetryLogger(e.message); - } - - telemetryLogger(`Sending telemetry: eventName = ${eventName}`, "Debug"); - - if (properties) { - telemetryLogger(`properties: ${Object.getOwnPropertyNames(properties).map(k => `${k} = "${properties[k]}"`).concat()}`, "Debug"); - } - - if (measures) { - telemetryLogger(`measures: ${Object.getOwnPropertyNames(measures).map(k => `${k} = "${measures[k]}"`).concat()}`, "Debug"); - } +export function logEvent( + eventName: string, + properties?: Properties, + measures?: Measures +): void { + // We don't want to log telemetry in testing. + if (telemetryReporter && process.env["MAKEFILE_TOOLS_TESTING"] !== "1") { + try { + telemetryReporter.sendTelemetryEvent(eventName, properties, measures); + } catch (e) { + telemetryLogger(e.message); } + + telemetryLogger(`Sending telemetry: eventName = ${eventName}`, "Debug"); + + if (properties) { + telemetryLogger( + `properties: ${Object.getOwnPropertyNames(properties) + .map((k) => `${k} = "${properties[k]}"`) + .concat()}`, + "Debug" + ); + } + + if (measures) { + telemetryLogger( + `measures: ${Object.getOwnPropertyNames(measures) + .map((k) => `${k} = "${measures[k]}"`) + .concat()}`, + "Debug" + ); + } + } } // Allow-lists for various settings. -function filterSetting(value: any, key: string, defaultValue: string) : string { - if (key === "makefile.dryrunSwitches") { - let dryrunSwitches: string[] = value; - let filteredSwitches: string[] | undefined = dryrunSwitches.map(sw => { - switch (sw) { - case "--dry-run": - case "-n": - case "--just-print": - case "--recon": +function filterSetting(value: any, key: string, defaultValue: string): string { + if (key === "makefile.dryrunSwitches") { + let dryrunSwitches: string[] = value; + let filteredSwitches: string[] | undefined = dryrunSwitches.map((sw) => { + switch (sw) { + case "--dry-run": + case "-n": + case "--just-print": + case "--recon": - case "--keep-going": - case "-k": + case "--keep-going": + case "-k": - case "--always-make": - case "-B": + case "--always-make": + case "-B": - case "--print-data-base": - case "-p": + case "--print-data-base": + case "-p": - case "--print-directory": - case "-w": - return sw; - default: - return "..."; - } - }); + case "--print-directory": + case "-w": + return sw; + default: + return "..."; + } + }); - return filteredSwitches.join(";"); - } + return filteredSwitches.join(";"); + } - // Even if the key represents a setting that shouldn't share its value, - // we can still record if it is undefined by the user (removed from settings.json) - // or equal to the default we set in package.json. - if (!value) { - return "undefined"; - } else if (value === defaultValue) { - return "default"; - } + // Even if the key represents a setting that shouldn't share its value, + // we can still record if it is undefined by the user (removed from settings.json) + // or equal to the default we set in package.json. + if (!value) { + return "undefined"; + } else if (value === defaultValue) { + return "default"; + } - return "..."; + return "..."; } // Detect which item from the given array setting is relevant for telemetry. @@ -116,34 +130,47 @@ function filterSetting(value: any, key: string, defaultValue: string) : string { // Example: don't report telemetry for all launch configurations but only for // the current launch configuration. function activeArrayItem(setting: string, key: string): number { - if (key === "makefile.configurations") { - let makefileConfigurations: configuration.MakefileConfiguration[] = configuration.getMakefileConfigurations(); - let currentMakefileConfigurationName: string | undefined = configuration.getCurrentMakefileConfiguration(); - if (!currentMakefileConfigurationName) { - return -1; - } - - let currentMakefileConfiguration: configuration.MakefileConfiguration | undefined = makefileConfigurations.find(config => { - if (config.name === currentMakefileConfigurationName) { - return config; - } - }); - - return currentMakefileConfiguration ? makefileConfigurations.indexOf(currentMakefileConfiguration) : -1; + if (key === "makefile.configurations") { + let makefileConfigurations: configuration.MakefileConfiguration[] = + configuration.getMakefileConfigurations(); + let currentMakefileConfigurationName: string | undefined = + configuration.getCurrentMakefileConfiguration(); + if (!currentMakefileConfigurationName) { + return -1; } - if (key === "makefile.launchConfigurations") { - let launchConfigurations: configuration.LaunchConfiguration[] = configuration.getLaunchConfigurations(); - let currentLaunchConfiguration: configuration.LaunchConfiguration | undefined = launchConfigurations.find(config => { - if (util.areEqual(config, configuration.getCurrentLaunchConfiguration())) { - return config; - } - }); + let currentMakefileConfiguration: + | configuration.MakefileConfiguration + | undefined = makefileConfigurations.find((config) => { + if (config.name === currentMakefileConfigurationName) { + return config; + } + }); - return currentLaunchConfiguration ? launchConfigurations.indexOf(currentLaunchConfiguration) : -1; - } + return currentMakefileConfiguration + ? makefileConfigurations.indexOf(currentMakefileConfiguration) + : -1; + } - return -1; + if (key === "makefile.launchConfigurations") { + let launchConfigurations: configuration.LaunchConfiguration[] = + configuration.getLaunchConfigurations(); + let currentLaunchConfiguration: + | configuration.LaunchConfiguration + | undefined = launchConfigurations.find((config) => { + if ( + util.areEqual(config, configuration.getCurrentLaunchConfiguration()) + ) { + return config; + } + }); + + return currentLaunchConfiguration + ? launchConfigurations.indexOf(currentLaunchConfiguration) + : -1; + } + + return -1; } // Filter the array item indexes from the key since for now, when we analyze an array, @@ -160,18 +187,18 @@ function activeArrayItem(setting: string, key: string): number { // This helper should be called when a property is ready to be collected for telemetry. // Calling this earlier would result in different dot patterns. function filterKey(key: string): string { - let filteredKey: string = key; - let lastDot: number = key.lastIndexOf("."); - let beforeLastDot: number = key.lastIndexOf(".", lastDot - 1); - if (lastDot !== -1 && beforeLastDot !== -1) { - let lastProp: any = key.substring(beforeLastDot + 1, lastDot); - let numericalProp: number = Number.parseInt(lastProp); - if (!Number.isNaN(numericalProp)) { - filteredKey = filteredKey.replace(`${numericalProp}.`, ""); - } + let filteredKey: string = key; + let lastDot: number = key.lastIndexOf("."); + let beforeLastDot: number = key.lastIndexOf(".", lastDot - 1); + if (lastDot !== -1 && beforeLastDot !== -1) { + let lastProp: any = key.substring(beforeLastDot + 1, lastDot); + let numericalProp: number = Number.parseInt(lastProp); + if (!Number.isNaN(numericalProp)) { + filteredKey = filteredKey.replace(`${numericalProp}.`, ""); } + } - return filteredKey; + return filteredKey; } // Analyze recursively all the settings for telemetry and type validation. @@ -180,165 +207,218 @@ function filterKey(key: string): string { // If analyzeSettings gets called before a configure (or after an unsuccesful one), it is possible to have // inaccurate or incomplete telemetry information for makefile and launch configurations. // This is not very critical since any of their state changes will update telemetry for them. -export async function analyzeSettings(setting: any, key: string, propSchema: any, ignoreDefault: boolean, telemetryProperties: Properties | null): Promise { - // type can be undefined if setting is null, - // which happens when the user removes that setting. - let type : string | undefined = setting ? typeof (setting) : undefined; - let jsonType : string | undefined = propSchema.type ? propSchema.type : undefined; +export async function analyzeSettings( + setting: any, + key: string, + propSchema: any, + ignoreDefault: boolean, + telemetryProperties: Properties | null +): Promise { + // type can be undefined if setting is null, + // which happens when the user removes that setting. + let type: string | undefined = setting ? typeof setting : undefined; + let jsonType: string | undefined = propSchema.type + ? propSchema.type + : undefined; - // Skip anything else if the current setting represents a function. - if (type === "function") { - return telemetryProperties; - } + // Skip anything else if the current setting represents a function. + if (type === "function") { + return telemetryProperties; + } - // Interested to continue only for properties that are different than their defaults, - // unless ignoreDefault requests we report those too (useful when the user is changing - // from a non default value back to default, usually via removing/undefining a setting). - if (util.areEqual(propSchema.default, setting) && ignoreDefault) { - return telemetryProperties; - } + // Interested to continue only for properties that are different than their defaults, + // unless ignoreDefault requests we report those too (useful when the user is changing + // from a non default value back to default, usually via removing/undefining a setting). + if (util.areEqual(propSchema.default, setting) && ignoreDefault) { + return telemetryProperties; + } - // The type "array" defined in package.json is seen as object by the workspace setting type. - // Not all package.json constructs have a type (example: configuration properties list) - // but the workspace setting type sees them as object. - if (jsonType !== type && - jsonType !== undefined && type !== undefined && - (type !== "object" || jsonType !== "array")) { - telemetryLogger(`Settings versus package.json type mismatch for "${key}".`); - } + // The type "array" defined in package.json is seen as object by the workspace setting type. + // Not all package.json constructs have a type (example: configuration properties list) + // but the workspace setting type sees them as object. + if ( + jsonType !== type && + jsonType !== undefined && + type !== undefined && + (type !== "object" || jsonType !== "array") + ) { + telemetryLogger(`Settings versus package.json type mismatch for "${key}".`); + } - // Enum values always safe to report. - // Validate the allowed values against the expanded variable. - let enumValues: any[] = propSchema.enum; - if (enumValues && enumValues.length > 0) { - const regexp: RegExp = /(makefile\.)(.+)/mg; - const res: RegExpExecArray | null = regexp.exec(key); - let expandedSetting: string = res ? await util.getExpandedSetting(res[2]) : setting; - if (!enumValues.includes(expandedSetting)) { - telemetryLogger(`Invalid value "${expandedSetting}" for enum "${key}". Only "${enumValues.join(";")}" values are allowed."`); - if (telemetryProperties) { - telemetryProperties[filterKey(key)] = "invalid"; - } - } else if (telemetryProperties) { - telemetryProperties[filterKey(key)] = expandedSetting; - } - - return telemetryProperties; - } - - // When propSchema does not have a type defined (for example at the root scope) - // use the setting type. We use the setting type second because it sees array as object. - switch (jsonType || type) { - // Report numbers and booleans since there is no private information in such types. - case "boolean": /* falls through */ - case "number": - if (telemetryProperties) { - telemetryProperties[filterKey(key)] = setting; - } - break; - - // Apply allow-lists for strings. - case "string": - if (telemetryProperties) { - telemetryProperties[filterKey(key)] = filterSetting(setting, key, propSchema.default); - } - break; - - case "array": - // We are interested in logging arrays of basic types - if (telemetryProperties && propSchema.items.type !== "object" && propSchema.items.type !== "array") { - telemetryProperties[filterKey(key)] = filterSetting(setting, key, propSchema.default); - break; - } - /* falls through */ - - case "object": - let settingsProps: string[] = Object.getOwnPropertyNames(setting); - let index: number = -1; - let active: number = 0; - if (jsonType === "array") { - active = activeArrayItem(setting, key); - } - - settingsProps.forEach(async (prop) => { - index++; - let jsonProps: any; - let newPropObj: any = setting[prop]; - if (jsonType === "array") { - jsonProps = propSchema.items.properties || propSchema.items; - } else { - // For a setting like "makefile.name1.name2.name3", - // when we need to query for its schema we should use the whole name as index - // but when we query for the workspace value, we have to use each sub object name: - // setting[name1][name2][name3]. - // Otherwise we will not read anything useful about such a setting and we will also - // report a schema mismatch, even if it is written correctly. - let newProp: string = prop; - let newFullProp: string = (key === "makefile") ? key + "." : ""; - while (jsonProps === undefined && newProp !== "") { - newFullProp = newFullProp + newProp; - if (propSchema.properties) { - jsonProps = Object.getOwnPropertyNames(propSchema.properties).includes(newFullProp) ? - propSchema.properties[newFullProp] : undefined; - } else { - jsonProps = Object.getOwnPropertyNames(propSchema).includes(newFullProp) ? - propSchema[newFullProp] : undefined; - } - - if (jsonProps === undefined && typeof(newPropObj) === "object") { - newProp = Object.getOwnPropertyNames(newPropObj)[0]; - newPropObj = newPropObj[newProp]; - newProp = "." + newProp; - } else { - newProp = ""; - } - } - } - - // The user defined a setting property wrong (example miMode instead of MIMode). - // Exceptions are 'has', 'get', 'update' and 'inspect' for the makefile root. - // They are functions and we can use this type to make the exclusion.. - if (jsonProps === undefined) { - if (typeof (setting[prop]) !== "function") { - telemetryLogger(`Schema mismatch between settings and package.json for property "${key}.${prop}"`); - } - } else { - // Skip if the analyzed prop is a function or if it's the length of an array. - if (type !== "function" /*&& jsonType !== undefined*/ && - (jsonType !== "array" || prop !== "length")) { - let newTelemetryProperties: Properties | null = {}; - newTelemetryProperties = await analyzeSettings(newPropObj, key + "." + prop, jsonProps, ignoreDefault, - ((jsonType !== "array" || index === active)) ? newTelemetryProperties : null); - - // If telemetryProperties is null, it means we're not interested in reporting any telemetry for this subtree - if (telemetryProperties) { - telemetryProperties = util.mergeProperties(telemetryProperties, newTelemetryProperties); - } - } - } - }); - break; - - default: - break; + // Enum values always safe to report. + // Validate the allowed values against the expanded variable. + let enumValues: any[] = propSchema.enum; + if (enumValues && enumValues.length > 0) { + const regexp: RegExp = /(makefile\.)(.+)/gm; + const res: RegExpExecArray | null = regexp.exec(key); + let expandedSetting: string = res + ? await util.getExpandedSetting(res[2]) + : setting; + if (!enumValues.includes(expandedSetting)) { + telemetryLogger( + `Invalid value "${expandedSetting}" for enum "${key}". Only "${enumValues.join( + ";" + )}" values are allowed."` + ); + if (telemetryProperties) { + telemetryProperties[filterKey(key)] = "invalid"; + } + } else if (telemetryProperties) { + telemetryProperties[filterKey(key)] = expandedSetting; } return telemetryProperties; + } + + // When propSchema does not have a type defined (for example at the root scope) + // use the setting type. We use the setting type second because it sees array as object. + switch (jsonType || type) { + // Report numbers and booleans since there is no private information in such types. + case "boolean": /* falls through */ + case "number": + if (telemetryProperties) { + telemetryProperties[filterKey(key)] = setting; + } + break; + + // Apply allow-lists for strings. + case "string": + if (telemetryProperties) { + telemetryProperties[filterKey(key)] = filterSetting( + setting, + key, + propSchema.default + ); + } + break; + + case "array": + // We are interested in logging arrays of basic types + if ( + telemetryProperties && + propSchema.items.type !== "object" && + propSchema.items.type !== "array" + ) { + telemetryProperties[filterKey(key)] = filterSetting( + setting, + key, + propSchema.default + ); + break; + } + /* falls through */ + + case "object": + let settingsProps: string[] = Object.getOwnPropertyNames(setting); + let index: number = -1; + let active: number = 0; + if (jsonType === "array") { + active = activeArrayItem(setting, key); + } + + settingsProps.forEach(async (prop) => { + index++; + let jsonProps: any; + let newPropObj: any = setting[prop]; + if (jsonType === "array") { + jsonProps = propSchema.items.properties || propSchema.items; + } else { + // For a setting like "makefile.name1.name2.name3", + // when we need to query for its schema we should use the whole name as index + // but when we query for the workspace value, we have to use each sub object name: + // setting[name1][name2][name3]. + // Otherwise we will not read anything useful about such a setting and we will also + // report a schema mismatch, even if it is written correctly. + let newProp: string = prop; + let newFullProp: string = key === "makefile" ? key + "." : ""; + while (jsonProps === undefined && newProp !== "") { + newFullProp = newFullProp + newProp; + if (propSchema.properties) { + jsonProps = Object.getOwnPropertyNames( + propSchema.properties + ).includes(newFullProp) + ? propSchema.properties[newFullProp] + : undefined; + } else { + jsonProps = Object.getOwnPropertyNames(propSchema).includes( + newFullProp + ) + ? propSchema[newFullProp] + : undefined; + } + + if (jsonProps === undefined && typeof newPropObj === "object") { + newProp = Object.getOwnPropertyNames(newPropObj)[0]; + newPropObj = newPropObj[newProp]; + newProp = "." + newProp; + } else { + newProp = ""; + } + } + } + + // The user defined a setting property wrong (example miMode instead of MIMode). + // Exceptions are 'has', 'get', 'update' and 'inspect' for the makefile root. + // They are functions and we can use this type to make the exclusion.. + if (jsonProps === undefined) { + if (typeof setting[prop] !== "function") { + telemetryLogger( + `Schema mismatch between settings and package.json for property "${key}.${prop}"` + ); + } + } else { + // Skip if the analyzed prop is a function or if it's the length of an array. + if ( + type !== "function" /*&& jsonType !== undefined*/ && + (jsonType !== "array" || prop !== "length") + ) { + let newTelemetryProperties: Properties | null = {}; + newTelemetryProperties = await analyzeSettings( + newPropObj, + key + "." + prop, + jsonProps, + ignoreDefault, + jsonType !== "array" || index === active + ? newTelemetryProperties + : null + ); + + // If telemetryProperties is null, it means we're not interested in reporting any telemetry for this subtree + if (telemetryProperties) { + telemetryProperties = util.mergeProperties( + telemetryProperties, + newTelemetryProperties + ); + } + } + } + }); + break; + + default: + break; + } + + return telemetryProperties; } function createReporter(): TelemetryReporter | null { - const packageInfo: IPackageInfo = getPackageInfo(); - if (packageInfo && packageInfo.aiKey) { - return new TelemetryReporter(packageInfo.name, packageInfo.version, packageInfo.aiKey); - } - return null; + const packageInfo: IPackageInfo = getPackageInfo(); + if (packageInfo && packageInfo.aiKey) { + return new TelemetryReporter( + packageInfo.name, + packageInfo.version, + packageInfo.aiKey + ); + } + return null; } function getPackageInfo(): IPackageInfo { - const packageJSON: util.PackageJSON = util.thisExtensionPackage(); - return { - name: `${packageJSON.publisher}.${packageJSON.name}`, - version: packageJSON.version, - aiKey: "AIF-d9b70cd4-b9f9-4d70-929b-a071c400b217" - }; + const packageJSON: util.PackageJSON = util.thisExtensionPackage(); + return { + name: `${packageJSON.publisher}.${packageJSON.name}`, + version: packageJSON.version, + aiKey: "AIF-d9b70cd4-b9f9-4d70-929b-a071c400b217", + }; } diff --git a/src/test/fakeSuite/Repros/.vscode/compile_commands.json b/src/test/fakeSuite/Repros/.vscode/compile_commands.json index 895b447..05e5c7e 100644 --- a/src/test/fakeSuite/Repros/.vscode/compile_commands.json +++ b/src/test/fakeSuite/Repros/.vscode/compile_commands.json @@ -1,7 +1,7 @@ [ - { - "command": "MyOwnFakeCompiler -I'd:\\Proj/src/test/fakeSuite/repros/\\'folder with space\\'/subfolder/' '-Id:\\Proj/src/test/fakeSuite/repros/folder' -I'..' -I'd:\\Proj/src/test/fakeSuite/repros/folder/' -I '../folder with space/' \"myfile.cpp\" -I 'd:\\Proj/src/test/fakeSuite/repros/\\\"folder1 with space\\\"/\\`folder2 with space\\`/subfolder/' /std:c++17\r", - "directory": "d:\\Proj\\vscode-makefile-tools\\src\\test\\fakeSuite\\Repros", - "file": "d:\\Proj\\vscode-makefile-tools\\src\\test\\fakeSuite\\Repros\\myfile.cpp" - } -] \ No newline at end of file + { + "command": "MyOwnFakeCompiler -I'd:\\Proj/src/test/fakeSuite/repros/\\'folder with space\\'/subfolder/' '-Id:\\Proj/src/test/fakeSuite/repros/folder' -I'..' -I'd:\\Proj/src/test/fakeSuite/repros/folder/' -I '../folder with space/' \"myfile.cpp\" -I 'd:\\Proj/src/test/fakeSuite/repros/\\\"folder1 with space\\\"/\\`folder2 with space\\`/subfolder/' /std:c++17\r", + "directory": "d:\\Proj\\vscode-makefile-tools\\src\\test\\fakeSuite\\Repros", + "file": "d:\\Proj\\vscode-makefile-tools\\src\\test\\fakeSuite\\Repros\\myfile.cpp" + } +] diff --git a/src/test/fakeSuite/Repros/.vscode/settings.json b/src/test/fakeSuite/Repros/.vscode/settings.json index c66d9a9..d318195 100644 --- a/src/test/fakeSuite/Repros/.vscode/settings.json +++ b/src/test/fakeSuite/Repros/.vscode/settings.json @@ -1,85 +1,85 @@ { - "C_Cpp.default.configurationProvider": "ms-vscode.makefile-tools", - "makefile.buildLog": "./dummy_dryrun.log", - "makefile.extensionOutputFolder": "./.vscode", - "makefile.extensionLog": "./.vscode/Makefile.out", - "makefile.makePath": "c:/some/other/fake/path", - "makefile.buildBeforeLaunch": false, - "makefile.loggingLevel": "Debug", - "makefile.additionalCompilerNames": ["MyOwnFakeCompiler"], - "makefile.compileCommandsPath": "./.vscode/compile_commands.json", - "makefile.panel.visibility": { - "debug": true + "C_Cpp.default.configurationProvider": "ms-vscode.makefile-tools", + "makefile.buildLog": "./dummy_dryrun.log", + "makefile.extensionOutputFolder": "./.vscode", + "makefile.extensionLog": "./.vscode/Makefile.out", + "makefile.makePath": "c:/some/other/fake/path", + "makefile.buildBeforeLaunch": false, + "makefile.loggingLevel": "Debug", + "makefile.additionalCompilerNames": ["MyOwnFakeCompiler"], + "makefile.compileCommandsPath": "./.vscode/compile_commands.json", + "makefile.panel.visibility": { + "debug": true + }, + "makefile.configureOnOpen": false, + "makefile.configureOnEdit": true, + "makefile.configureAfterCommand": true, + "makefile.configurations": [ + { + "name": "varexp" }, - "makefile.configureOnOpen": false, - "makefile.configureOnEdit": true, - "makefile.configureAfterCommand": true, - "makefile.configurations": [ - { - "name": "varexp", - }, - { - "name": "test-make-f", - "makePath": "./doesnt/exist/make", - "buildLog": "./doesnt_exist.log", - "makeArgs": ["-f", "./SubDir/makefile with space"] - }, - { - "name": "test-make-C", - "makePath": "./doesnt/exist/make", - "buildLog": "./doesnt_exist.log", - "makeArgs": ["-C", "./SubDir with space/"] - }, - { - "name": "complex_escaped_quotes", - "buildLog": "./complex_escaped_quotes_dryrun.log" - }, - { - "name": "complex_escaped_quotes_winOnly", - "buildLog": "./complex_escaped_quotes_winOnly_dryrun.log" - }, - { - "name": "InterestingSmallMakefile_windows_configDebug", - "buildLog": "./InterestingSmallMakefile_windows_dryrunDebug.log" - }, - { - "name": "InterestingSmallMakefile_windows_configRelSize", - "makePath": "make", - "makeArgs": ["OPT=RelSize"], - "buildLog": "./InterestingSmallMakefile_windows_dryrunRelSize.log" - }, - { - "name": "InterestingSmallMakefile_windows_configRelSpeed", - "makePath": "c:/fake/path/make.exe", - "makeArgs": ["OPT=RelSpeed"] - }, - { - "name": "8cc_linux", - "buildLog": "./8cc_linux_dryrun.log" - }, - { - "name": "8cc_mingw", - "buildLog": "./8cc_mingw_dryrun.log" - }, - { - "name": "Fido_linux", - "buildLog": "./Fido_linux_dryrun.log" - }, - { - "name": "Fido_mingw", - "buildLog": "./Fido_mingw_dryrun.log" - }, - { - "name": "tinyvm_linux_pedantic", - "buildLog": "./tinyvm_linux_dryrunPedantic.log", - "makePath": "make", - "makeArgs": ["PEDANTIC=yes"] - }, - { - "name": "tinyvm_mingw_pedantic", - "buildLog": "./tinyvm_mingw_dryrunPedantic.log", - "makePath": "make", - "makeArgs": ["PEDANTIC=yes"] - } -] -} \ No newline at end of file + { + "name": "test-make-f", + "makePath": "./doesnt/exist/make", + "buildLog": "./doesnt_exist.log", + "makeArgs": ["-f", "./SubDir/makefile with space"] + }, + { + "name": "test-make-C", + "makePath": "./doesnt/exist/make", + "buildLog": "./doesnt_exist.log", + "makeArgs": ["-C", "./SubDir with space/"] + }, + { + "name": "complex_escaped_quotes", + "buildLog": "./complex_escaped_quotes_dryrun.log" + }, + { + "name": "complex_escaped_quotes_winOnly", + "buildLog": "./complex_escaped_quotes_winOnly_dryrun.log" + }, + { + "name": "InterestingSmallMakefile_windows_configDebug", + "buildLog": "./InterestingSmallMakefile_windows_dryrunDebug.log" + }, + { + "name": "InterestingSmallMakefile_windows_configRelSize", + "makePath": "make", + "makeArgs": ["OPT=RelSize"], + "buildLog": "./InterestingSmallMakefile_windows_dryrunRelSize.log" + }, + { + "name": "InterestingSmallMakefile_windows_configRelSpeed", + "makePath": "c:/fake/path/make.exe", + "makeArgs": ["OPT=RelSpeed"] + }, + { + "name": "8cc_linux", + "buildLog": "./8cc_linux_dryrun.log" + }, + { + "name": "8cc_mingw", + "buildLog": "./8cc_mingw_dryrun.log" + }, + { + "name": "Fido_linux", + "buildLog": "./Fido_linux_dryrun.log" + }, + { + "name": "Fido_mingw", + "buildLog": "./Fido_mingw_dryrun.log" + }, + { + "name": "tinyvm_linux_pedantic", + "buildLog": "./tinyvm_linux_dryrunPedantic.log", + "makePath": "make", + "makeArgs": ["PEDANTIC=yes"] + }, + { + "name": "tinyvm_mingw_pedantic", + "buildLog": "./tinyvm_mingw_dryrunPedantic.log", + "makePath": "make", + "makeArgs": ["PEDANTIC=yes"] + } + ] +} diff --git a/src/test/fakeSuite/extension.test.ts b/src/test/fakeSuite/extension.test.ts index b99ebe4..39586cb 100644 --- a/src/test/fakeSuite/extension.test.ts +++ b/src/test/fakeSuite/extension.test.ts @@ -27,61 +27,141 @@ // that are called with arguments in the makefile. // See comment in parser.ts, parseLineAsTool and parseLaunchConfiguration. -import * as configuration from '../../configuration'; -import { expect } from 'chai'; -import * as launch from '../../launch'; -import * as make from '../../make'; -import * as util from '../../util'; -import * as fs from 'fs'; -import * as path from 'path'; -import * as vscode from 'vscode'; -import { extension } from '../../extension'; +import * as configuration from "../../configuration"; +import { expect } from "chai"; +import * as launch from "../../launch"; +import * as make from "../../make"; +import * as util from "../../util"; +import * as fs from "fs"; +import * as path from "path"; +import * as vscode from "vscode"; +import { extension } from "../../extension"; // TODO: refactor initialization and cleanup of each test -suite('Fake dryrun parsing', () => { +suite("Fake dryrun parsing", () => { + suiteSetup(async function (this: Mocha.Context) { + this.timeout(100000); - suiteSetup(async function (this: Mocha.Context) { - this.timeout(100000); + await vscode.commands.executeCommand("makefile.resetState", false); + }); - await vscode.commands.executeCommand('makefile.resetState', false); - }); + setup(async function (this: Mocha.Context) { + this.timeout(100000); - setup(async function (this: Mocha.Context) { - this.timeout(100000); + await vscode.commands.executeCommand("makefile.resetState", false); + }); - await vscode.commands.executeCommand('makefile.resetState', false); - }); + // Interesting scenarios with string paths, corner cases in defining includes/defines, + // complex configurations-targets-files associations. + // For now, this test needs to run in an environment with VS 2019. + // The output log varies depending on finding a particular VS toolset or not. + // We need to test the scenario of providing in the makefile a full path to the compiler, + // so there is no way around this. Using only compiler name and relying on path is not sufficient. + // Also, for the cases when a path (relative or full) is given to the compiler in the makefile + // and the compiler is not found there, the parser will skip over the compiler command + // (see comment in parser.ts - parseLineAsTool), so again, we need to find the toolset that is referenced in the makefile. + // TODO: mock various scenarios of VS environments without depending on what is installed. + // TODO: adapt the makefile on mac/linux/mingw and add new tests in this suite + // to parse the dry-run logs obtained on those platforms. + let systemPlatform: string; + if (process.platform === "win32") { + systemPlatform = + process.env.MSYSTEM === undefined ? "win32" : process.env.MSYSTEM; + } else { + systemPlatform = process.platform; + } - // Interesting scenarios with string paths, corner cases in defining includes/defines, - // complex configurations-targets-files associations. - // For now, this test needs to run in an environment with VS 2019. - // The output log varies depending on finding a particular VS toolset or not. - // We need to test the scenario of providing in the makefile a full path to the compiler, - // so there is no way around this. Using only compiler name and relying on path is not sufficient. - // Also, for the cases when a path (relative or full) is given to the compiler in the makefile - // and the compiler is not found there, the parser will skip over the compiler command - // (see comment in parser.ts - parseLineAsTool), so again, we need to find the toolset that is referenced in the makefile. - // TODO: mock various scenarios of VS environments without depending on what is installed. - // TODO: adapt the makefile on mac/linux/mingw and add new tests in this suite - // to parse the dry-run logs obtained on those platforms. - let systemPlatform: string; - if (process.platform === "win32") { - systemPlatform = (process.env.MSYSTEM === undefined) ? "win32" : process.env.MSYSTEM; - } else { - systemPlatform = process.platform; - } - - const ext = vscode.extensions.getExtension("ms-vscode.makefile-tools"); - if (ext) { + const ext = vscode.extensions.getExtension("ms-vscode.makefile-tools"); + if (ext) { ext.activate(); - } + } + test(`Complex scenarios with quotes and escaped quotes - ${systemPlatform}`, async () => { + // Settings reset from the previous test run. + await vscode.commands.executeCommand("makefile.resetState", false); + await vscode.workspace + .getConfiguration("makefile") + .update("launchConfigurations", undefined); + await vscode.commands.executeCommand( + "makefile.setBuildConfigurationByName", + "Default" + ); - test(`Complex scenarios with quotes and escaped quotes - ${systemPlatform}`, async () => { + // We define extension log here as opposed to in the fake repro .vscode/settings.json + // because the logging produced at the first project load has too few important data to verify and much variations + // that are not worth to be processed when comparing with a baseline. + // Example: when running a test after incomplete debugging or after loading the fake repro project independently of the testing framework, + // which leaves the workspace state not clean, resulting in a different extension output log + // than without debugging/loading the project before. + // If we define extension log here instead of .vscode/settings.json, we also have to clean it up + // because at project load time, there is no makefile log identified and no file is deleted on activation. + let extensionLogPath: string = path.join( + vscode.workspace.rootPath || "./", + ".vscode/Makefile.out" + ); + if (util.checkFileExistsSync(extensionLogPath)) { + util.deleteFileSync(extensionLogPath); + } + + // Run a preconfigure script to include our tests fake compilers path so that we always find gcc/gpp/clang/...etc... + // from this extension repository instead of a real installation which may vary from system to system. + await vscode.commands.executeCommand( + "makefile.setPreconfigureScriptByPath", + path.join( + vscode.workspace.rootPath || "./", + systemPlatform === "win32" + ? ".vscode/preconfigure.bat" + : ".vscode/preconfigure_nonwin.sh" + ) + ); + await vscode.commands.executeCommand("makefile.preConfigure"); + + await vscode.commands.executeCommand( + "makefile.setBuildConfigurationByName", + "complex_escaped_quotes" + ); + + await vscode.commands.executeCommand("makefile.cleanConfigure"); + + // Compare the output log with the baseline + // TODO: incorporate relevant diff snippets into the test log. + // Until then, print into base and diff files for easier viewing + // when the test fails. + let parsedPath: path.ParsedPath = path.parse(extensionLogPath); + let baselineLogPath: string = path.join( + parsedPath.dir, + systemPlatform === "win32" + ? "../complex_escaped_quotes_baseline.out" + : "../complex_escaped_quotes_nonWin_baseline.out" + ); + let extensionLogContent: string = util.readFile(extensionLogPath) || ""; + extensionLogContent = extensionLogContent.replace(/\r\n/gm, "\n"); + let baselineLogContent: string = util.readFile(baselineLogPath) || ""; + let extensionRootPath: string = path.resolve(__dirname, "../../../.."); + baselineLogContent = baselineLogContent.replace( + /{REPO:VSCODE-MAKEFILE-TOOLS}/gm, + extensionRootPath + ); + console.log("HEYHEYHEYHEYHEY " + extensionRootPath); + baselineLogContent = baselineLogContent.replace(/\r\n/gm, "\n"); + // fs.writeFileSync(path.join(parsedPath.dir, "base6.out"), baselineLogContent); + // fs.writeFileSync(path.join(parsedPath.dir, "diff6.out"), extensionLogContent); + + expect(extensionLogContent).to.be.equal(baselineLogContent); + }); + + if (systemPlatform === "win32") { + test(`Complex scenarios with quotes and escaped quotes - winOnly`, async () => { // Settings reset from the previous test run. await vscode.commands.executeCommand("makefile.resetState", false); - await vscode.workspace.getConfiguration("makefile").update("launchConfigurations", undefined); - await vscode.commands.executeCommand('makefile.setBuildConfigurationByName', "Default"); + await vscode.commands.executeCommand("makefile.testResetState"); + await vscode.workspace + .getConfiguration("makefile") + .update("launchConfigurations", undefined); + await vscode.commands.executeCommand( + "makefile.setBuildConfigurationByName", + "Default" + ); // We define extension log here as opposed to in the fake repro .vscode/settings.json // because the logging produced at the first project load has too few important data to verify and much variations @@ -91,424 +171,611 @@ suite('Fake dryrun parsing', () => { // than without debugging/loading the project before. // If we define extension log here instead of .vscode/settings.json, we also have to clean it up // because at project load time, there is no makefile log identified and no file is deleted on activation. - let extensionLogPath: string = path.join(vscode.workspace.rootPath || "./", ".vscode/Makefile.out"); + let extensionLogPath: string = + configuration.getExtensionLog() || + path.join(vscode.workspace.rootPath || "./", ".vscode/Makefile.out"); if (util.checkFileExistsSync(extensionLogPath)) { - util.deleteFileSync(extensionLogPath); + util.deleteFileSync(extensionLogPath); } // Run a preconfigure script to include our tests fake compilers path so that we always find gcc/gpp/clang/...etc... // from this extension repository instead of a real installation which may vary from system to system. - await vscode.commands.executeCommand('makefile.setPreconfigureScriptByPath', path.join(vscode.workspace.rootPath || "./", systemPlatform === "win32" ? ".vscode/preconfigure.bat" : ".vscode/preconfigure_nonwin.sh")); - await vscode.commands.executeCommand('makefile.preConfigure'); - await vscode.commands.executeCommand('makefile.setBuildConfigurationByName', "complex_escaped_quotes"); + await vscode.commands.executeCommand( + "makefile.setPreconfigureScriptByPath", + path.join(vscode.workspace.rootPath || "./", ".vscode/preconfigure.bat") + ); + await vscode.commands.executeCommand("makefile.preConfigure"); - await vscode.commands.executeCommand('makefile.cleanConfigure'); + await vscode.commands.executeCommand( + "makefile.setBuildConfigurationByName", + "complex_escaped_quotes_winOnly" + ); + await vscode.commands.executeCommand("makefile.cleanConfigure"); // Compare the output log with the baseline // TODO: incorporate relevant diff snippets into the test log. // Until then, print into base and diff files for easier viewing // when the test fails. let parsedPath: path.ParsedPath = path.parse(extensionLogPath); - let baselineLogPath: string = path.join(parsedPath.dir, systemPlatform === "win32" ? "../complex_escaped_quotes_baseline.out" : "../complex_escaped_quotes_nonWin_baseline.out"); + let baselineLogPath: string = path.join( + parsedPath.dir, + "../complex_escaped_quotes_winOnly_baseline.out" + ); let extensionLogContent: string = util.readFile(extensionLogPath) || ""; - extensionLogContent = extensionLogContent.replace(/\r\n/mg, "\n"); + extensionLogContent = extensionLogContent.replace(/\r\n/gm, "\n"); let baselineLogContent: string = util.readFile(baselineLogPath) || ""; - let extensionRootPath: string = path.resolve(__dirname, "../../../.."); - baselineLogContent = baselineLogContent.replace(/{REPO:VSCODE-MAKEFILE-TOOLS}/mg, extensionRootPath); - console.log("HEYHEYHEYHEYHEY " + extensionRootPath); - baselineLogContent = baselineLogContent.replace(/\r\n/mg, "\n"); - // fs.writeFileSync(path.join(parsedPath.dir, "base6.out"), baselineLogContent); - // fs.writeFileSync(path.join(parsedPath.dir, "diff6.out"), extensionLogContent); + let extensionRootPath: string = path.resolve(__dirname, "../../../../"); + baselineLogContent = baselineLogContent.replace( + /{REPO:VSCODE-MAKEFILE-TOOLS}/gm, + extensionRootPath + ); + baselineLogContent = baselineLogContent.replace(/\r\n/gm, "\n"); + // fs.writeFileSync(path.join(parsedPath.dir, "base.out"), baselineLogContent); + // fs.writeFileSync(path.join(parsedPath.dir, "diff.out"), extensionLogContent); expect(extensionLogContent).to.be.equal(baselineLogContent); - }); + }); + } - if (systemPlatform === "win32") { - test(`Complex scenarios with quotes and escaped quotes - winOnly`, async () => { - // Settings reset from the previous test run. - await vscode.commands.executeCommand('makefile.resetState', false); - await vscode.commands.executeCommand("makefile.testResetState"); - await vscode.workspace.getConfiguration("makefile").update("launchConfigurations", undefined); - await vscode.commands.executeCommand('makefile.setBuildConfigurationByName', "Default"); - - // We define extension log here as opposed to in the fake repro .vscode/settings.json - // because the logging produced at the first project load has too few important data to verify and much variations - // that are not worth to be processed when comparing with a baseline. - // Example: when running a test after incomplete debugging or after loading the fake repro project independently of the testing framework, - // which leaves the workspace state not clean, resulting in a different extension output log - // than without debugging/loading the project before. - // If we define extension log here instead of .vscode/settings.json, we also have to clean it up - // because at project load time, there is no makefile log identified and no file is deleted on activation. - let extensionLogPath: string = configuration.getExtensionLog() || path.join(vscode.workspace.rootPath || "./", ".vscode/Makefile.out"); - if (util.checkFileExistsSync(extensionLogPath)) { - util.deleteFileSync(extensionLogPath); - } - - // Run a preconfigure script to include our tests fake compilers path so that we always find gcc/gpp/clang/...etc... - // from this extension repository instead of a real installation which may vary from system to system. - - await vscode.commands.executeCommand( - "makefile.setPreconfigureScriptByPath", - path.join( - vscode.workspace.rootPath || "./", - ".vscode/preconfigure.bat" - ) - ); - await vscode.commands.executeCommand('makefile.preConfigure'); - - await vscode.commands.executeCommand('makefile.setBuildConfigurationByName', "complex_escaped_quotes_winOnly"); - await vscode.commands.executeCommand('makefile.cleanConfigure'); - - // Compare the output log with the baseline - // TODO: incorporate relevant diff snippets into the test log. - // Until then, print into base and diff files for easier viewing - // when the test fails. - let parsedPath: path.ParsedPath = path.parse(extensionLogPath); - let baselineLogPath: string = path.join(parsedPath.dir, "../complex_escaped_quotes_winOnly_baseline.out"); - let extensionLogContent: string = util.readFile(extensionLogPath) || ""; - extensionLogContent = extensionLogContent.replace(/\r\n/mg, "\n"); - let baselineLogContent: string = util.readFile(baselineLogPath) || ""; - let extensionRootPath: string = path.resolve(__dirname, "../../../../"); - baselineLogContent = baselineLogContent.replace(/{REPO:VSCODE-MAKEFILE-TOOLS}/mg, extensionRootPath); - baselineLogContent = baselineLogContent.replace(/\r\n/mg, "\n"); - // fs.writeFileSync(path.join(parsedPath.dir, "base.out"), baselineLogContent); - // fs.writeFileSync(path.join(parsedPath.dir, "diff.out"), extensionLogContent); - - expect(extensionLogContent).to.be.equal(baselineLogContent); - }); - } - - if (systemPlatform === "win32") { - test('Interesting small makefile - windows', async () => { - // Settings reset from the previous test run. - await vscode.commands.executeCommand('makefile.resetState', false); - await vscode.commands.executeCommand("makefile.testResetState"); - await vscode.workspace.getConfiguration("makefile").update("launchConfigurations", undefined); - await vscode.commands.executeCommand('makefile.setBuildConfigurationByName', "Default"); - - // Extension log is defined in the test .vscode/settings.json but delete it now - // because we are interested to compare against a baseline from this point further. - let extensionLogPath: string = path.join(vscode.workspace.rootPath || "./", ".vscode/Makefile.out"); - if (extensionLogPath && util.checkFileExistsSync(extensionLogPath)) { - util.deleteFileSync(extensionLogPath); - } - - // Run a preconfigure script to include our tests "Program Files" path so that we always find a cl.exe - // from this extension repository instead of a real VS installation that happens to be in the path. - await vscode.commands.executeCommand('makefile.setPreconfigureScriptByPath', path.join(vscode.workspace.rootPath || "./", ".vscode/preconfigure.bat")); - await vscode.commands.executeCommand('makefile.preConfigure'); - - await vscode.commands.executeCommand('makefile.setBuildConfigurationByName', "InterestingSmallMakefile_windows_configDebug"); - await vscode.commands.executeCommand('makefile.cleanConfigure'); - - const launchConfigurations: string[] = ["bin\\InterestingSmallMakefile\\ARC H3\\Debug\\main.exe(str3a,str3b,str3c)", - "bin\\InterestingSmallMakefile\\arch1\\Debug\\main.exe(str3a,str3b,str3c)", - "bin\\InterestingSmallMakefile\\arch2\\Debug\\main.exe()"]; - for (const config of launchConfigurations) { - await vscode.commands.executeCommand('makefile.setLaunchConfigurationByName', vscode.workspace.rootPath + ">" + config); - let status: string = await vscode.commands.executeCommand('makefile.validateLaunchConfiguration'); - let launchConfiguration: configuration.LaunchConfiguration | undefined; - if (status === launch.LaunchStatuses.success) { - launchConfiguration = await vscode.commands.executeCommand('makefile.getCurrentLaunchConfiguration'); - } - - if (launchConfiguration) { - await vscode.commands.executeCommand('makefile.prepareDebugAndRunCurrentTarget', launchConfiguration); - } - } - - // A bit more coverage, "RelSize" and "RelSpeed" are set up - // to exercise different combinations of pre-created build log and/or make tools. - // No configure is necessary to be run here, it is enough to look at what happens - // when changing a configuration. - await vscode.commands.executeCommand('makefile.setBuildConfigurationByName', "InterestingSmallMakefile_windows_configRelSize"); - await vscode.commands.executeCommand('makefile.setBuildConfigurationByName', "InterestingSmallMakefile_windows_configRelSpeed"); - - // InterestingSmallMakefile_windows_configRelSpeed constructs a more interesting build command. - await vscode.commands.executeCommand('makefile.setTargetByName', "Execute_Arch3"); - await vscode.commands.executeCommand('makefile.prepareBuildTarget', "Execute_Arch3") - - await vscode.commands.executeCommand("makefile.resetState", false); - await vscode.workspace.getConfiguration("makefile").update("launchConfigurations", undefined); - - // Compare the output log with the baseline - // TODO: incorporate relevant diff snippets into the test log. - // Until then, print into base and diff files for easier viewing - // when the test fails. - let parsedPath: path.ParsedPath = path.parse(extensionLogPath); - let baselineLogPath: string = path.join(parsedPath.dir, "../InterestingSmallMakefile_windows_baseline.out"); - let extensionLogContent: string = util.readFile(extensionLogPath) || ""; - extensionLogContent = extensionLogContent.replace(/\r\n/mg, "\n"); - let baselineLogContent: string = util.readFile(baselineLogPath) || ""; - let extensionRootPath: string = path.resolve(__dirname, "../../../../"); - baselineLogContent = baselineLogContent.replace(/{REPO:VSCODE-MAKEFILE-TOOLS}/mg, extensionRootPath); - baselineLogContent = baselineLogContent.replace(/\r\n/mg, "\n"); - // fs.writeFileSync(path.join(parsedPath.dir, "base.out"), baselineLogContent); - // fs.writeFileSync(path.join(parsedPath.dir, "diff.out"), extensionLogContent); - expect(extensionLogContent).to.be.equal(baselineLogContent); - }); - } - - // dry-run logs for https://github.com/rui314/8cc.git - if (systemPlatform === "linux" || systemPlatform === "mingw") { - test(`8cc - ${systemPlatform}`, async () => { - // Settings reset from the previous test run. - await vscode.commands.executeCommand('makefile.resetState', false); - await vscode.commands.executeCommand("makefile.testResetState"); - await vscode.workspace.getConfiguration("makefile").update("launchConfigurations", undefined); - await vscode.commands.executeCommand('makefile.setBuildConfigurationByName', "Default"); - - // Extension log is defined in the test .vscode/settings.json but delete it now - // because we are interested to compare against a baseline from this point further. - let extensionLogPath: string = path.join(vscode.workspace.rootPath || "./", ".vscode/Makefile.out"); - if (extensionLogPath && util.checkFileExistsSync(extensionLogPath)) { - util.deleteFileSync(extensionLogPath); - } - - // Run a preconfigure script to include our tests fake compilers path so that we always find gcc/gpp/clang/...etc... - // from this extension repository instead of a real installation which may vary from system to system. - await vscode.commands.executeCommand('makefile.setPreconfigureScriptByPath', path.join(vscode.workspace.rootPath || "./", ".vscode/preconfigure_nonwin.sh")); - await vscode.commands.executeCommand('makefile.preConfigure'); - - await vscode.commands.executeCommand('makefile.setBuildConfigurationByName', process.platform === "linux" ? "8cc_linux" : "8cc_mingw"); - await vscode.commands.executeCommand('makefile.cleanConfigure'); - - const launchConfigurations: string[] = ["8cc()"]; - for (const config of launchConfigurations) { - await vscode.commands.executeCommand('makefile.setLaunchConfigurationByName', vscode.workspace.rootPath + ">" + config); - let status: string = await vscode.commands.executeCommand('makefile.validateLaunchConfiguration'); - let launchConfiguration: configuration.LaunchConfiguration | undefined; - if (status === launch.LaunchStatuses.success) { - launchConfiguration = await vscode.commands.executeCommand('makefile.getCurrentLaunchConfiguration'); - } - - if (launchConfiguration) { - await vscode.commands.executeCommand('makefile.prepareDebugAndRunCurrentTarget', launchConfiguration); - } - } - - await vscode.commands.executeCommand('makefile.setTargetByName', "all"); - await vscode.commands.executeCommand('makefile.prepareBuildTarget', "all"); - - // Compare the output log with the baseline - // TODO: incorporate relevant diff snippets into the test log. - // Until then, print into base and diff files for easier viewing - // when the test fails. - let parsedPath: path.ParsedPath = path.parse(extensionLogPath); - let baselineLogPath: string = path.join(parsedPath.dir, process.platform === "linux" ? "../8cc_linux_baseline.out" : "../8cc_mingw_baseline.out"); - let extensionLogContent: string = util.readFile(extensionLogPath) || ""; - let baselineLogContent: string = util.readFile(baselineLogPath) || ""; - let extensionRootPath: string = path.resolve(__dirname, "../../../../"); - baselineLogContent = baselineLogContent.replace(/{REPO:VSCODE-MAKEFILE-TOOLS}/mg, extensionRootPath); - // fs.writeFileSync(path.join(parsedPath.dir, "base5.out"), baselineLogContent); - // fs.writeFileSync(path.join(parsedPath.dir, "diff5.out"), extensionLogContent); - expect(extensionLogContent).to.be.equal(baselineLogContent); - }); - } - - // dry-run logs for https://github.com/FidoProject/Fido.git - if (systemPlatform === "linux" || systemPlatform === "mingw") { - test(`Fido - ${systemPlatform}`, async () => { - // Settings reset from the previous test run. - await vscode.commands.executeCommand('makefile.resetState', false); - await vscode.commands.executeCommand("makefile.testResetState"); - await vscode.workspace.getConfiguration("makefile").update("launchConfigurations", undefined); - await vscode.commands.executeCommand('makefile.setBuildConfigurationByName', "Default"); - - // Extension log is defined in the test .vscode/settings.json but delete it now - // because we are interested to compare against a baseline from this point further. - let extensionLogPath: string = path.join(vscode.workspace.rootPath || "./", ".vscode/Makefile.out"); - if (extensionLogPath && util.checkFileExistsSync(extensionLogPath)) { - util.deleteFileSync(extensionLogPath); - } - - // Run a preconfigure script to include our tests fake compilers path so that we always find gcc/gpp/clang/...etc... - // from this extension repository instead of a real installation which may vary from system to system. - await vscode.commands.executeCommand('makefile.setPreconfigureScriptByPath', path.join(vscode.workspace.rootPath || "./", ".vscode/preconfigure_nonwin.sh")); - await vscode.commands.executeCommand('makefile.preConfigure'); - - await vscode.commands.executeCommand('makefile.setBuildConfigurationByName', process.platform === "linux" ? "Fido_linux" : "Fido_mingw"); - await vscode.commands.executeCommand('makefile.cleanConfigure'); - - const launchConfigurations: string[] = ["bin/foo.o()"]; - for (const config of launchConfigurations) { - await vscode.commands.executeCommand('makefile.setLaunchConfigurationByName', vscode.workspace.rootPath + ">" + config); - let status: string = await vscode.commands.executeCommand('makefile.validateLaunchConfiguration'); - let launchConfiguration: configuration.LaunchConfiguration | undefined; - if (status === launch.LaunchStatuses.success) { - launchConfiguration = await vscode.commands.executeCommand('makefile.getCurrentLaunchConfiguration'); - } - - if (launchConfiguration) { - await vscode.commands.executeCommand('makefile.prepareDebugAndRunCurrentTarget', launchConfiguration); - } - } - - await vscode.commands.executeCommand('makefile.setTargetByName', "bin/foo.o"); - await vscode.commands.executeCommand('makefile.prepareBuildTarget', "bin/foo.o"); - - // Compare the output log with the baseline - // TODO: incorporate relevant diff snippets into the test log. - // Until then, print into base and diff files for easier viewing - // when the test fails. - let parsedPath: path.ParsedPath = path.parse(extensionLogPath); - let baselineLogPath: string = path.join(parsedPath.dir, process.platform === "linux" ? "../Fido_linux_baseline.out" : "../Fido_mingw_baseline.out"); - let extensionLogContent: string = util.readFile(extensionLogPath) || ""; - let baselineLogContent: string = util.readFile(baselineLogPath) || ""; - let extensionRootPath: string = path.resolve(__dirname, "../../../../"); - baselineLogContent = baselineLogContent.replace(/{REPO:VSCODE-MAKEFILE-TOOLS}/mg, extensionRootPath); - // fs.writeFileSync(path.join(parsedPath.dir, "base4.out"), baselineLogContent); - // fs.writeFileSync(path.join(parsedPath.dir, "diff4.out"), extensionLogContent); - expect(extensionLogContent).to.be.equal(baselineLogContent); - }); - } - - // dry-run logs for https://github.com/jakogut/tinyvm.git - if (systemPlatform === "linux" || systemPlatform === "mingw") { - test(`tinyvm - ${systemPlatform}`, async () => { - // Settings reset from the previous test run. - await vscode.commands.executeCommand('makefile.resetState', false); - await vscode.commands.executeCommand("makefile.testResetState"); - await vscode.workspace.getConfiguration("makefile").update("launchConfigurations", undefined); - await vscode.commands.executeCommand('makefile.setBuildConfigurationByName', "Default"); - - // Extension log is defined in the test .vscode/settings.json but delete it now - // because we are interested to compare against a baseline from this point further. - let extensionLogPath: string = path.join(vscode.workspace.rootPath || "./", ".vscode/Makefile.out"); - if (extensionLogPath && util.checkFileExistsSync(extensionLogPath)) { - util.deleteFileSync(extensionLogPath); - } - - // Run a preconfigure script to include our tests fake compilers path so that we always find gcc/gpp/clang/...etc... - // from this extension repository instead of a real installation which may vary from system to system. - await vscode.commands.executeCommand('makefile.setPreconfigureScriptByPath', path.join(vscode.workspace.rootPath || "./", ".vscode/preconfigure_nonwin.sh")); - await vscode.commands.executeCommand('makefile.preConfigure'); - - await vscode.commands.executeCommand('makefile.setBuildConfigurationByName', process.platform === "linux" ? "tinyvm_linux_pedantic" : "tinyvm_mingw_pedantic"); - await vscode.commands.executeCommand('makefile.cleanConfigure'); - - const launchConfigurations: string[] = ["bin/tvmi()"]; - for (const config of launchConfigurations) { - await vscode.commands.executeCommand('makefile.setLaunchConfigurationByName', vscode.workspace.rootPath + ">" + config); - let status: string = await vscode.commands.executeCommand('makefile.validateLaunchConfiguration'); - let launchConfiguration: configuration.LaunchConfiguration | undefined; - if (status === launch.LaunchStatuses.success) { - launchConfiguration = await vscode.commands.executeCommand('makefile.getCurrentLaunchConfiguration'); - } - - if (launchConfiguration) { - await vscode.commands.executeCommand('makefile.prepareDebugAndRunCurrentTarget', launchConfiguration); - } - } - - await vscode.commands.executeCommand('makefile.setTargetByName', "tvmi"); - await vscode.commands.executeCommand('makefile.prepareBuildTarget', "tvmi"); - - // Compare the output log with the baseline - // TODO: incorporate relevant diff snippets into the test log. - // Until then, print into base and diff files for easier viewing - // when the test fails. - let parsedPath: path.ParsedPath = path.parse(extensionLogPath); - let baselineLogPath: string = path.join(parsedPath.dir, process.platform === "linux" ? "../tinyvm_linux_baseline.out" : "../tinyvm_mingw_baseline.out"); - let extensionLogContent: string = util.readFile(extensionLogPath) || ""; - let baselineLogContent: string = util.readFile(baselineLogPath) || ""; - let extensionRootPath: string = path.resolve(__dirname, "../../../../"); - baselineLogContent = baselineLogContent.replace(/{REPO:VSCODE-MAKEFILE-TOOLS}/mg, extensionRootPath); - // fs.writeFileSync(path.join(parsedPath.dir, "base3.out"), baselineLogContent); - // fs.writeFileSync(path.join(parsedPath.dir, "diff3.out"), extensionLogContent); - expect(extensionLogContent).to.be.equal(baselineLogContent); - }); - } - - test(`Test real make - ${systemPlatform}`, async () => { + if (systemPlatform === "win32") { + test("Interesting small makefile - windows", async () => { // Settings reset from the previous test run. - await vscode.commands.executeCommand('makefile.resetState', false); - await vscode.commands.executeCommand('makefile.testResetState'); - await vscode.workspace.getConfiguration("makefile").update("launchConfigurations", undefined); - await vscode.commands.executeCommand('makefile.setBuildConfigurationByName', "Default"); + await vscode.commands.executeCommand("makefile.resetState", false); + await vscode.commands.executeCommand("makefile.testResetState"); + await vscode.workspace + .getConfiguration("makefile") + .update("launchConfigurations", undefined); + await vscode.commands.executeCommand( + "makefile.setBuildConfigurationByName", + "Default" + ); // Extension log is defined in the test .vscode/settings.json but delete it now // because we are interested to compare against a baseline from this point further. - let extensionLogPath: string = path.join(vscode.workspace.rootPath || "./", ".vscode/Makefile.out"); + let extensionLogPath: string = path.join( + vscode.workspace.rootPath || "./", + ".vscode/Makefile.out" + ); if (extensionLogPath && util.checkFileExistsSync(extensionLogPath)) { - util.deleteFileSync(extensionLogPath); + util.deleteFileSync(extensionLogPath); } - await vscode.commands.executeCommand('makefile.setBuildConfigurationByName', "test-make-f"); - await vscode.commands.executeCommand('makefile.cleanConfigure'); + // Run a preconfigure script to include our tests "Program Files" path so that we always find a cl.exe + // from this extension repository instead of a real VS installation that happens to be in the path. + await vscode.commands.executeCommand( + "makefile.setPreconfigureScriptByPath", + path.join(vscode.workspace.rootPath || "./", ".vscode/preconfigure.bat") + ); + await vscode.commands.executeCommand("makefile.preConfigure"); - await vscode.commands.executeCommand('makefile.setBuildConfigurationByName', "test-make-C"); - await vscode.commands.executeCommand('makefile.buildCleanAll'); + await vscode.commands.executeCommand( + "makefile.setBuildConfigurationByName", + "InterestingSmallMakefile_windows_configDebug" + ); + await vscode.commands.executeCommand("makefile.cleanConfigure"); + + const launchConfigurations: string[] = [ + "bin\\InterestingSmallMakefile\\ARC H3\\Debug\\main.exe(str3a,str3b,str3c)", + "bin\\InterestingSmallMakefile\\arch1\\Debug\\main.exe(str3a,str3b,str3c)", + "bin\\InterestingSmallMakefile\\arch2\\Debug\\main.exe()", + ]; + for (const config of launchConfigurations) { + await vscode.commands.executeCommand( + "makefile.setLaunchConfigurationByName", + vscode.workspace.rootPath + ">" + config + ); + let status: string = await vscode.commands.executeCommand( + "makefile.validateLaunchConfiguration" + ); + let launchConfiguration: configuration.LaunchConfiguration | undefined; + if (status === launch.LaunchStatuses.success) { + launchConfiguration = await vscode.commands.executeCommand( + "makefile.getCurrentLaunchConfiguration" + ); + } + + if (launchConfiguration) { + await vscode.commands.executeCommand( + "makefile.prepareDebugAndRunCurrentTarget", + launchConfiguration + ); + } + } + + // A bit more coverage, "RelSize" and "RelSpeed" are set up + // to exercise different combinations of pre-created build log and/or make tools. + // No configure is necessary to be run here, it is enough to look at what happens + // when changing a configuration. + await vscode.commands.executeCommand( + "makefile.setBuildConfigurationByName", + "InterestingSmallMakefile_windows_configRelSize" + ); + await vscode.commands.executeCommand( + "makefile.setBuildConfigurationByName", + "InterestingSmallMakefile_windows_configRelSpeed" + ); + + // InterestingSmallMakefile_windows_configRelSpeed constructs a more interesting build command. + await vscode.commands.executeCommand( + "makefile.setTargetByName", + "Execute_Arch3" + ); + await vscode.commands.executeCommand( + "makefile.prepareBuildTarget", + "Execute_Arch3" + ); + + await vscode.commands.executeCommand("makefile.resetState", false); + await vscode.workspace + .getConfiguration("makefile") + .update("launchConfigurations", undefined); // Compare the output log with the baseline // TODO: incorporate relevant diff snippets into the test log. // Until then, print into base and diff files for easier viewing // when the test fails. let parsedPath: path.ParsedPath = path.parse(extensionLogPath); - let baselineLogPath: string = path.join(parsedPath.dir, process.platform === "win32" ? "../test_real_make_windows_baseline.out" : "../test_real_make_nonWin_baseline.out"); + let baselineLogPath: string = path.join( + parsedPath.dir, + "../InterestingSmallMakefile_windows_baseline.out" + ); let extensionLogContent: string = util.readFile(extensionLogPath) || ""; - extensionLogContent = extensionLogContent.replace(/\r\n/mg, "\n"); + extensionLogContent = extensionLogContent.replace(/\r\n/gm, "\n"); let baselineLogContent: string = util.readFile(baselineLogPath) || ""; let extensionRootPath: string = path.resolve(__dirname, "../../../../"); - baselineLogContent = baselineLogContent.replace(/{REPO:VSCODE-MAKEFILE-TOOLS}/mg, extensionRootPath); - baselineLogContent = baselineLogContent.replace(/\r\n/mg, "\n"); - // fs.writeFileSync(path.join(parsedPath.dir, "base2.out"), baselineLogContent); - // fs.writeFileSync(path.join(parsedPath.dir, "diff2.out"), extensionLogContent); + baselineLogContent = baselineLogContent.replace( + /{REPO:VSCODE-MAKEFILE-TOOLS}/gm, + extensionRootPath + ); + baselineLogContent = baselineLogContent.replace(/\r\n/gm, "\n"); + // fs.writeFileSync(path.join(parsedPath.dir, "base.out"), baselineLogContent); + // fs.writeFileSync(path.join(parsedPath.dir, "diff.out"), extensionLogContent); expect(extensionLogContent).to.be.equal(baselineLogContent); - }); + }); + } - test(`Variables expansion - ${systemPlatform}`, async () => { + // dry-run logs for https://github.com/rui314/8cc.git + if (systemPlatform === "linux" || systemPlatform === "mingw") { + test(`8cc - ${systemPlatform}`, async () => { // Settings reset from the previous test run. - await vscode.commands.executeCommand('makefile.resetState', false); + await vscode.commands.executeCommand("makefile.resetState", false); await vscode.commands.executeCommand("makefile.testResetState"); - await vscode.workspace.getConfiguration("makefile").update("launchConfigurations", undefined); - await vscode.commands.executeCommand('makefile.setBuildConfigurationByName', "Default"); + await vscode.workspace + .getConfiguration("makefile") + .update("launchConfigurations", undefined); + await vscode.commands.executeCommand( + "makefile.setBuildConfigurationByName", + "Default" + ); - await vscode.commands.executeCommand('makefile.setBuildConfigurationByName', "varexp"); - await vscode.commands.executeCommand('makefile.setTargetByName', "all"); - - - // Delete extension log a bit later than other tests. For this one, we only care to capture varexp. - // All else that happens before, it was covered during the other tests in this suite. - let extensionLogPath: string = path.join(vscode.workspace.rootPath || "./", ".vscode/Makefile.out"); + // Extension log is defined in the test .vscode/settings.json but delete it now + // because we are interested to compare against a baseline from this point further. + let extensionLogPath: string = path.join( + vscode.workspace.rootPath || "./", + ".vscode/Makefile.out" + ); if (extensionLogPath && util.checkFileExistsSync(extensionLogPath)) { - util.deleteFileSync(extensionLogPath); + util.deleteFileSync(extensionLogPath); } - await vscode.commands.executeCommand('makefile.getExpandedSettingValue', "buildLog", "./${workspaceFolder}/${configuration}/${buildTarget}/something/${configuration}/${buildTarget}/build.log"); + // Run a preconfigure script to include our tests fake compilers path so that we always find gcc/gpp/clang/...etc... + // from this extension repository instead of a real installation which may vary from system to system. + await vscode.commands.executeCommand( + "makefile.setPreconfigureScriptByPath", + path.join( + vscode.workspace.rootPath || "./", + ".vscode/preconfigure_nonwin.sh" + ) + ); + await vscode.commands.executeCommand("makefile.preConfigure"); - let stopAtEntry: string = await vscode.commands.executeCommand('makefile.expandVariablesInSetting', 'defaultLaunchConfiguration.stopAtEntry', "${config:makefile.panel.visibility.debug}"); - let tmpDefaultLaunchConfiguration: configuration.DefaultLaunchConfiguration = { - miDebuggerPath: "./${workspaceRoot}/${command:makefile.getConfiguration}/${command:makefile.getBuildTarget}", - stopAtEntry: util.booleanify(stopAtEntry) - }; - await vscode.commands.executeCommand('makefile.getExpandedSettingValue', "defaultLaunchConfiguration", tmpDefaultLaunchConfiguration); + await vscode.commands.executeCommand( + "makefile.setBuildConfigurationByName", + process.platform === "linux" ? "8cc_linux" : "8cc_mingw" + ); + await vscode.commands.executeCommand("makefile.cleanConfigure"); - let tmpConfigurations: configuration.MakefileConfiguration[] = [{ - name: "MyTmpName", - makePath: "${env:ProgramFiles(x86)}/${workspaceFolderBasename}/make", - makeArgs: ["${command:makefile.getLaunchTargetPath}", - "${SomeUnsupportedVar}", - "try_\\${escape_varexp1}_various_\\${escape_varexp2}_escapes", - "${command:makefile.inexistentCommand}", - "${config:makefile.inexistentSetting}"]}]; - await vscode.commands.executeCommand('makefile.getExpandedSettingValue', "configurations", tmpConfigurations); + const launchConfigurations: string[] = ["8cc()"]; + for (const config of launchConfigurations) { + await vscode.commands.executeCommand( + "makefile.setLaunchConfigurationByName", + vscode.workspace.rootPath + ">" + config + ); + let status: string = await vscode.commands.executeCommand( + "makefile.validateLaunchConfiguration" + ); + let launchConfiguration: configuration.LaunchConfiguration | undefined; + if (status === launch.LaunchStatuses.success) { + launchConfiguration = await vscode.commands.executeCommand( + "makefile.getCurrentLaunchConfiguration" + ); + } + + if (launchConfiguration) { + await vscode.commands.executeCommand( + "makefile.prepareDebugAndRunCurrentTarget", + launchConfiguration + ); + } + } + + await vscode.commands.executeCommand("makefile.setTargetByName", "all"); + await vscode.commands.executeCommand( + "makefile.prepareBuildTarget", + "all" + ); // Compare the output log with the baseline // TODO: incorporate relevant diff snippets into the test log. // Until then, print into base and diff files for easier viewing // when the test fails. let parsedPath: path.ParsedPath = path.parse(extensionLogPath); - let baselineLogPath: string = path.join(parsedPath.dir, process.platform === "win32" ? "../varexp_win32_baseline.out" : "../varexp_baseline.out"); + let baselineLogPath: string = path.join( + parsedPath.dir, + process.platform === "linux" + ? "../8cc_linux_baseline.out" + : "../8cc_mingw_baseline.out" + ); let extensionLogContent: string = util.readFile(extensionLogPath) || ""; - extensionLogContent = extensionLogContent.replace(/\r\n/mg, "\n"); let baselineLogContent: string = util.readFile(baselineLogPath) || ""; let extensionRootPath: string = path.resolve(__dirname, "../../../../"); - baselineLogContent = baselineLogContent.replace(/{REPO:VSCODE-MAKEFILE-TOOLS}/mg, extensionRootPath); - baselineLogContent = baselineLogContent.replace(/\r\n/mg, "\n"); - // fs.writeFileSync(path.join(parsedPath.dir, "base1.out"), baselineLogContent); - // fs.writeFileSync(path.join(parsedPath.dir, "diff1.out"), extensionLogContent); + baselineLogContent = baselineLogContent.replace( + /{REPO:VSCODE-MAKEFILE-TOOLS}/gm, + extensionRootPath + ); + // fs.writeFileSync(path.join(parsedPath.dir, "base5.out"), baselineLogContent); + // fs.writeFileSync(path.join(parsedPath.dir, "diff5.out"), extensionLogContent); expect(extensionLogContent).to.be.equal(baselineLogContent); - }); + }); + } + + // dry-run logs for https://github.com/FidoProject/Fido.git + if (systemPlatform === "linux" || systemPlatform === "mingw") { + test(`Fido - ${systemPlatform}`, async () => { + // Settings reset from the previous test run. + await vscode.commands.executeCommand("makefile.resetState", false); + await vscode.commands.executeCommand("makefile.testResetState"); + await vscode.workspace + .getConfiguration("makefile") + .update("launchConfigurations", undefined); + await vscode.commands.executeCommand( + "makefile.setBuildConfigurationByName", + "Default" + ); + + // Extension log is defined in the test .vscode/settings.json but delete it now + // because we are interested to compare against a baseline from this point further. + let extensionLogPath: string = path.join( + vscode.workspace.rootPath || "./", + ".vscode/Makefile.out" + ); + if (extensionLogPath && util.checkFileExistsSync(extensionLogPath)) { + util.deleteFileSync(extensionLogPath); + } + + // Run a preconfigure script to include our tests fake compilers path so that we always find gcc/gpp/clang/...etc... + // from this extension repository instead of a real installation which may vary from system to system. + await vscode.commands.executeCommand( + "makefile.setPreconfigureScriptByPath", + path.join( + vscode.workspace.rootPath || "./", + ".vscode/preconfigure_nonwin.sh" + ) + ); + await vscode.commands.executeCommand("makefile.preConfigure"); + + await vscode.commands.executeCommand( + "makefile.setBuildConfigurationByName", + process.platform === "linux" ? "Fido_linux" : "Fido_mingw" + ); + await vscode.commands.executeCommand("makefile.cleanConfigure"); + + const launchConfigurations: string[] = ["bin/foo.o()"]; + for (const config of launchConfigurations) { + await vscode.commands.executeCommand( + "makefile.setLaunchConfigurationByName", + vscode.workspace.rootPath + ">" + config + ); + let status: string = await vscode.commands.executeCommand( + "makefile.validateLaunchConfiguration" + ); + let launchConfiguration: configuration.LaunchConfiguration | undefined; + if (status === launch.LaunchStatuses.success) { + launchConfiguration = await vscode.commands.executeCommand( + "makefile.getCurrentLaunchConfiguration" + ); + } + + if (launchConfiguration) { + await vscode.commands.executeCommand( + "makefile.prepareDebugAndRunCurrentTarget", + launchConfiguration + ); + } + } + + await vscode.commands.executeCommand( + "makefile.setTargetByName", + "bin/foo.o" + ); + await vscode.commands.executeCommand( + "makefile.prepareBuildTarget", + "bin/foo.o" + ); + + // Compare the output log with the baseline + // TODO: incorporate relevant diff snippets into the test log. + // Until then, print into base and diff files for easier viewing + // when the test fails. + let parsedPath: path.ParsedPath = path.parse(extensionLogPath); + let baselineLogPath: string = path.join( + parsedPath.dir, + process.platform === "linux" + ? "../Fido_linux_baseline.out" + : "../Fido_mingw_baseline.out" + ); + let extensionLogContent: string = util.readFile(extensionLogPath) || ""; + let baselineLogContent: string = util.readFile(baselineLogPath) || ""; + let extensionRootPath: string = path.resolve(__dirname, "../../../../"); + baselineLogContent = baselineLogContent.replace( + /{REPO:VSCODE-MAKEFILE-TOOLS}/gm, + extensionRootPath + ); + // fs.writeFileSync(path.join(parsedPath.dir, "base4.out"), baselineLogContent); + // fs.writeFileSync(path.join(parsedPath.dir, "diff4.out"), extensionLogContent); + expect(extensionLogContent).to.be.equal(baselineLogContent); + }); + } + + // dry-run logs for https://github.com/jakogut/tinyvm.git + if (systemPlatform === "linux" || systemPlatform === "mingw") { + test(`tinyvm - ${systemPlatform}`, async () => { + // Settings reset from the previous test run. + await vscode.commands.executeCommand("makefile.resetState", false); + await vscode.commands.executeCommand("makefile.testResetState"); + await vscode.workspace + .getConfiguration("makefile") + .update("launchConfigurations", undefined); + await vscode.commands.executeCommand( + "makefile.setBuildConfigurationByName", + "Default" + ); + + // Extension log is defined in the test .vscode/settings.json but delete it now + // because we are interested to compare against a baseline from this point further. + let extensionLogPath: string = path.join( + vscode.workspace.rootPath || "./", + ".vscode/Makefile.out" + ); + if (extensionLogPath && util.checkFileExistsSync(extensionLogPath)) { + util.deleteFileSync(extensionLogPath); + } + + // Run a preconfigure script to include our tests fake compilers path so that we always find gcc/gpp/clang/...etc... + // from this extension repository instead of a real installation which may vary from system to system. + await vscode.commands.executeCommand( + "makefile.setPreconfigureScriptByPath", + path.join( + vscode.workspace.rootPath || "./", + ".vscode/preconfigure_nonwin.sh" + ) + ); + await vscode.commands.executeCommand("makefile.preConfigure"); + + await vscode.commands.executeCommand( + "makefile.setBuildConfigurationByName", + process.platform === "linux" + ? "tinyvm_linux_pedantic" + : "tinyvm_mingw_pedantic" + ); + await vscode.commands.executeCommand("makefile.cleanConfigure"); + + const launchConfigurations: string[] = ["bin/tvmi()"]; + for (const config of launchConfigurations) { + await vscode.commands.executeCommand( + "makefile.setLaunchConfigurationByName", + vscode.workspace.rootPath + ">" + config + ); + let status: string = await vscode.commands.executeCommand( + "makefile.validateLaunchConfiguration" + ); + let launchConfiguration: configuration.LaunchConfiguration | undefined; + if (status === launch.LaunchStatuses.success) { + launchConfiguration = await vscode.commands.executeCommand( + "makefile.getCurrentLaunchConfiguration" + ); + } + + if (launchConfiguration) { + await vscode.commands.executeCommand( + "makefile.prepareDebugAndRunCurrentTarget", + launchConfiguration + ); + } + } + + await vscode.commands.executeCommand("makefile.setTargetByName", "tvmi"); + await vscode.commands.executeCommand( + "makefile.prepareBuildTarget", + "tvmi" + ); + + // Compare the output log with the baseline + // TODO: incorporate relevant diff snippets into the test log. + // Until then, print into base and diff files for easier viewing + // when the test fails. + let parsedPath: path.ParsedPath = path.parse(extensionLogPath); + let baselineLogPath: string = path.join( + parsedPath.dir, + process.platform === "linux" + ? "../tinyvm_linux_baseline.out" + : "../tinyvm_mingw_baseline.out" + ); + let extensionLogContent: string = util.readFile(extensionLogPath) || ""; + let baselineLogContent: string = util.readFile(baselineLogPath) || ""; + let extensionRootPath: string = path.resolve(__dirname, "../../../../"); + baselineLogContent = baselineLogContent.replace( + /{REPO:VSCODE-MAKEFILE-TOOLS}/gm, + extensionRootPath + ); + // fs.writeFileSync(path.join(parsedPath.dir, "base3.out"), baselineLogContent); + // fs.writeFileSync(path.join(parsedPath.dir, "diff3.out"), extensionLogContent); + expect(extensionLogContent).to.be.equal(baselineLogContent); + }); + } + + test(`Test real make - ${systemPlatform}`, async () => { + // Settings reset from the previous test run. + await vscode.commands.executeCommand("makefile.resetState", false); + await vscode.commands.executeCommand("makefile.testResetState"); + await vscode.workspace + .getConfiguration("makefile") + .update("launchConfigurations", undefined); + await vscode.commands.executeCommand( + "makefile.setBuildConfigurationByName", + "Default" + ); + + // Extension log is defined in the test .vscode/settings.json but delete it now + // because we are interested to compare against a baseline from this point further. + let extensionLogPath: string = path.join( + vscode.workspace.rootPath || "./", + ".vscode/Makefile.out" + ); + if (extensionLogPath && util.checkFileExistsSync(extensionLogPath)) { + util.deleteFileSync(extensionLogPath); + } + + await vscode.commands.executeCommand( + "makefile.setBuildConfigurationByName", + "test-make-f" + ); + await vscode.commands.executeCommand("makefile.cleanConfigure"); + + await vscode.commands.executeCommand( + "makefile.setBuildConfigurationByName", + "test-make-C" + ); + await vscode.commands.executeCommand("makefile.buildCleanAll"); + + // Compare the output log with the baseline + // TODO: incorporate relevant diff snippets into the test log. + // Until then, print into base and diff files for easier viewing + // when the test fails. + let parsedPath: path.ParsedPath = path.parse(extensionLogPath); + let baselineLogPath: string = path.join( + parsedPath.dir, + process.platform === "win32" + ? "../test_real_make_windows_baseline.out" + : "../test_real_make_nonWin_baseline.out" + ); + let extensionLogContent: string = util.readFile(extensionLogPath) || ""; + extensionLogContent = extensionLogContent.replace(/\r\n/gm, "\n"); + let baselineLogContent: string = util.readFile(baselineLogPath) || ""; + let extensionRootPath: string = path.resolve(__dirname, "../../../../"); + baselineLogContent = baselineLogContent.replace( + /{REPO:VSCODE-MAKEFILE-TOOLS}/gm, + extensionRootPath + ); + baselineLogContent = baselineLogContent.replace(/\r\n/gm, "\n"); + // fs.writeFileSync(path.join(parsedPath.dir, "base2.out"), baselineLogContent); + // fs.writeFileSync(path.join(parsedPath.dir, "diff2.out"), extensionLogContent); + expect(extensionLogContent).to.be.equal(baselineLogContent); + }); + + test(`Variables expansion - ${systemPlatform}`, async () => { + // Settings reset from the previous test run. + await vscode.commands.executeCommand("makefile.resetState", false); + await vscode.commands.executeCommand("makefile.testResetState"); + await vscode.workspace + .getConfiguration("makefile") + .update("launchConfigurations", undefined); + await vscode.commands.executeCommand( + "makefile.setBuildConfigurationByName", + "Default" + ); + + await vscode.commands.executeCommand( + "makefile.setBuildConfigurationByName", + "varexp" + ); + await vscode.commands.executeCommand("makefile.setTargetByName", "all"); + + // Delete extension log a bit later than other tests. For this one, we only care to capture varexp. + // All else that happens before, it was covered during the other tests in this suite. + let extensionLogPath: string = path.join( + vscode.workspace.rootPath || "./", + ".vscode/Makefile.out" + ); + if (extensionLogPath && util.checkFileExistsSync(extensionLogPath)) { + util.deleteFileSync(extensionLogPath); + } + + await vscode.commands.executeCommand( + "makefile.getExpandedSettingValue", + "buildLog", + "./${workspaceFolder}/${configuration}/${buildTarget}/something/${configuration}/${buildTarget}/build.log" + ); + + let stopAtEntry: string = await vscode.commands.executeCommand( + "makefile.expandVariablesInSetting", + "defaultLaunchConfiguration.stopAtEntry", + "${config:makefile.panel.visibility.debug}" + ); + let tmpDefaultLaunchConfiguration: configuration.DefaultLaunchConfiguration = + { + miDebuggerPath: + "./${workspaceRoot}/${command:makefile.getConfiguration}/${command:makefile.getBuildTarget}", + stopAtEntry: util.booleanify(stopAtEntry), + }; + await vscode.commands.executeCommand( + "makefile.getExpandedSettingValue", + "defaultLaunchConfiguration", + tmpDefaultLaunchConfiguration + ); + + let tmpConfigurations: configuration.MakefileConfiguration[] = [ + { + name: "MyTmpName", + makePath: "${env:ProgramFiles(x86)}/${workspaceFolderBasename}/make", + makeArgs: [ + "${command:makefile.getLaunchTargetPath}", + "${SomeUnsupportedVar}", + "try_\\${escape_varexp1}_various_\\${escape_varexp2}_escapes", + "${command:makefile.inexistentCommand}", + "${config:makefile.inexistentSetting}", + ], + }, + ]; + await vscode.commands.executeCommand( + "makefile.getExpandedSettingValue", + "configurations", + tmpConfigurations + ); + + // Compare the output log with the baseline + // TODO: incorporate relevant diff snippets into the test log. + // Until then, print into base and diff files for easier viewing + // when the test fails. + let parsedPath: path.ParsedPath = path.parse(extensionLogPath); + let baselineLogPath: string = path.join( + parsedPath.dir, + process.platform === "win32" + ? "../varexp_win32_baseline.out" + : "../varexp_baseline.out" + ); + let extensionLogContent: string = util.readFile(extensionLogPath) || ""; + extensionLogContent = extensionLogContent.replace(/\r\n/gm, "\n"); + let baselineLogContent: string = util.readFile(baselineLogPath) || ""; + let extensionRootPath: string = path.resolve(__dirname, "../../../../"); + baselineLogContent = baselineLogContent.replace( + /{REPO:VSCODE-MAKEFILE-TOOLS}/gm, + extensionRootPath + ); + baselineLogContent = baselineLogContent.replace(/\r\n/gm, "\n"); + // fs.writeFileSync(path.join(parsedPath.dir, "base1.out"), baselineLogContent); + // fs.writeFileSync(path.join(parsedPath.dir, "diff1.out"), extensionLogContent); + expect(extensionLogContent).to.be.equal(baselineLogContent); + }); }); diff --git a/src/test/fakeSuite/index.ts b/src/test/fakeSuite/index.ts index f763087..e1ae9db 100644 --- a/src/test/fakeSuite/index.ts +++ b/src/test/fakeSuite/index.ts @@ -2,42 +2,42 @@ // Licensed under the MIT license. // index.ts -import * as path from 'path'; -import * as Mocha from 'mocha'; -import * as glob from 'glob'; +import * as path from "path"; +import * as Mocha from "mocha"; +import * as glob from "glob"; export function run(): Promise { - // Create the mocha test - const mocha : Mocha = new Mocha({ - ui: 'tdd', - timeout: 100000000, - color: true - }); + // Create the mocha test + const mocha: Mocha = new Mocha({ + ui: "tdd", + timeout: 100000000, + color: true, + }); - const testsRoot : string = path.resolve(__dirname, '..'); + const testsRoot: string = path.resolve(__dirname, ".."); - return new Promise((c, e) => { - glob('**/**.test.js', { cwd: testsRoot }, (err, files) => { - if (err) { - return e(err); - } + return new Promise((c, e) => { + glob("**/**.test.js", { cwd: testsRoot }, (err, files) => { + if (err) { + return e(err); + } - // Add files to the test suite - files.forEach(f => mocha.addFile(path.resolve(testsRoot, f))); + // Add files to the test suite + files.forEach((f) => mocha.addFile(path.resolve(testsRoot, f))); - try { - // Run the mocha test - mocha.run(failures => { - if (failures > 0) { - e(new Error(`${failures} tests failed.`)); - } else { - c(); - } - }); - } catch (err) { - console.error(err); - e(err); - } + try { + // Run the mocha test + mocha.run((failures) => { + if (failures > 0) { + e(new Error(`${failures} tests failed.`)); + } else { + c(); + } }); + } catch (err) { + console.error(err); + e(err); + } }); + }); } diff --git a/src/test/runTest.ts b/src/test/runTest.ts index e5a5a8d..538d935 100644 --- a/src/test/runTest.ts +++ b/src/test/runTest.ts @@ -2,44 +2,53 @@ // Licensed under the MIT license. // Makefile Tools Tests -import * as path from 'path'; +import * as path from "path"; //import { runTests } from 'vscode-test'; //import * as tests from 'vscode-test'; -import * as testRunner from '@vscode/test-electron/out/runTest'; +import * as testRunner from "@vscode/test-electron/out/runTest"; async function main(): Promise { - try { - // The folder containing the Extension Manifest package.json - // Passed to `--extensionDevelopmentPath` - const extensionDevelopmentPath : string = path.resolve(__dirname, '../../../'); + try { + // The folder containing the Extension Manifest package.json + // Passed to `--extensionDevelopmentPath` + const extensionDevelopmentPath: string = path.resolve( + __dirname, + "../../../" + ); - // The path to the extension test script - // Passed to --extensionTestsPath - const extensionTestsPath : string = path.resolve(__dirname, './fakeSuite/index'); + // The path to the extension test script + // Passed to --extensionTestsPath + const extensionTestsPath: string = path.resolve( + __dirname, + "./fakeSuite/index" + ); - // The path to the makefile repro (containing the root makefile and .vscode folder) - const reproRootPath : string = path.resolve(extensionDevelopmentPath, "./src/test/fakeSuite/Repros/"); + // The path to the makefile repro (containing the root makefile and .vscode folder) + const reproRootPath: string = path.resolve( + extensionDevelopmentPath, + "./src/test/fakeSuite/Repros/" + ); - // Download VS Code, unzip it and run the integration test - let myOpt: testRunner.TestOptions = { - extensionDevelopmentPath: extensionDevelopmentPath, - launchArgs: [ - "--disable-workspace-trust", - "--disable-extensions", - reproRootPath, - ], - extensionTestsPath: extensionTestsPath, - extensionTestsEnv: { - MAKEFILE_TOOLS_TESTING: "1", - WindowsSDKVersion: "12.3.45678.9\\", - }, - }; - await testRunner.runTests(myOpt); - } catch (err) { - console.error('Failed to run tests'); - process.exit(1); - } + // Download VS Code, unzip it and run the integration test + let myOpt: testRunner.TestOptions = { + extensionDevelopmentPath: extensionDevelopmentPath, + launchArgs: [ + "--disable-workspace-trust", + "--disable-extensions", + reproRootPath, + ], + extensionTestsPath: extensionTestsPath, + extensionTestsEnv: { + MAKEFILE_TOOLS_TESTING: "1", + WindowsSDKVersion: "12.3.45678.9\\", + }, + }; + await testRunner.runTests(myOpt); + } catch (err) { + console.error("Failed to run tests"); + process.exit(1); + } } main(); diff --git a/src/tree.ts b/src/tree.ts index 0a5fe2f..90d8697 100644 --- a/src/tree.ts +++ b/src/tree.ts @@ -3,377 +3,497 @@ // Tree.ts -import * as configuration from './configuration'; -import * as path from 'path'; -import * as util from './util'; -import * as vscode from 'vscode'; +import * as configuration from "./configuration"; +import * as path from "path"; +import * as util from "./util"; +import * as vscode from "vscode"; -import * as nls from 'vscode-nls'; -import { extension } from './extension'; -nls.config({ messageFormat: nls.MessageFormat.bundle, bundleFormat: nls.BundleFormat.standalone })(); +import * as nls from "vscode-nls"; +import { extension } from "./extension"; +nls.config({ + messageFormat: nls.MessageFormat.bundle, + bundleFormat: nls.BundleFormat.standalone, +})(); const localize: nls.LocalizeFunc = nls.loadMessageBundle(); interface NamedItem { - name: string; + name: string; } abstract class BaseNode { - constructor(public readonly id: string) { } - abstract getTreeItem(): vscode.TreeItem; - abstract getChildren(): BaseNode[]; + constructor(public readonly id: string) {} + abstract getTreeItem(): vscode.TreeItem; + abstract getChildren(): BaseNode[]; } export class BuildTargetNode extends BaseNode { - constructor(targetName: string) { - super(`buildTarget:${targetName}`); - this._name = targetName; + constructor(targetName: string) { + super(`buildTarget:${targetName}`); + this._name = targetName; + } + + _name: string; + + update(targetName: string): void { + this._name = localize( + "tree.build.target", + "Build target: {0}", + `[${targetName}]` + ); + } + + getChildren(): BaseNode[] { + return []; + } + + getTreeItem(): vscode.TreeItem { + try { + const item: vscode.TreeItem = new vscode.TreeItem(this._name); + item.collapsibleState = vscode.TreeItemCollapsibleState.None; + item.tooltip = localize( + "makefile.target.currently.selected.for.build", + "The makefile target currently selected for build." + ); + item.contextValue = [`nodeType=buildTarget`].join(","); + return item; + } catch (e) { + return new vscode.TreeItem( + localize( + "issue.rendering.item", + "{0} (there was an issue rendering this item)", + this._name + ) + ); } - - _name: string; - - update(targetName: string): void { - this._name = localize("tree.build.target", "Build target: {0}", `[${targetName}]`); - } - - getChildren(): BaseNode[] { - return []; - } - - getTreeItem(): vscode.TreeItem { - try { - const item: vscode.TreeItem = new vscode.TreeItem(this._name); - item.collapsibleState = vscode.TreeItemCollapsibleState.None; - item.tooltip = localize("makefile.target.currently.selected.for.build", "The makefile target currently selected for build."); - item.contextValue = [ - `nodeType=buildTarget`, - ].join(','); - return item; - } catch (e) { - return new vscode.TreeItem(localize("issue.rendering.item", "{0} (there was an issue rendering this item)", this._name)); - } - } - + } } export class LaunchTargetNode extends BaseNode { - _name: string; - _toolTip: string; + _name: string; + _toolTip: string; - // Keep the tree node label as short as possible. - // The binary path is the most important component of a launch target. - async getShortLaunchTargetName(completeLaunchTargetName: string): Promise { - let launchConfiguration: configuration.LaunchConfiguration | undefined = await configuration.stringToLaunchConfiguration(completeLaunchTargetName); - let shortName: string; + // Keep the tree node label as short as possible. + // The binary path is the most important component of a launch target. + async getShortLaunchTargetName( + completeLaunchTargetName: string + ): Promise { + let launchConfiguration: configuration.LaunchConfiguration | undefined = + await configuration.stringToLaunchConfiguration(completeLaunchTargetName); + let shortName: string; - if (!launchConfiguration) { - shortName = "Unset"; - } else { - if (vscode.workspace.workspaceFolders) { - // In a complete launch target string, the binary path is relative to cwd. - // In here, since we don't show cwd, make it relative to current workspace folder. - shortName = util.makeRelPath(launchConfiguration.binaryPath, vscode.workspace.workspaceFolders[0].uri.fsPath); - } else { - // Just in case, if for some reason we don't have a workspace folder, return full binary path. - shortName = launchConfiguration.binaryPath; - } - } - - return localize("tree.launch.target", "Launch target: {0}", `[${shortName}]`); + if (!launchConfiguration) { + shortName = "Unset"; + } else { + if (vscode.workspace.workspaceFolders) { + // In a complete launch target string, the binary path is relative to cwd. + // In here, since we don't show cwd, make it relative to current workspace folder. + shortName = util.makeRelPath( + launchConfiguration.binaryPath, + vscode.workspace.workspaceFolders[0].uri.fsPath + ); + } else { + // Just in case, if for some reason we don't have a workspace folder, return full binary path. + shortName = launchConfiguration.binaryPath; + } } - constructor(targetName: string) { - super(`launchTarget:${targetName}`); + return localize( + "tree.launch.target", + "Launch target: {0}", + `[${shortName}]` + ); + } - // Show the complete launch target name as tooltip and the short name as label - this._name = targetName; - this._toolTip = targetName; + constructor(targetName: string) { + super(`launchTarget:${targetName}`); + + // Show the complete launch target name as tooltip and the short name as label + this._name = targetName; + this._toolTip = targetName; + } + + async update(targetName: string): Promise { + // Show the complete launch target name as tooltip and the short name as label + this._name = await this.getShortLaunchTargetName(targetName); + this._toolTip = targetName; + } + + getChildren(): BaseNode[] { + return []; + } + + getTreeItem(): vscode.TreeItem { + try { + const item: vscode.TreeItem = new vscode.TreeItem(this._name); + item.collapsibleState = vscode.TreeItemCollapsibleState.None; + item.tooltip = localize( + "launch.target.currently.selected.for.debug.run.in.terminal", + "The launch target currently selected for debug and run in terminal.\n{0}", + this._toolTip + ); + // enablement in makefile.outline.setLaunchConfiguration is not + // disabling this TreeItem + item.command = { + command: "makefile.outline.setLaunchConfiguration", + title: "%makefile-tools.command.makefile.setLaunchConfiguration.title%", + }; + item.contextValue = [`nodeType=launchTarget`].join(","); + return item; + } catch (e) { + return new vscode.TreeItem( + localize( + "issue.rendering.item", + "{0} (there was an issue rendering this item)", + this._name + ) + ); } - - async update(targetName: string): Promise { - // Show the complete launch target name as tooltip and the short name as label - this._name = await this.getShortLaunchTargetName(targetName); - this._toolTip = targetName; - } - - getChildren(): BaseNode[] { - return []; - } - - getTreeItem(): vscode.TreeItem { - try { - const item: vscode.TreeItem = new vscode.TreeItem(this._name); - item.collapsibleState = vscode.TreeItemCollapsibleState.None; - item.tooltip = localize("launch.target.currently.selected.for.debug.run.in.terminal", - "The launch target currently selected for debug and run in terminal.\n{0}", this._toolTip); - // enablement in makefile.outline.setLaunchConfiguration is not - // disabling this TreeItem - item.command = { - command: "makefile.outline.setLaunchConfiguration", - title: "%makefile-tools.command.makefile.setLaunchConfiguration.title%" - }; - item.contextValue = [ - `nodeType=launchTarget`, - ].join(','); - return item; - } catch (e) { - return new vscode.TreeItem(localize("issue.rendering.item", "{0} (there was an issue rendering this item)", this._name)); - } - } - + } } export class ConfigurationNode extends BaseNode { - constructor(configurationName: string) { - super(`configuration:${configurationName}`); - this._name = configurationName; + constructor(configurationName: string) { + super(`configuration:${configurationName}`); + this._name = configurationName; + } + + _name: string; + + update(configurationName: string): void { + this._name = localize( + "tree.configuration", + "Configuration: {0}", + `[${configurationName}]` + ); + } + + getChildren(): BaseNode[] { + return []; + } + + getTreeItem(): vscode.TreeItem { + try { + const item: vscode.TreeItem = new vscode.TreeItem(this._name); + item.collapsibleState = vscode.TreeItemCollapsibleState.None; + item.tooltip = + "The makefile configuration currently selected from settings ('makefile.configurations')."; + item.contextValue = [`nodeType=configuration`].join(","); + return item; + } catch (e) { + return new vscode.TreeItem( + localize( + "issue.rendering.item", + "{0} (there was an issue rendering this item)", + this._name + ) + ); } - - _name: string; - - update(configurationName: string): void { - this._name = localize("tree.configuration", "Configuration: {0}", `[${configurationName}]`); - } - - getChildren(): BaseNode[] { - return []; - } - - getTreeItem(): vscode.TreeItem { - try { - const item: vscode.TreeItem = new vscode.TreeItem(this._name); - item.collapsibleState = vscode.TreeItemCollapsibleState.None; - item.tooltip = "The makefile configuration currently selected from settings ('makefile.configurations')."; - item.contextValue = [ - `nodeType=configuration`, - ].join(','); - return item; - } catch (e) { - return new vscode.TreeItem(localize("issue.rendering.item", "{0} (there was an issue rendering this item)", this._name)); - } - } - + } } export class MakefilePathInfoNode extends BaseNode { - constructor(pathInSettings: string, pathDisplayed: string) { - super(pathDisplayed); - this._title = pathDisplayed; - this._tooltip = pathInSettings; - } + constructor(pathInSettings: string, pathDisplayed: string) { + super(pathDisplayed); + this._title = pathDisplayed; + this._tooltip = pathInSettings; + } - _title: string; - _tooltip: string; + _title: string; + _tooltip: string; - update(pathInSettings: string, pathDisplayed: string): void { - this._title = localize("tree.makefile.path.info", "{0}", `${pathDisplayed}`); - this._tooltip = pathInSettings; - } + update(pathInSettings: string, pathDisplayed: string): void { + this._title = localize( + "tree.makefile.path.info", + "{0}", + `${pathDisplayed}` + ); + this._tooltip = pathInSettings; + } - getChildren(): BaseNode[] { - return []; - } + getChildren(): BaseNode[] { + return []; + } - getTreeItem(): vscode.TreeItem { - try { - const item: vscode.TreeItem = new vscode.TreeItem(this._title); - item.collapsibleState = vscode.TreeItemCollapsibleState.None; - item.tooltip = this._tooltip; - item.contextValue = [ - `nodeType=makefilePathInfo`, - ].join(','); - return item; - } catch (e) { - return new vscode.TreeItem(localize("issue.rendering.item", "{0} (there was an issue rendering this item)", this._title)); - } - } + getTreeItem(): vscode.TreeItem { + try { + const item: vscode.TreeItem = new vscode.TreeItem(this._title); + item.collapsibleState = vscode.TreeItemCollapsibleState.None; + item.tooltip = this._tooltip; + item.contextValue = [`nodeType=makefilePathInfo`].join(","); + return item; + } catch (e) { + return new vscode.TreeItem( + localize( + "issue.rendering.item", + "{0} (there was an issue rendering this item)", + this._title + ) + ); + } + } } export class MakePathInfoNode extends BaseNode { - constructor(pathInSettings: string, pathDisplayed: string) { - super(pathDisplayed); - this._title = pathDisplayed; - this._tooltip = pathInSettings; - } + constructor(pathInSettings: string, pathDisplayed: string) { + super(pathDisplayed); + this._title = pathDisplayed; + this._tooltip = pathInSettings; + } - _title: string; - _tooltip: string; + _title: string; + _tooltip: string; - update(pathInSettings: string, pathDisplayed: string): void { - this._title = localize("tree.make.path.info", "{0}", `${pathDisplayed}`); - this._tooltip = pathInSettings; - } + update(pathInSettings: string, pathDisplayed: string): void { + this._title = localize("tree.make.path.info", "{0}", `${pathDisplayed}`); + this._tooltip = pathInSettings; + } - getChildren(): BaseNode[] { - return []; - } + getChildren(): BaseNode[] { + return []; + } - getTreeItem(): vscode.TreeItem { - try { - const item: vscode.TreeItem = new vscode.TreeItem(this._title); - item.collapsibleState = vscode.TreeItemCollapsibleState.None; - item.tooltip = this._tooltip; - item.contextValue = [ - `nodeType=makePathInfo`, - ].join(','); - return item; - } catch (e) { - return new vscode.TreeItem(localize("issue.rendering.item", "{0} (there was an issue rendering this item)", this._title)); - } - } + getTreeItem(): vscode.TreeItem { + try { + const item: vscode.TreeItem = new vscode.TreeItem(this._title); + item.collapsibleState = vscode.TreeItemCollapsibleState.None; + item.tooltip = this._tooltip; + item.contextValue = [`nodeType=makePathInfo`].join(","); + return item; + } catch (e) { + return new vscode.TreeItem( + localize( + "issue.rendering.item", + "{0} (there was an issue rendering this item)", + this._title + ) + ); + } + } } export class BuildLogPathInfoNode extends BaseNode { - constructor(pathInSettings: string, pathDisplayed: string) { - super(pathDisplayed); - this._title = pathDisplayed; - this._tooltip = pathInSettings; - } + constructor(pathInSettings: string, pathDisplayed: string) { + super(pathDisplayed); + this._title = pathDisplayed; + this._tooltip = pathInSettings; + } - _title: string; - _tooltip: string; + _title: string; + _tooltip: string; - update(pathInSettings: string, pathDisplayed: string): void { - this._title = localize("tree.build.log.path.info", "{0}", `${pathDisplayed}`); - this._tooltip = pathInSettings; - } + update(pathInSettings: string, pathDisplayed: string): void { + this._title = localize( + "tree.build.log.path.info", + "{0}", + `${pathDisplayed}` + ); + this._tooltip = pathInSettings; + } - getChildren(): BaseNode[] { - return []; - } + getChildren(): BaseNode[] { + return []; + } - getTreeItem(): vscode.TreeItem { - try { - const item: vscode.TreeItem = new vscode.TreeItem(this._title); - item.collapsibleState = vscode.TreeItemCollapsibleState.None; - item.tooltip = this._tooltip; - item.contextValue = [ - `nodeType=buildLogPathInfo`, - ].join(','); - return item; - } catch (e) { - return new vscode.TreeItem(localize("issue.rendering.item", "{0} (there was an issue rendering this item)", this._title)); - } - } + getTreeItem(): vscode.TreeItem { + try { + const item: vscode.TreeItem = new vscode.TreeItem(this._title); + item.collapsibleState = vscode.TreeItemCollapsibleState.None; + item.tooltip = this._tooltip; + item.contextValue = [`nodeType=buildLogPathInfo`].join(","); + return item; + } catch (e) { + return new vscode.TreeItem( + localize( + "issue.rendering.item", + "{0} (there was an issue rendering this item)", + this._title + ) + ); + } + } } -export class ProjectOutlineProvider implements vscode.TreeDataProvider { - private readonly _changeEvent = new vscode.EventEmitter(); - private readonly _unsetString = localize("Unset", "Unset"); +export class ProjectOutlineProvider + implements vscode.TreeDataProvider +{ + private readonly _changeEvent = new vscode.EventEmitter(); + private readonly _unsetString = localize("Unset", "Unset"); - constructor() { - this._currentConfigurationItem = new ConfigurationNode(this._unsetString); - this._currentBuildTargetItem = new BuildTargetNode(this._unsetString); - this._currentLaunchTargetItem = new LaunchTargetNode(this._unsetString); - this._currentMakefilePathInfoItem = new MakefilePathInfoNode(this._unsetString, ""); - this._currentMakePathInfoItem = new MakePathInfoNode(this._unsetString, ""); - this._currentBuildLogPathInfoItem = new BuildLogPathInfoNode(this._unsetString, ""); + constructor() { + this._currentConfigurationItem = new ConfigurationNode(this._unsetString); + this._currentBuildTargetItem = new BuildTargetNode(this._unsetString); + this._currentLaunchTargetItem = new LaunchTargetNode(this._unsetString); + this._currentMakefilePathInfoItem = new MakefilePathInfoNode( + this._unsetString, + "" + ); + this._currentMakePathInfoItem = new MakePathInfoNode(this._unsetString, ""); + this._currentBuildLogPathInfoItem = new BuildLogPathInfoNode( + this._unsetString, + "" + ); + } + + private _currentConfigurationItem: ConfigurationNode; + private _currentBuildTargetItem: BuildTargetNode; + private _currentLaunchTargetItem: LaunchTargetNode; + private _currentMakefilePathInfoItem: MakefilePathInfoNode; + private _currentMakePathInfoItem: MakePathInfoNode; + private _currentBuildLogPathInfoItem: BuildLogPathInfoNode; + + get onDidChangeTreeData(): any { + return this._changeEvent.event; + } + async getTreeItem(node: BaseNode): Promise { + return node.getTreeItem(); + } + getChildren(node?: BaseNode): BaseNode[] { + if (node) { + return node.getChildren(); + } + if ( + configuration.isOptionalFeatureEnabled("debug") || + configuration.isOptionalFeatureEnabled("run") + ) { + return [ + this._currentConfigurationItem, + this._currentBuildTargetItem, + this._currentLaunchTargetItem, + this._currentMakefilePathInfoItem, + this._currentMakePathInfoItem, + this._currentBuildLogPathInfoItem, + ]; + } else { + return [ + this._currentConfigurationItem, + this._currentBuildTargetItem, + this._currentMakefilePathInfoItem, + this._currentMakePathInfoItem, + this._currentBuildLogPathInfoItem, + ]; + } + } + + pathDisplayed( + pathInSettings: string | undefined, + kind: string, + searchInPath: boolean, + makeRelative: boolean + ): string { + if (!pathInSettings) { + if (kind === "Build Log") { + extension.updateBuildLogPresent(false); + } else if (kind === "Makefile") { + extension.updateMakefileFilePresent(false); + } + return `${kind}: [Unset]`; } - private _currentConfigurationItem: ConfigurationNode; - private _currentBuildTargetItem: BuildTargetNode; - private _currentLaunchTargetItem: LaunchTargetNode; - private _currentMakefilePathInfoItem: MakefilePathInfoNode; - private _currentMakePathInfoItem: MakePathInfoNode; - private _currentBuildLogPathInfoItem: BuildLogPathInfoNode; + const pathInSettingsToTest: string | undefined = + process.platform === "win32" && + !pathInSettings?.endsWith(".exe") && + kind === "Make" + ? pathInSettings?.concat(".exe") + : pathInSettings; + const pathBase: string | undefined = + searchInPath && path.parse(pathInSettingsToTest).dir === "" + ? path.parse(pathInSettingsToTest).base + : undefined; + const pathInEnv: string | undefined = pathBase + ? path.join(util.toolPathInEnv(pathBase) || "", pathBase) + : undefined; + const finalPath: string = pathInEnv || pathInSettingsToTest; + const checkFileExists = util.checkFileExistsSync(finalPath); - get onDidChangeTreeData(): any { - return this._changeEvent.event; - } - async getTreeItem(node: BaseNode): Promise { - return node.getTreeItem(); - } - getChildren(node?: BaseNode): BaseNode[] { - if (node) { - return node.getChildren(); - } - if (configuration.isOptionalFeatureEnabled("debug") || configuration.isOptionalFeatureEnabled("run")) { - return [this._currentConfigurationItem, - this._currentBuildTargetItem, - this._currentLaunchTargetItem, - this._currentMakefilePathInfoItem, - this._currentMakePathInfoItem, - this._currentBuildLogPathInfoItem]; - } else { - return [this._currentConfigurationItem, - this._currentBuildTargetItem, - this._currentMakefilePathInfoItem, - this._currentMakePathInfoItem, - this._currentBuildLogPathInfoItem]; - } - } - - pathDisplayed(pathInSettings: string | undefined, kind: string, searchInPath: boolean, makeRelative: boolean): string { - if (!pathInSettings) { - if (kind === "Build Log") { - extension.updateBuildLogPresent(false); - } else if (kind === "Makefile") { - extension.updateMakefileFilePresent(false); - } - return `${kind}: [Unset]`; - } - - const pathInSettingsToTest: string | undefined = process.platform === "win32" && !pathInSettings?.endsWith(".exe") && kind === "Make" ? pathInSettings?.concat(".exe") : pathInSettings; - const pathBase: string | undefined = (searchInPath && path.parse(pathInSettingsToTest).dir === "") ? path.parse(pathInSettingsToTest).base : undefined; - const pathInEnv: string | undefined = pathBase ? (path.join(util.toolPathInEnv(pathBase) || "", pathBase)) : undefined; - const finalPath: string = pathInEnv || pathInSettingsToTest; - const checkFileExists = util.checkFileExistsSync(finalPath); - - if (kind === "Build Log") { - extension.updateBuildLogPresent(checkFileExists); - } else if (kind === "Makefile") { - extension.updateMakefileFilePresent(checkFileExists); - } - - return (!checkFileExists ? `${kind} (not found)` : `${kind}`) + `: [${makeRelative ? util.makeRelPath(finalPath, util.getWorkspaceRoot()) : finalPath}]`; + if (kind === "Build Log") { + extension.updateBuildLogPresent(checkFileExists); + } else if (kind === "Makefile") { + extension.updateMakefileFilePresent(checkFileExists); } - async update(configuration: string | undefined, - buildTarget: string | undefined, - launchTarget: string | undefined, - makefilePathInfo: string | undefined, - makePathInfo: string | undefined, - buildLogInfo: string | undefined): Promise { - this._currentConfigurationItem.update(configuration || this._unsetString); - this._currentBuildTargetItem.update(buildTarget || this._unsetString); - await this._currentLaunchTargetItem.update(launchTarget || this._unsetString); - this._currentMakefilePathInfoItem.update(makefilePathInfo || this._unsetString, this.pathDisplayed(makefilePathInfo, "Makefile", false, false)); - this._currentMakePathInfoItem.update(makePathInfo || this._unsetString, this.pathDisplayed(makePathInfo, "Make", true, false)); - this._currentBuildLogPathInfoItem.update(buildLogInfo || this._unsetString, this.pathDisplayed(buildLogInfo, "Build Log", false, false)); + return ( + (!checkFileExists ? `${kind} (not found)` : `${kind}`) + + `: [${ + makeRelative + ? util.makeRelPath(finalPath, util.getWorkspaceRoot()) + : finalPath + }]` + ); + } - this.updateTree(); - } + async update( + configuration: string | undefined, + buildTarget: string | undefined, + launchTarget: string | undefined, + makefilePathInfo: string | undefined, + makePathInfo: string | undefined, + buildLogInfo: string | undefined + ): Promise { + this._currentConfigurationItem.update(configuration || this._unsetString); + this._currentBuildTargetItem.update(buildTarget || this._unsetString); + await this._currentLaunchTargetItem.update( + launchTarget || this._unsetString + ); + this._currentMakefilePathInfoItem.update( + makefilePathInfo || this._unsetString, + this.pathDisplayed(makefilePathInfo, "Makefile", false, false) + ); + this._currentMakePathInfoItem.update( + makePathInfo || this._unsetString, + this.pathDisplayed(makePathInfo, "Make", true, false) + ); + this._currentBuildLogPathInfoItem.update( + buildLogInfo || this._unsetString, + this.pathDisplayed(buildLogInfo, "Build Log", false, false) + ); - updateConfiguration(configuration: string): void { - this._currentConfigurationItem.update(configuration); - this.updateTree(); - } + this.updateTree(); + } - updateBuildTarget(buildTarget: string): void { - this._currentBuildTargetItem.update(buildTarget); - this.updateTree(); - } + updateConfiguration(configuration: string): void { + this._currentConfigurationItem.update(configuration); + this.updateTree(); + } - async updateLaunchTarget(launchTarget: string): Promise { - await this._currentLaunchTargetItem.update(launchTarget); - this.updateTree(); - } + updateBuildTarget(buildTarget: string): void { + this._currentBuildTargetItem.update(buildTarget); + this.updateTree(); + } - async updateMakefilePathInfo(makefilePathInfo: string | undefined): Promise { - this._currentMakefilePathInfoItem.update(makefilePathInfo || this._unsetString, this.pathDisplayed(makefilePathInfo, "Makefile", false, true)); - this.updateTree(); - } + async updateLaunchTarget(launchTarget: string): Promise { + await this._currentLaunchTargetItem.update(launchTarget); + this.updateTree(); + } - async updateMakePathInfo(makePathInfo: string | undefined): Promise { - this._currentMakePathInfoItem.update(makePathInfo || this._unsetString, this.pathDisplayed(makePathInfo, "Make", true, false)); - this.updateTree(); - } + async updateMakefilePathInfo( + makefilePathInfo: string | undefined + ): Promise { + this._currentMakefilePathInfoItem.update( + makefilePathInfo || this._unsetString, + this.pathDisplayed(makefilePathInfo, "Makefile", false, true) + ); + this.updateTree(); + } - async updateBuildLogPathInfo(buildLogPathInfo: string | undefined): Promise { - this._currentBuildLogPathInfoItem.update(buildLogPathInfo || this._unsetString, this.pathDisplayed(buildLogPathInfo, "Build Log", false, true)); - this.updateTree(); - } + async updateMakePathInfo(makePathInfo: string | undefined): Promise { + this._currentMakePathInfoItem.update( + makePathInfo || this._unsetString, + this.pathDisplayed(makePathInfo, "Make", true, false) + ); + this.updateTree(); + } - updateTree(): void { - this._changeEvent.fire(null); - } + async updateBuildLogPathInfo( + buildLogPathInfo: string | undefined + ): Promise { + this._currentBuildLogPathInfoItem.update( + buildLogPathInfo || this._unsetString, + this.pathDisplayed(buildLogPathInfo, "Build Log", false, true) + ); + this.updateTree(); + } + + updateTree(): void { + this._changeEvent.fire(null); + } } diff --git a/src/ui.ts b/src/ui.ts index d0212d5..da14d94 100644 --- a/src/ui.ts +++ b/src/ui.ts @@ -5,86 +5,109 @@ // Replaced by makefile.outline view in the left side bar. // To be removed, hidden for now. -import * as vscode from 'vscode'; +import * as vscode from "vscode"; let ui: UI; export class UI { - private configurationButton: vscode.StatusBarItem; - private targetButton: vscode.StatusBarItem; - private launchConfigurationButton: vscode.StatusBarItem; - private buildButton: vscode.StatusBarItem; - private debugButton: vscode.StatusBarItem; - private runButton: vscode.StatusBarItem; + private configurationButton: vscode.StatusBarItem; + private targetButton: vscode.StatusBarItem; + private launchConfigurationButton: vscode.StatusBarItem; + private buildButton: vscode.StatusBarItem; + private debugButton: vscode.StatusBarItem; + private runButton: vscode.StatusBarItem; - public setConfiguration(configuration: string): void { - this.configurationButton.text = "$(settings) Build configuration: " + configuration; + public setConfiguration(configuration: string): void { + this.configurationButton.text = + "$(settings) Build configuration: " + configuration; + } + + public setTarget(target: string): void { + this.targetButton.text = "$(tag) Target to build: " + target; + } + + public setLaunchConfiguration( + launchConfigurationStr: string | undefined + ): void { + if (launchConfigurationStr) { + this.launchConfigurationButton.text = "$(rocket) Launch configuration: "; + this.launchConfigurationButton.text += "["; + this.launchConfigurationButton.text += launchConfigurationStr; + this.launchConfigurationButton.text += "]"; + } else { + this.launchConfigurationButton.text = "No launch configuration set"; } + } - public setTarget(target: string): void { - this.targetButton.text = "$(tag) Target to build: " + target; - } + public constructor() { + this.configurationButton = vscode.window.createStatusBarItem( + vscode.StatusBarAlignment.Left, + 4.6 + ); + this.configurationButton.command = "makefile.setBuildConfiguration"; + this.configurationButton.tooltip = + "Click to select the workspace make configuration"; + this.configurationButton.hide(); - public setLaunchConfiguration(launchConfigurationStr: string | undefined): void { - if (launchConfigurationStr) { - this.launchConfigurationButton.text = "$(rocket) Launch configuration: "; - this.launchConfigurationButton.text += "["; - this.launchConfigurationButton.text += launchConfigurationStr; - this.launchConfigurationButton.text += "]"; - } else { - this.launchConfigurationButton.text = "No launch configuration set"; - } - } + this.targetButton = vscode.window.createStatusBarItem( + vscode.StatusBarAlignment.Left, + 4.5 + ); + this.targetButton.command = "makefile.setBuildTarget"; + this.targetButton.tooltip = "Click to select the target to be run by make"; + this.targetButton.hide(); - public constructor() { - this.configurationButton = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Left, 4.6); - this.configurationButton.command = "makefile.setBuildConfiguration"; - this.configurationButton.tooltip = "Click to select the workspace make configuration"; - this.configurationButton.hide(); + this.buildButton = vscode.window.createStatusBarItem( + vscode.StatusBarAlignment.Left, + 4.4 + ); + this.buildButton.command = "makefile.buildTarget"; + this.buildButton.tooltip = "Click to build the selected target"; + this.buildButton.text = "$(gear) Build"; + this.buildButton.hide(); - this.targetButton = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Left, 4.5); - this.targetButton.command = "makefile.setBuildTarget"; - this.targetButton.tooltip = "Click to select the target to be run by make"; - this.targetButton.hide(); + this.launchConfigurationButton = vscode.window.createStatusBarItem( + vscode.StatusBarAlignment.Left, + 4.3 + ); + this.launchConfigurationButton.command = "makefile.setLaunchConfiguration"; + this.launchConfigurationButton.tooltip = + "Click to select the make launch configuration (binary, args and current path)"; + this.launchConfigurationButton.hide(); - this.buildButton = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Left, 4.4); - this.buildButton.command = "makefile.buildTarget"; - this.buildButton.tooltip = "Click to build the selected target"; - this.buildButton.text = "$(gear) Build"; - this.buildButton.hide(); + this.debugButton = vscode.window.createStatusBarItem( + vscode.StatusBarAlignment.Left, + 4.2 + ); + this.debugButton.command = "makefile.launchDebug"; + this.debugButton.tooltip = "Click to debug the selected executable"; + this.debugButton.text = "$(bug) Debug"; + this.debugButton.hide(); - this.launchConfigurationButton = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Left, 4.3); - this.launchConfigurationButton.command = "makefile.setLaunchConfiguration"; - this.launchConfigurationButton.tooltip = "Click to select the make launch configuration (binary, args and current path)"; - this.launchConfigurationButton.hide(); + this.runButton = vscode.window.createStatusBarItem( + vscode.StatusBarAlignment.Left, + 4.1 + ); + this.runButton.command = "makefile.launchRun"; + this.runButton.tooltip = "Click to launch the selected executable"; + this.runButton.text = "$(terminal) Run"; + this.runButton.hide(); + } - this.debugButton = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Left, 4.2); - this.debugButton.command = "makefile.launchDebug"; - this.debugButton.tooltip = "Click to debug the selected executable"; - this.debugButton.text = "$(bug) Debug"; - this.debugButton.hide(); - - this.runButton = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Left, 4.1); - this.runButton.command = "makefile.launchRun"; - this.runButton.tooltip = "Click to launch the selected executable"; - this.runButton.text = "$(terminal) Run"; - this.runButton.hide(); - } - - public dispose(): void { - this.configurationButton.dispose(); - this.targetButton.dispose(); - this.launchConfigurationButton.dispose(); - this.buildButton.dispose(); - this.debugButton.dispose(); - this.runButton.dispose(); - } + public dispose(): void { + this.configurationButton.dispose(); + this.targetButton.dispose(); + this.launchConfigurationButton.dispose(); + this.buildButton.dispose(); + this.debugButton.dispose(); + this.runButton.dispose(); + } } export function getUI(): UI { - if (ui === undefined) { - ui = new UI(); - } + if (ui === undefined) { + ui = new UI(); + } - return ui; + return ui; } diff --git a/src/util.ts b/src/util.ts index e4967a2..e62c80b 100644 --- a/src/util.ts +++ b/src/util.ts @@ -3,157 +3,196 @@ // Helper APIs used by this extension -import * as configuration from './configuration'; -import * as fs from 'fs'; -import * as child_process from 'child_process'; -import * as logger from './logger'; -import * as make from './make'; -import * as path from 'path'; -import * as telemetry from './telemetry'; -import * as vscode from 'vscode'; +import * as configuration from "./configuration"; +import * as fs from "fs"; +import * as child_process from "child_process"; +import * as logger from "./logger"; +import * as make from "./make"; +import * as path from "path"; +import * as telemetry from "./telemetry"; +import * as vscode from "vscode"; import * as nls from "vscode-nls"; -import { extension } from './extension'; -nls.config({ messageFormat: nls.MessageFormat.bundle, bundleFormat: nls.BundleFormat.standalone })(); +import { extension } from "./extension"; +nls.config({ + messageFormat: nls.MessageFormat.bundle, + bundleFormat: nls.BundleFormat.standalone, +})(); const localize: nls.LocalizeFunc = nls.loadMessageBundle(); // C/CPP standard versions -export type StandardVersion = 'c89' | 'c99' | 'c11' | 'c17' | 'c++98' | 'c++03' | 'c++11' | 'c++14' | 'c++17' | 'c++20' | 'c++23' | - 'gnu89' | 'gnu99' | 'gnu11' | 'gnu17' | 'gnu++98' | 'gnu++03' | 'gnu++11' | 'gnu++14' | 'gnu++17' | 'gnu++20' | 'gnu++23' | undefined; +export type StandardVersion = + | "c89" + | "c99" + | "c11" + | "c17" + | "c++98" + | "c++03" + | "c++11" + | "c++14" + | "c++17" + | "c++20" + | "c++23" + | "gnu89" + | "gnu99" + | "gnu11" + | "gnu17" + | "gnu++98" + | "gnu++03" + | "gnu++11" + | "gnu++14" + | "gnu++17" + | "gnu++20" + | "gnu++23" + | undefined; // Supported target architectures (for code generated by the compiler) -export type TargetArchitecture = 'x86' | 'x64' | 'arm' | 'arm64' | undefined; +export type TargetArchitecture = "x86" | "x64" | "arm" | "arm64" | undefined; // IntelliSense modes -export type IntelliSenseMode = "msvc-x64" | "msvc-x86" | "msvc-arm" | "msvc-arm64" | - "gcc-x64" | "gcc-x86" | "gcc-arm" | "gcc-arm64" | - "clang-x64" | "clang-x86" | "clang-arm" | "clang-arm64" | undefined; +export type IntelliSenseMode = + | "msvc-x64" + | "msvc-x86" + | "msvc-arm" + | "msvc-arm64" + | "gcc-x64" + | "gcc-x86" + | "gcc-arm" + | "gcc-arm64" + | "clang-x64" + | "clang-x86" + | "clang-arm" + | "clang-arm64" + | undefined; // Language types export type Language = "c" | "cpp" | undefined; export function checkFileExistsSync(filePath: string): boolean { - try { - // Often a path is added by the user to the PATH environment variable with surrounding quotes, - // especially on Windows where they get automatically added after TAB. - // These quotes become inner (not surrounding) quotes after we append various file names or do oher processing, - // making file sysem stats fail. Safe to remove here. - let filePathUnq: string = filePath; - filePathUnq = removeQuotes(filePathUnq); - return fs.statSync(filePathUnq).isFile(); - } catch (e) { - } - return false; + try { + // Often a path is added by the user to the PATH environment variable with surrounding quotes, + // especially on Windows where they get automatically added after TAB. + // These quotes become inner (not surrounding) quotes after we append various file names or do oher processing, + // making file sysem stats fail. Safe to remove here. + let filePathUnq: string = filePath; + filePathUnq = removeQuotes(filePathUnq); + return fs.statSync(filePathUnq).isFile(); + } catch (e) {} + return false; } export function checkDirectoryExistsSync(directoryPath: string): boolean { - try { - return fs.statSync(directoryPath).isDirectory(); - } catch (e) { - } - return false; + try { + return fs.statSync(directoryPath).isDirectory(); + } catch (e) {} + return false; } export function createDirectorySync(directoryPath: string): boolean { - try { - fs.mkdirSync(directoryPath, { recursive: true }); - return true; - } catch { - } - return false; + try { + fs.mkdirSync(directoryPath, { recursive: true }); + return true; + } catch {} + return false; } export function deleteFileSync(filePath: string): void { - try { - fs.unlinkSync(filePath); - } catch (e) { - } + try { + fs.unlinkSync(filePath); + } catch (e) {} } export function readFile(filePath: string): string | undefined { - try { - if (checkFileExistsSync(filePath)) { - return fs.readFileSync(filePath).toString(); - } - } catch (e) { + try { + if (checkFileExistsSync(filePath)) { + return fs.readFileSync(filePath).toString(); } + } catch (e) {} - return undefined; + return undefined; } -export function writeFile(filePath: string, content: string): string | undefined { - try { - fs.writeFileSync(filePath, content); - } catch (e) { - } +export function writeFile( + filePath: string, + content: string +): string | undefined { + try { + fs.writeFileSync(filePath, content); + } catch (e) {} - return undefined; + return undefined; } // Get the platform-specific temporary directory export function tmpDir(): string { - if (process.platform === 'win32') { - return process.env['TEMP'] || ""; - } else { - const xdg = process.env['XDG_RUNTIME_DIR']; - if (xdg) { - if (!fs.existsSync(xdg)) { - fs.mkdirSync(xdg); - } - return xdg; - } - return '/tmp'; + if (process.platform === "win32") { + return process.env["TEMP"] || ""; + } else { + const xdg = process.env["XDG_RUNTIME_DIR"]; + if (xdg) { + if (!fs.existsSync(xdg)) { + fs.mkdirSync(xdg); + } + return xdg; } + return "/tmp"; + } } // Returns the full path to a temporary script generated by the extension // and used to parse any additional compiler switches that need to be sent to CppTools. export function parseCompilerArgsScriptFile(): string { - const extensionPath = extension.extensionContext.extensionPath; - let scriptFile: string = path.join(extensionPath, "assets", "parseCompilerArgs"); + const extensionPath = extension.extensionContext.extensionPath; + let scriptFile: string = path.join( + extensionPath, + "assets", + "parseCompilerArgs" + ); - if (process.platform === "win32") { - scriptFile += ".bat"; - } else { - scriptFile += ".sh"; - } + if (process.platform === "win32") { + scriptFile += ".bat"; + } else { + scriptFile += ".sh"; + } - return scriptFile; + return scriptFile; } export function getWorkspaceRoot(): string { - return vscode.workspace.workspaceFolders ? vscode.workspace.workspaceFolders[0].uri.fsPath : ""; + return vscode.workspace.workspaceFolders + ? vscode.workspace.workspaceFolders[0].uri.fsPath + : ""; } - // Evaluate whether a string looks like a path or not, +// Evaluate whether a string looks like a path or not, // without using fs.stat, since dry-run may output tools // that are not found yet at certain locations, // without running the prep targets that would copy them there export function looksLikePath(pathStr: string): boolean { - // TODO: to be implemented - return true; + // TODO: to be implemented + return true; } // Evaluate whether the tool is invoked from the current directory export function pathIsCurrentDirectory(pathStr: string): boolean { - // Ignore any spaces or tabs before the invocation - pathStr = pathStr.trimLeft(); + // Ignore any spaces or tabs before the invocation + pathStr = pathStr.trimLeft(); - if (pathStr === "") { - return true; + if (pathStr === "") { + return true; + } + + if (process.platform === "win32" && process.env.MSYSTEM === undefined) { + if (pathStr === ".\\") { + return true; } - - if (process.platform === "win32" && process.env.MSYSTEM === undefined) { - if (pathStr === ".\\") { - return true; - } - } else { - if (pathStr === "./") { - return true; - } + } else { + if (pathStr === "./") { + return true; } + } - return false; + return false; } // Helper that searches for a tool in all the paths forming the PATH environment variable @@ -161,183 +200,234 @@ export function pathIsCurrentDirectory(pathStr: string): boolean { // TODO: implement a variation of this helper that scans on disk for the tools installed, // to help when VSCode is not launched from the proper environment export function toolPathInEnv(name: string): string | undefined { - let envPath: string | undefined = process.env["PATH"]; - let envPathSplit: string[] = []; - if (envPath) { - envPathSplit = envPath.split(path.delimiter); + let envPath: string | undefined = process.env["PATH"]; + let envPathSplit: string[] = []; + if (envPath) { + envPathSplit = envPath.split(path.delimiter); + } + + // todo: if the compiler is not found in path, scan on disk and point the user to all the options + // (the concept of kit for cmake extension) + + return envPathSplit.find((p) => { + const fullPath: string = path.join(p, path.basename(name)); + if (checkFileExistsSync(fullPath)) { + return fullPath; } - - // todo: if the compiler is not found in path, scan on disk and point the user to all the options - // (the concept of kit for cmake extension) - - return envPathSplit.find(p => { - const fullPath: string = path.join(p, path.basename(name)); - if (checkFileExistsSync(fullPath)) { - return fullPath; - } - }); + }); } function taskKill(pid: number): Promise { - return new Promise((resolve, reject) => { - child_process.exec(`taskkill /pid ${pid} /T /F`, (error) => { - if (error) { - reject(error); - } else { - resolve(); - } - }); + return new Promise((resolve, reject) => { + child_process.exec(`taskkill /pid ${pid} /T /F`, (error) => { + if (error) { + reject(error); + } else { + resolve(); + } }); + }); } -export async function killTree(progress: vscode.Progress<{}>, pid: number): Promise { - if (process.platform === 'win32') { - try { - await taskKill(pid); - } catch (e) { - logger.message(`Failed to kill process ${pid}: ${e}`); - } - return; - } - - let children: number[] = []; - let stdoutStr: string = ""; - - let stdout: any = (result: string): void => { - stdoutStr += result; - }; - +export async function killTree( + progress: vscode.Progress<{}>, + pid: number +): Promise { + if (process.platform === "win32") { try { - // pgrep should run on english, regardless of the system setting. - const result: SpawnProcessResult = await spawnChildProcess('pgrep', ['-P', pid.toString()], getWorkspaceRoot(), true, false, stdout); - if (!!stdoutStr.length) { - children = stdoutStr.split('\n').map((line: string) => Number.parseInt(line)); - - logger.message(`Found children subprocesses: ${stdoutStr}.`); - for (const other of children) { - if (other) { - await killTree(progress, other); - } - } - } + await taskKill(pid); } catch (e) { - logger.message(e.message); - throw e; + logger.message(`Failed to kill process ${pid}: ${e}`); } + return; + } - try { - logger.message(`Killing process PID = ${pid}`); - progress.report({ increment: 1, message: localize("utils.terminate.process", "Terminating process PID=${0} ...", pid) }); - process.kill(pid, 'SIGINT'); - } catch (e) { - if (e.code !== 'ESRCH') { - throw e; + let children: number[] = []; + let stdoutStr: string = ""; + + let stdout: any = (result: string): void => { + stdoutStr += result; + }; + + try { + // pgrep should run on english, regardless of the system setting. + const result: SpawnProcessResult = await spawnChildProcess( + "pgrep", + ["-P", pid.toString()], + getWorkspaceRoot(), + true, + false, + stdout + ); + if (!!stdoutStr.length) { + children = stdoutStr + .split("\n") + .map((line: string) => Number.parseInt(line)); + + logger.message(`Found children subprocesses: ${stdoutStr}.`); + for (const other of children) { + if (other) { + await killTree(progress, other); } + } } + } catch (e) { + logger.message(e.message); + throw e; + } + + try { + logger.message(`Killing process PID = ${pid}`); + progress.report({ + increment: 1, + message: localize( + "utils.terminate.process", + "Terminating process PID=${0} ...", + pid + ), + }); + process.kill(pid, "SIGINT"); + } catch (e) { + if (e.code !== "ESRCH") { + throw e; + } + } } // Environment variables helpers (inspired from CMake Tools utils). -export interface EnvironmentVariables { [key: string]: string; } - -export function normalizeEnvironmentVarname(varname: string): string { - return process.platform === 'win32' ? varname.toUpperCase() : varname; +export interface EnvironmentVariables { + [key: string]: string; } -export function mergeEnvironment(...env: EnvironmentVariables[]): EnvironmentVariables { - return env.reduce((acc, vars) => { - if (process.platform === 'win32') { - // Env vars on windows are case insensitive, so we take the ones from - // active env and overwrite the ones in our current process env - const norm_vars: EnvironmentVariables = Object.getOwnPropertyNames(vars).reduce((acc2, key: string) => { - acc2[normalizeEnvironmentVarname(key)] = vars[key]; - return acc2; - }, {}); - return {...acc, ...norm_vars}; - } else { - return {...acc, ...vars}; - } - }, {}); +export function normalizeEnvironmentVarname(varname: string): string { + return process.platform === "win32" ? varname.toUpperCase() : varname; +} + +export function mergeEnvironment( + ...env: EnvironmentVariables[] +): EnvironmentVariables { + return env.reduce((acc, vars) => { + if (process.platform === "win32") { + // Env vars on windows are case insensitive, so we take the ones from + // active env and overwrite the ones in our current process env + const norm_vars: EnvironmentVariables = Object.getOwnPropertyNames( + vars + ).reduce((acc2, key: string) => { + acc2[normalizeEnvironmentVarname(key)] = vars[key]; + return acc2; + }, {}); + return { ...acc, ...norm_vars }; + } else { + return { ...acc, ...vars }; + } + }, {}); } export interface SpawnProcessResult { - returnCode: number; - signal: string; + returnCode: number; + signal: string; } // Helper to spawn a child process, hooked to callbacks that are processing stdout/stderr // forceEnglish is true when the caller relies on parsing english words from the output. export function spawnChildProcess( - processName: string, - args: string[], - workingDirectory: string, - forceEnglish: boolean, - ensureQuoted: boolean, - stdoutCallback?: (stdout: string) => void, - stderrCallback?: (stderr: string) => void): Promise { + processName: string, + args: string[], + workingDirectory: string, + forceEnglish: boolean, + ensureQuoted: boolean, + stdoutCallback?: (stdout: string) => void, + stderrCallback?: (stderr: string) => void +): Promise { + const localeOverride: EnvironmentVariables = { + LANG: "C", + LC_ALL: "C", + }; - const localeOverride: EnvironmentVariables = { - LANG: "C", - LC_ALL: "C" - }; + // Use english language for this process regardless of the system setting. + const environment: EnvironmentVariables = forceEnglish ? localeOverride : {}; + const finalEnvironment: EnvironmentVariables = mergeEnvironment( + process.env as EnvironmentVariables, + environment + ); - // Use english language for this process regardless of the system setting. - const environment: EnvironmentVariables = (forceEnglish) ? localeOverride : {}; - const finalEnvironment: EnvironmentVariables = mergeEnvironment(process.env as EnvironmentVariables, environment); + return new Promise((resolve, reject) => { + // Honor the "terminal.integrated.automationShell." setting. + // According to documentation (and settings.json schema), the three allowed values for are "windows", "linux" and "osx". + // child_process.SpawnOptions accepts a string (which can be read from the above setting) or the boolean true to let VSCode pick a default + // based on where it is running. + let shellType: string | undefined; + let shellPlatform: string = + process.platform === "win32" + ? "windows" + : process.platform === "linux" + ? "linux" + : "osx"; + let workspaceConfiguration: vscode.WorkspaceConfiguration = + vscode.workspace.getConfiguration("terminal"); + shellType = + workspaceConfiguration.get( + `integrated.automationProfile.${shellPlatform}` + ) || // automationShell is deprecated + workspaceConfiguration.get( + `integrated.automationShell.${shellPlatform}` + ); // and replaced with automationProfile - return new Promise((resolve, reject) => { - // Honor the "terminal.integrated.automationShell." setting. - // According to documentation (and settings.json schema), the three allowed values for are "windows", "linux" and "osx". - // child_process.SpawnOptions accepts a string (which can be read from the above setting) or the boolean true to let VSCode pick a default - // based on where it is running. - let shellType: string | undefined; - let shellPlatform: string = (process.platform === "win32") ? "windows" : (process.platform === "linux") ? "linux" : "osx"; - let workspaceConfiguration: vscode.WorkspaceConfiguration = vscode.workspace.getConfiguration("terminal"); - shellType = workspaceConfiguration.get(`integrated.automationProfile.${shellPlatform}`) || // automationShell is deprecated - workspaceConfiguration.get(`integrated.automationShell.${shellPlatform}`); // and replaced with automationProfile + // Final quoting decisions for process name and args before being executed. + let qProcessName: string = ensureQuoted + ? quoteStringIfNeeded(processName) + : processName; + let qArgs: string[] = ensureQuoted + ? args.map((arg) => { + return quoteStringIfNeeded(arg); + }) + : args; - // Final quoting decisions for process name and args before being executed. - let qProcessName: string = ensureQuoted ? quoteStringIfNeeded(processName) : processName; - let qArgs: string[] = ensureQuoted ? args.map(arg => { - return quoteStringIfNeeded(arg); - }) : args; + if (ensureQuoted) { + logger.message( + `Spawning child process with:\n process name: ${qProcessName}\n process args: ${qArgs}\n working directory: ${workingDirectory}\n shell type: ${ + shellType || "default" + }`, + "Debug" + ); + } - if (ensureQuoted) { - logger.message(`Spawning child process with:\n process name: ${qProcessName}\n process args: ${qArgs}\n working directory: ${workingDirectory}\n shell type: ${shellType || "default"}`, "Debug"); - } + const child: child_process.ChildProcess = child_process.spawn( + qProcessName, + qArgs, + { cwd: workingDirectory, shell: shellType || true, env: finalEnvironment } + ); + make.setCurPID(child.pid); - const child: child_process.ChildProcess = child_process.spawn(qProcessName, qArgs, - { cwd: workingDirectory, shell: shellType || true, env: finalEnvironment }); - make.setCurPID(child.pid); + if (stdoutCallback) { + child.stdout.on("data", (data) => { + stdoutCallback(`${data}`); + }); + } - if (stdoutCallback) { - child.stdout.on('data', (data) => { - stdoutCallback(`${data}`); - }); - } + if (stderrCallback) { + child.stderr.on("data", (data) => { + stderrCallback(`${data}`); + }); + } - if (stderrCallback) { - child.stderr.on('data', (data) => { - stderrCallback(`${data}`); - }); - } - - child.on('close', (returnCode: number, signal: string) => { - resolve({returnCode, signal}); - }); - - child.on('exit', (returnCode: number) => { - resolve({returnCode, signal: ""}); - }); - - if (child.pid === undefined) { - reject(new Error(`Failed to spawn process: ${processName} ${args}`)); - } + child.on("close", (returnCode: number, signal: string) => { + resolve({ returnCode, signal }); }); + + child.on("exit", (returnCode: number) => { + resolve({ returnCode, signal: "" }); + }); + + if (child.pid === undefined) { + reject(new Error(`Failed to spawn process: ${processName} ${args}`)); + } + }); } // Helper to eliminate empty items in an array export function dropNulls(items: (T | null | undefined)[]): T[] { - return items.filter(item => (item !== null && item !== undefined)) as T[]; + return items.filter((item) => item !== null && item !== undefined) as T[]; } // Convert a posix path (/home/dir1/dir2/file.ext) into windows path, @@ -346,10 +436,10 @@ export function dropNulls(items: (T | null | undefined)[]): T[] { // result: c:\msys64\home\dir1\dir2\file.ext // Called usually for Windows subsystems: MinGW, CygWin. export async function cygpath(pathStr: string): Promise { - let windowsPath: string = pathStr; + let windowsPath: string = pathStr; - let stdout: any = (result: string): void => { - windowsPath = result.replace(/\n/mg, ""); // remove the end of line + let stdout: any = (result: string): void => { + windowsPath = result.replace(/\n/gm, ""); // remove the end of line }; // Running cygpath can use the system locale. @@ -360,91 +450,104 @@ export async function cygpath(pathStr: string): Promise { // Helper that transforms a posix path (used in various non windows environments on a windows system) // into a windows style path. export async function ensureWindowsPath(path: string): Promise { - if (process.platform !== "win32" || !path.startsWith("/")) { - return path; - } + if (process.platform !== "win32" || !path.startsWith("/")) { + return path; + } - let winPath: string = path; + let winPath: string = path; - if (process.env.MSYSTEM !== undefined) { - // When in MSYS/MinGW/CygWin environments, cygpath can help transform into a windows path - // that we know CppTools will use when querying us. - winPath = await cygpath(winPath); - } else { - // Even in a pure windows environment, there are tools that may report posix paths. - // Instead of searching a cygpath tool somewhere, do the most basic transformations: + if (process.env.MSYSTEM !== undefined) { + // When in MSYS/MinGW/CygWin environments, cygpath can help transform into a windows path + // that we know CppTools will use when querying us. + winPath = await cygpath(winPath); + } else { + // Even in a pure windows environment, there are tools that may report posix paths. + // Instead of searching a cygpath tool somewhere, do the most basic transformations: - // Mount drives names like "cygdrive" or "mnt" can be ignored. - const mountDrives: string[] = ["cygdrive", "mnt"]; - for (const drv of mountDrives) { - if (winPath.startsWith(`/${drv}`)) { - winPath = winPath.substr(drv.length + 1); + // Mount drives names like "cygdrive" or "mnt" can be ignored. + const mountDrives: string[] = ["cygdrive", "mnt"]; + for (const drv of mountDrives) { + if (winPath.startsWith(`/${drv}`)) { + winPath = winPath.substr(drv.length + 1); - // Exit the loop, because we don't want to remove anything else - // in case the path happens to follow with a subfolder with the same name - // as other mountable drives for various systems/environments. - break; - } + // Exit the loop, because we don't want to remove anything else + // in case the path happens to follow with a subfolder with the same name + // as other mountable drives for various systems/environments. + break; } + } - // Remove the slash and add the : for the drive. - winPath = winPath.substr(1); - const driveEndIndex: number = winPath.search("/"); - winPath = winPath.substring(0, driveEndIndex) + ":" + winPath.substr(driveEndIndex); + // Remove the slash and add the : for the drive. + winPath = winPath.substr(1); + const driveEndIndex: number = winPath.search("/"); + winPath = + winPath.substring(0, driveEndIndex) + ":" + winPath.substr(driveEndIndex); - // Replace / with \. - winPath = winPath.replace(/\//mg, "\\"); - } + // Replace / with \. + winPath = winPath.replace(/\//gm, "\\"); + } - return winPath; + return winPath; } // Helper to reinterpret one relative path (to the given current path) printed by make as full path -export async function makeFullPath(relPath: string, curPath: string | undefined): Promise { - let fullPath: string = relPath; +export async function makeFullPath( + relPath: string, + curPath: string | undefined +): Promise { + let fullPath: string = relPath; - if (!path.isAbsolute(fullPath) && curPath) { - fullPath = path.join(curPath, relPath); - } + if (!path.isAbsolute(fullPath) && curPath) { + fullPath = path.join(curPath, relPath); + } - // For win32, ensure we have a windows style path. - fullPath = await ensureWindowsPath(fullPath); + // For win32, ensure we have a windows style path. + fullPath = await ensureWindowsPath(fullPath); - return fullPath; + return fullPath; } // Helper to reinterpret the relative paths (to the given current path) printed by make as full paths -export async function makeFullPaths(relPaths: string[], curPath: string | undefined): Promise { - let fullPaths: string[] = []; +export async function makeFullPaths( + relPaths: string[], + curPath: string | undefined +): Promise { + let fullPaths: string[] = []; - for (const p of relPaths) { - let fullPath: string = await makeFullPath(p, curPath); - fullPaths.push(fullPath); - } + for (const p of relPaths) { + let fullPath: string = await makeFullPath(p, curPath); + fullPaths.push(fullPath); + } - return fullPaths; + return fullPaths; } // Helper to reinterpret one full path as relative to the given current path -export function makeRelPath(fullPath: string, curPath: string | undefined): string { - let relPath: string = fullPath; +export function makeRelPath( + fullPath: string, + curPath: string | undefined +): string { + let relPath: string = fullPath; - if (path.isAbsolute(fullPath) && curPath) { - relPath = path.relative(curPath, fullPath); - } + if (path.isAbsolute(fullPath) && curPath) { + relPath = path.relative(curPath, fullPath); + } - return relPath; + return relPath; } // Helper to reinterpret the relative paths (to the given current path) printed by make as full paths -export function makeRelPaths(fullPaths: string[], curPath: string | undefined): string[] { - let relPaths: string[] = []; +export function makeRelPaths( + fullPaths: string[], + curPath: string | undefined +): string[] { + let relPaths: string[] = []; - fullPaths.forEach(p => { - relPaths.push(makeRelPath(p, curPath)); - }); + fullPaths.forEach((p) => { + relPaths.push(makeRelPath(p, curPath)); + }); - return fullPaths; + return fullPaths; } // Helper to remove any quotes(", ' or `) from a given string @@ -452,280 +555,333 @@ export function makeRelPaths(fullPaths: string[], curPath: string | undefined): // having quotes in the middle. const quotesStr: string[] = ["'", '"', "`"]; export function removeQuotes(str: string): string { - for (const p in quotesStr) { - if (str.includes(quotesStr[p])) { - let regExpStr: string = `${quotesStr[p]}`; - let regExp: RegExp = RegExp(regExpStr, 'g'); - str = str.replace(regExp, ""); + for (const p in quotesStr) { + if (str.includes(quotesStr[p])) { + let regExpStr: string = `${quotesStr[p]}`; + let regExp: RegExp = RegExp(regExpStr, "g"); + str = str.replace(regExp, ""); } - } + } - return str; + return str; } // Remove only the quotes (", ' or `) that are surrounding the given string. export function removeSurroundingQuotes(str: string): string { - let result: string = str.trim(); - for (const p in quotesStr) { - if (result.startsWith(quotesStr[p]) && result.endsWith(quotesStr[p])) { - result = result.substring(1, str.length - 1); - return result; - } - } + let result: string = str.trim(); + for (const p in quotesStr) { + if (result.startsWith(quotesStr[p]) && result.endsWith(quotesStr[p])) { + result = result.substring(1, str.length - 1); + return result; + } + } - return str; + return str; } // Quote given string if it contains space and is not quoted already -export function quoteStringIfNeeded(str: string) : string { - // No need to quote if there is no space or ampersand present. - if (!str.includes(" ") && !str.includes("&")) { +export function quoteStringIfNeeded(str: string): string { + // No need to quote if there is no space or ampersand present. + if (!str.includes(" ") && !str.includes("&")) { + return str; + } + + // Return if already quoted. + for (const q in quotesStr) { + if (str.startsWith(quotesStr[q]) && str.endsWith(quotesStr[q])) { return str; - } + } + } - // Return if already quoted. - for (const q in quotesStr) { - if (str.startsWith(quotesStr[q]) && str.endsWith(quotesStr[q])) { - return str; - } - } - - // Quote and return. - return `"${str}"`; + // Quote and return. + return `"${str}"`; } // Used when constructing a regular expression from file names which can contain // special characters (+, ", ...etc...). -const escapeChars: RegExp = /[\\\^\$\*\+\?\{\}\(\)\.\!\=\|\[\]\ \/]/; // characters that should be escaped. +const escapeChars: RegExp = /[\\\^\$\*\+\?\{\}\(\)\.\!\=\|\[\]\ \/]/; // characters that should be escaped. export function escapeString(str: string): string { - let escapedString: string = ""; - for (const char of str) { - if (char.match(escapeChars)) { - escapedString += `\\${char}`; - } else { - escapedString += char; - } + let escapedString: string = ""; + for (const char of str) { + if (char.match(escapeChars)) { + escapedString += `\\${char}`; + } else { + escapedString += char; } - return escapedString; + } + return escapedString; } export function elapsedTimeSince(start: number): number { - // Real elapsed times not useful in testing mode and we want to avoid diffs. - // We could alternatively disable the messages from being printed. - return (process.env['MAKEFILE_TOOLS_TESTING'] === '1') ? 0 : (Date.now() - start) / 1000; + // Real elapsed times not useful in testing mode and we want to avoid diffs. + // We could alternatively disable the messages from being printed. + return process.env["MAKEFILE_TOOLS_TESTING"] === "1" + ? 0 + : (Date.now() - start) / 1000; } // Helper to evaluate whether two settings (objects or simple types) represent the same content. // It recursively analyzes any inner subobjects and is also not affected // by a different order of properties. export function areEqual(setting1: any, setting2: any): boolean { - if (setting1 === null || setting1 === undefined || - setting2 === null || setting2 === undefined) { - return setting1 === setting2; + if ( + setting1 === null || + setting1 === undefined || + setting2 === null || + setting2 === undefined + ) { + return setting1 === setting2; + } + + // This is simply type + if ( + typeof setting1 !== "function" && + typeof setting1 !== "object" && + typeof setting2 !== "function" && + typeof setting2 !== "object" + ) { + return setting1 === setting2; + } + + let properties1: string[] = Object.getOwnPropertyNames(setting1); + let properties2: string[] = Object.getOwnPropertyNames(setting2); + + if (properties1.length !== properties2.length) { + return false; + } + + for (let p: number = 0; p < properties1.length; p++) { + let property: string = properties1[p]; + let isEqual: boolean; + if ( + typeof setting1[property] === "object" && + typeof setting2[property] === "object" + ) { + isEqual = areEqual(setting1[property], setting2[property]); + } else { + isEqual = setting1[property] === setting2[property]; } - // This is simply type - if (typeof (setting1) !== "function" && typeof (setting1) !== "object" && - typeof (setting2) !== "function" && typeof (setting2) !== "object") { - return setting1 === setting2; + if (!isEqual) { + return false; } + } - let properties1: string[] = Object.getOwnPropertyNames(setting1); - let properties2: string[] = Object.getOwnPropertyNames(setting2); - - if (properties1.length !== properties2.length) { - return false; - } - - for (let p: number = 0; p < properties1.length; p++) { - let property: string = properties1[p]; - let isEqual: boolean; - if (typeof(setting1[property]) === 'object' && typeof(setting2[property]) === 'object') { - isEqual = areEqual(setting1[property], setting2[property]); - } else { - isEqual = (setting1[property] === setting2[property]); - } - - if (!isEqual) { - return false; - } - } - - return true; + return true; } // Answers whether the given object has at least one property. export function hasProperties(obj: any): boolean { - if (obj === null || obj === undefined) { - return false; - } + if (obj === null || obj === undefined) { + return false; + } - let props: string[] = Object.getOwnPropertyNames(obj); - return props && props.length > 0; + let props: string[] = Object.getOwnPropertyNames(obj); + return props && props.length > 0; } // Apply any properties from source to destination, logging for overwrite. // To make things simpler for the caller, create a valid dst if given null or undefined. export function mergeProperties(dst: any, src: any): any { - let props: string[] = src ? Object.getOwnPropertyNames(src) : []; - props.forEach(prop => { - if (!dst) { - dst = {}; - } + let props: string[] = src ? Object.getOwnPropertyNames(src) : []; + props.forEach((prop) => { + if (!dst) { + dst = {}; + } - if (dst[prop] !== undefined) { - logger.message(`Destination object already has property ${prop} set to ${dst[prop]}. Overwriting from source with ${src[prop]}`, "Debug"); - } + if (dst[prop] !== undefined) { + logger.message( + `Destination object already has property ${prop} set to ${dst[prop]}. Overwriting from source with ${src[prop]}`, + "Debug" + ); + } - dst[prop] = src[prop]; - }); + dst[prop] = src[prop]; + }); - return dst; + return dst; } -export function removeDuplicates(src: string[]) : string[] { - let seen: {[key: string]: boolean} = {}; - let result: string[] = []; - src.forEach(item => { - if (!seen[item]) { - seen[item] = true; - result.push(item); - } - }); +export function removeDuplicates(src: string[]): string[] { + let seen: { [key: string]: boolean } = {}; + let result: string[] = []; + src.forEach((item) => { + if (!seen[item]) { + seen[item] = true; + result.push(item); + } + }); - return result; + return result; } -export function sortAndRemoveDuplicates(src: string[]) : string[] { - return removeDuplicates(src.sort()); +export function sortAndRemoveDuplicates(src: string[]): string[] { + return removeDuplicates(src.sort()); } export function reportDryRunError(dryrunOutputFile: string): void { - logger.message(`You can see the detailed dry-run output at ${dryrunOutputFile}`); - logger.message("Make sure that the extension is invoking the same make command as in your development prompt environment."); - logger.message("You may need to define or tweak a custom makefile configuration in settings via 'makefile.configurations' like described here: [link]"); - logger.message("Also make sure your code base does not have any known issues with the dry-run switches used by this extension (makefile.dryrunSwitches)."); - logger.message("If you are not able to fix the dry-run, open a GitHub issue in Makefile Tools repo: " - + "https://github.com/microsoft/vscode-makefile-tools/issues"); + logger.message( + `You can see the detailed dry-run output at ${dryrunOutputFile}` + ); + logger.message( + "Make sure that the extension is invoking the same make command as in your development prompt environment." + ); + logger.message( + "You may need to define or tweak a custom makefile configuration in settings via 'makefile.configurations' like described here: [link]" + ); + logger.message( + "Also make sure your code base does not have any known issues with the dry-run switches used by this extension (makefile.dryrunSwitches)." + ); + logger.message( + "If you are not able to fix the dry-run, open a GitHub issue in Makefile Tools repo: " + + "https://github.com/microsoft/vscode-makefile-tools/issues" + ); } // Helper to make paths absolute until the extension handles variables expansion. export function resolvePathToRoot(relPath: string): string { - if (!path.isAbsolute(relPath)) { - return path.join(getWorkspaceRoot(), relPath); - } + if (!path.isAbsolute(relPath)) { + return path.join(getWorkspaceRoot(), relPath); + } - return relPath; + return relPath; } // Return the string representing the user home location. // Inspired from CMake Tools. TODO: implement more such paths and refactor into a separate class. export function userHome(): string { - if (process.platform === 'win32') { - return path.join(process.env['HOMEDRIVE'] || 'C:', process.env['HOMEPATH'] || 'Users\\Public'); - } else { - return process.env['HOME'] || process.env['PROFILE'] || ""; - } - } + if (process.platform === "win32") { + return path.join( + process.env["HOMEDRIVE"] || "C:", + process.env["HOMEPATH"] || "Users\\Public" + ); + } else { + return process.env["HOME"] || process.env["PROFILE"] || ""; + } +} - // Helper to correctly interpret boolean values out of strings. - // Currently used during settings variable expansion. - export function booleanify(value: string) : boolean { - const truthy: string[] = ["true", "True", "1"]; - return truthy.includes(value); - } +// Helper to correctly interpret boolean values out of strings. +// Currently used during settings variable expansion. +export function booleanify(value: string): boolean { + const truthy: string[] = ["true", "True", "1"]; + return truthy.includes(value); +} // Read setting from workspace settings and expand according to various supported patterns. // Do this for the simple types (converting to boolean or numerals when the varexp syntax // is used on such types of settings) and for arrays or objects, expand recursively // until we reach the simple types for submembers. This handles any structure. -export async function getExpandedSetting(settingId: string, propSchema?: any): Promise { - let workspaceConfiguration: vscode.WorkspaceConfiguration = vscode.workspace.getConfiguration("makefile"); - let settingVal: any | undefined = workspaceConfiguration.get(settingId); +export async function getExpandedSetting( + settingId: string, + propSchema?: any +): Promise { + let workspaceConfiguration: vscode.WorkspaceConfiguration = + vscode.workspace.getConfiguration("makefile"); + let settingVal: any | undefined = workspaceConfiguration.get(settingId); - if (!propSchema) { - propSchema = thisExtensionPackage().contributes.configuration.properties; - propSchema = propSchema.properties ? propSchema.properties[`makefile.${settingId}`] : propSchema[`makefile.${settingId}`]; - } + if (!propSchema) { + propSchema = thisExtensionPackage().contributes.configuration.properties; + propSchema = propSchema.properties + ? propSchema.properties[`makefile.${settingId}`] + : propSchema[`makefile.${settingId}`]; + } - // Read what's at settingId in the workspace settings and for objects and arrays of complex types make sure - // to copy into a new counterpart that we will modify, because we don't want to persist expanded values in settings. - let copySettingVal: any | undefined; - if (propSchema && propSchema.type === "array") { - // A simple .concat() is not enough. We need to push(Object.assign) on all object entries in the array. - copySettingVal = []; - (settingVal as any[]).forEach(element => { - let copyElement: any = {}; - copyElement = (typeof(element) === "object") ? Object.assign(copyElement, element) : element; - copySettingVal.push(copyElement); - }); - } else if (propSchema && propSchema.type === "object") { - copySettingVal = {}; - copySettingVal = Object.assign(copySettingVal, settingVal); - } else { - copySettingVal = settingVal; - } + // Read what's at settingId in the workspace settings and for objects and arrays of complex types make sure + // to copy into a new counterpart that we will modify, because we don't want to persist expanded values in settings. + let copySettingVal: any | undefined; + if (propSchema && propSchema.type === "array") { + // A simple .concat() is not enough. We need to push(Object.assign) on all object entries in the array. + copySettingVal = []; + (settingVal as any[]).forEach((element) => { + let copyElement: any = {}; + copyElement = + typeof element === "object" + ? Object.assign(copyElement, element) + : element; + copySettingVal.push(copyElement); + }); + } else if (propSchema && propSchema.type === "object") { + copySettingVal = {}; + copySettingVal = Object.assign(copySettingVal, settingVal); + } else { + copySettingVal = settingVal; + } - return getExpandedSettingVal(settingId, copySettingVal, propSchema); + return getExpandedSettingVal(settingId, copySettingVal, propSchema); } // Same as above but read from an object instead of the settings (as if we get<> before calling this). // Such approach was needed for tests. -export async function getExpandedSettingVal(settingId: string, settingVal: any, propSchema?: any): Promise { - // Currently, we have no ${} variables in the default values of our settings. - // Skip expanding defaults to keep things faster simpler and safer. - // Change this when needed. - const typeJson: string | undefined = propSchema ? propSchema.type : undefined; - if (settingVal !== undefined && - ((propSchema && !areEqual(propSchema.default, settingVal)) || - !propSchema)) { // This OR is for variables not defined in the extension package.json - // but the user can define any variable in settings.json to reference later - if (typeof(settingVal) === 'string') { - const expandedVal: string = await expandVariablesInSetting(settingId, settingVal); - let result: T = expandedVal as T; - if (typeJson === "boolean") { - result = booleanify(expandedVal) as T; - } else if (typeJson === "number" || typeJson === "integer") { - result = Number(expandedVal) as T; - } +export async function getExpandedSettingVal( + settingId: string, + settingVal: any, + propSchema?: any +): Promise { + // Currently, we have no ${} variables in the default values of our settings. + // Skip expanding defaults to keep things faster simpler and safer. + // Change this when needed. + const typeJson: string | undefined = propSchema ? propSchema.type : undefined; + if ( + settingVal !== undefined && + ((propSchema && !areEqual(propSchema.default, settingVal)) || !propSchema) + ) { + // This OR is for variables not defined in the extension package.json + // but the user can define any variable in settings.json to reference later + if (typeof settingVal === "string") { + const expandedVal: string = await expandVariablesInSetting( + settingId, + settingVal + ); + let result: T = expandedVal as T; + if (typeJson === "boolean") { + result = booleanify(expandedVal) as T; + } else if (typeJson === "number" || typeJson === "integer") { + result = Number(expandedVal) as T; + } - return result; - } else if (typeof(settingVal) === 'object') { - // arrays are also seen as objects: - // example: array[5] is seen as property object with index array.5 - // and at the next call we'll see the string. - let properties: string[] = Object.getOwnPropertyNames(settingVal); - for (let p: number = 0; p < properties.length; p++) { - let prop: string = properties[p]; - let childPropSchema: any; - if (propSchema) { - if (typeJson === "array") { - childPropSchema = propSchema.items; - } else { - childPropSchema = propSchema.properties ? propSchema.properties[`${prop}`] : propSchema[`${prop}`]; - } - } - - try { - // The settingVal that was given to this function was already a separate copy from its workspace settings counterpart - // but if that contained an array anywhere in its structure, if we don't copy here, this expansion will modify - // workspace settings which we want to leave untouched. - let copySettingValProp: any = settingVal[prop]; - if (childPropSchema && childPropSchema.type === "array") { - copySettingValProp = [].concat(settingVal[prop]); - } - - let expandedProp: T = await getExpandedSettingVal(settingId + "." + prop, copySettingValProp, childPropSchema); - if (!areEqual(settingVal[prop], expandedProp)) { - settingVal[prop] = expandedProp; - } - } catch (e) { - logger.message(`Exception while expanding string "${settingId}.${prop}": '${e.message}'`); - } - } + return result; + } else if (typeof settingVal === "object") { + // arrays are also seen as objects: + // example: array[5] is seen as property object with index array.5 + // and at the next call we'll see the string. + let properties: string[] = Object.getOwnPropertyNames(settingVal); + for (let p: number = 0; p < properties.length; p++) { + let prop: string = properties[p]; + let childPropSchema: any; + if (propSchema) { + if (typeJson === "array") { + childPropSchema = propSchema.items; + } else { + childPropSchema = propSchema.properties + ? propSchema.properties[`${prop}`] + : propSchema[`${prop}`]; + } } - } - return settingVal; + try { + // The settingVal that was given to this function was already a separate copy from its workspace settings counterpart + // but if that contained an array anywhere in its structure, if we don't copy here, this expansion will modify + // workspace settings which we want to leave untouched. + let copySettingValProp: any = settingVal[prop]; + if (childPropSchema && childPropSchema.type === "array") { + copySettingValProp = [].concat(settingVal[prop]); + } + + let expandedProp: T = await getExpandedSettingVal< + typeof childPropSchema + >(settingId + "." + prop, copySettingValProp, childPropSchema); + if (!areEqual(settingVal[prop], expandedProp)) { + settingVal[prop] = expandedProp; + } + } catch (e) { + logger.message( + `Exception while expanding string "${settingId}.${prop}": '${e.message}'` + ); + } + } + } + } + + return settingVal; } // Helper for expanding variables in a setting. The following scenarios are currently supported: @@ -756,126 +912,141 @@ export async function getExpandedSettingVal(settingId: string, settingVal: an // TODO: Currently, after applying any expansion pattern, if the result is another expansion pattern // we log an error but in future let's handle the recursivity and complications of expanding anything // coming via this entrypoint. -export async function expandVariablesInSetting(settingId: string, settingVal: string): Promise { - // Do some string preprocessing first, related to escaping. - // Since we don't want to change the value persisted in settings but we need to lose the separator - // (so that the final beneficiaries of these settings don't need to handle the separator character) - // we will keep the varexp pattern in the final value without the escape character. - // The escape character is only for our regexp here to know to not expand it. - // Safe to replace \\${ with ESCAPED_VARIABLE_EXPANSION. This will cause the pattern to be skipped - // by the regular expression below and also we will replace in reverse at the end (without \\). - const telemetryProperties: telemetry.Properties = {setting: settingId}; - let preprocStr: string = settingVal.replace(/\\\$\{/mg, "ESCAPED_VARIABLE_EXPANSION"); - if (preprocStr !== settingVal) { - logger.message(`Detected escaped variable expansion patterns in setting '${settingId}', within value '${settingVal}'.`); - telemetryProperties.pattern = "escaped"; - telemetry.logEvent("varexp", telemetryProperties); - settingVal = preprocStr; - } +export async function expandVariablesInSetting( + settingId: string, + settingVal: string +): Promise { + // Do some string preprocessing first, related to escaping. + // Since we don't want to change the value persisted in settings but we need to lose the separator + // (so that the final beneficiaries of these settings don't need to handle the separator character) + // we will keep the varexp pattern in the final value without the escape character. + // The escape character is only for our regexp here to know to not expand it. + // Safe to replace \\${ with ESCAPED_VARIABLE_EXPANSION. This will cause the pattern to be skipped + // by the regular expression below and also we will replace in reverse at the end (without \\). + const telemetryProperties: telemetry.Properties = { setting: settingId }; + let preprocStr: string = settingVal.replace( + /\\\$\{/gm, + "ESCAPED_VARIABLE_EXPANSION" + ); + if (preprocStr !== settingVal) { + logger.message( + `Detected escaped variable expansion patterns in setting '${settingId}', within value '${settingVal}'.` + ); + telemetryProperties.pattern = "escaped"; + telemetry.logEvent("varexp", telemetryProperties); + settingVal = preprocStr; + } - // Try the predefined VSCode variable first. The regexp for ${variable} won't fit the others because of the ":". - let expandedSetting: string = settingVal; - let regexpVSCodeVar: RegExp = /(\$\{(\w+)\})|(\$\{(\w+):(.+?)\})/mg; - let result: RegExpExecArray | null = regexpVSCodeVar.exec(expandedSetting); - while (result) { - const telemetryProperties: telemetry.Properties = {setting: settingId}; - let toStr: string = ""; - if (result[2] === "workspaceFolder" || result[2] === "workspaceRoot") { - toStr = getWorkspaceRoot(); - telemetryProperties.pattern = result[2]; - } else if (result[2] === "workspaceFolderBasename") { - toStr = path.basename(getWorkspaceRoot()); - telemetryProperties.pattern = result[2]; - } else if (result[2] === "userHome") { - toStr = userHome(); - telemetryProperties.pattern = result[2]; - } else if (result[2] === "configuration") { - toStr = configuration.getCurrentMakefileConfiguration(); - telemetryProperties.pattern = result[2]; - } else if (result[2] === "buildTarget") { - toStr = configuration.getCurrentTarget() || ""; - telemetryProperties.pattern = result[2]; - } else if (result[4] === "env" && result[5]) { - toStr = process.env[result[5]] || ""; - telemetryProperties.pattern = result[4]; - } else if (result[4] === "command") { - telemetryProperties.pattern = result[4]; - telemetryProperties.info = result[5]; - try { - toStr = await vscode.commands.executeCommand(result[5]); - } catch (e) { - toStr = "unknown"; - logger.message(`Exception while executing command "${result[5]}": '${e.message}'`); - } - } else if (result[4] === "config" && result[5]) { - // Extract the name of the extension we read this setting from (before the dot) - // and the setting follows the first dot. - telemetryProperties.pattern = result[4]; - telemetryProperties.info = result[5]; - const regexpCfg: RegExp = /(\w+)\.(.+)/mg; - const res: RegExpExecArray | null = regexpCfg.exec(result[5]); - if (res && res[1] && res[2]) { - let workspaceCfg: vscode.WorkspaceConfiguration = vscode.workspace.getConfiguration(res[1]); - toStr = (workspaceCfg.get(res[2]) as string); + // Try the predefined VSCode variable first. The regexp for ${variable} won't fit the others because of the ":". + let expandedSetting: string = settingVal; + let regexpVSCodeVar: RegExp = /(\$\{(\w+)\})|(\$\{(\w+):(.+?)\})/gm; + let result: RegExpExecArray | null = regexpVSCodeVar.exec(expandedSetting); + while (result) { + const telemetryProperties: telemetry.Properties = { setting: settingId }; + let toStr: string = ""; + if (result[2] === "workspaceFolder" || result[2] === "workspaceRoot") { + toStr = getWorkspaceRoot(); + telemetryProperties.pattern = result[2]; + } else if (result[2] === "workspaceFolderBasename") { + toStr = path.basename(getWorkspaceRoot()); + telemetryProperties.pattern = result[2]; + } else if (result[2] === "userHome") { + toStr = userHome(); + telemetryProperties.pattern = result[2]; + } else if (result[2] === "configuration") { + toStr = configuration.getCurrentMakefileConfiguration(); + telemetryProperties.pattern = result[2]; + } else if (result[2] === "buildTarget") { + toStr = configuration.getCurrentTarget() || ""; + telemetryProperties.pattern = result[2]; + } else if (result[4] === "env" && result[5]) { + toStr = process.env[result[5]] || ""; + telemetryProperties.pattern = result[4]; + } else if (result[4] === "command") { + telemetryProperties.pattern = result[4]; + telemetryProperties.info = result[5]; + try { + toStr = await vscode.commands.executeCommand(result[5]); + } catch (e) { + toStr = "unknown"; + logger.message( + `Exception while executing command "${result[5]}": '${e.message}'` + ); + } + } else if (result[4] === "config" && result[5]) { + // Extract the name of the extension we read this setting from (before the dot) + // and the setting follows the first dot. + telemetryProperties.pattern = result[4]; + telemetryProperties.info = result[5]; + const regexpCfg: RegExp = /(\w+)\.(.+)/gm; + const res: RegExpExecArray | null = regexpCfg.exec(result[5]); + if (res && res[1] && res[2]) { + let workspaceCfg: vscode.WorkspaceConfiguration = + vscode.workspace.getConfiguration(res[1]); + toStr = workspaceCfg.get(res[2]) as string; - // The setting is either undefined or maybe we encountered a case with multiple names separated by dot for a property: - // makefile.set1.set2.set3.set4... which cannot be seen if given the whole setting ID at once. - // Example: - // "makefile.set1.set2.set3": { - // "set4.set5": "val" - // "something.else": "other" - // } - // A get on the root workspace cannot see "makefile.set1.set2.set3.set4.set5", returns undefined. - // In the above case, one get of "makefile.set1.set2.set3" returns an object, then an access on "set4.set5" gets the final value "val". - // We don't know at which dot to stop for the first and the subsequent get operations, so starting with the workspace root - // we query for properties and see how much it matches from the full setting id, then we query again on the left over, - // until we get the final value. - // In the above case, the root makefile workspace has a property set1 (not set1.set2.set3), then the object retrieved - // has a set2 property then set3. That last object has a "set4.set5" property (not set4 then set5). - if (toStr === null || toStr === undefined) { - toStr = getSettingMultipleDots(workspaceCfg, res[2]); - } - - if (toStr === null || toStr === undefined) { - toStr = "unknown"; - } - } - } else { - logger.message(`Unrecognized variable format: ${result[0]}`); - toStr = "unknown"; - telemetryProperties.pattern = "unrecognized"; + // The setting is either undefined or maybe we encountered a case with multiple names separated by dot for a property: + // makefile.set1.set2.set3.set4... which cannot be seen if given the whole setting ID at once. + // Example: + // "makefile.set1.set2.set3": { + // "set4.set5": "val" + // "something.else": "other" + // } + // A get on the root workspace cannot see "makefile.set1.set2.set3.set4.set5", returns undefined. + // In the above case, one get of "makefile.set1.set2.set3" returns an object, then an access on "set4.set5" gets the final value "val". + // We don't know at which dot to stop for the first and the subsequent get operations, so starting with the workspace root + // we query for properties and see how much it matches from the full setting id, then we query again on the left over, + // until we get the final value. + // In the above case, the root makefile workspace has a property set1 (not set1.set2.set3), then the object retrieved + // has a set2 property then set3. That last object has a "set4.set5" property (not set4 then set5). + if (toStr === null || toStr === undefined) { + toStr = getSettingMultipleDots(workspaceCfg, res[2]); } - telemetry.logEvent("varexp", telemetryProperties); - - // Because we replace at the same time as we evaluate possible consecutive $ patterns - // we need to start each time the search from the beginning (otherwise the lastIndex gets messed up). - // It is guaranteed we exit this loop because if we match, we replace with something. - // That is why we cannot leave the ${} as they are and we replace with "unknown" when they can't resolve. - // Replacing with empty string was not an option because we want unrecognized patterns to stand out quickly. - regexpVSCodeVar.lastIndex = 0; - - // Warn if the expanded value contains yet another expansion pattern and leave as is. - // We will address in future multiple passes. - if (regexpVSCodeVar.exec(toStr) !== null) { - logger.message(`"${result[0]}" resolves to "${toStr}" which requires another expansion.` + - " We will support multiple expansion passes in the future. "); - expandedSetting = expandedSetting.replace(result[0], "unknown"); - } else { - expandedSetting = expandedSetting.replace(result[0], toStr); + if (toStr === null || toStr === undefined) { + toStr = "unknown"; } - - regexpVSCodeVar.lastIndex = 0; - result = regexpVSCodeVar.exec(expandedSetting); + } + } else { + logger.message(`Unrecognized variable format: ${result[0]}`); + toStr = "unknown"; + telemetryProperties.pattern = "unrecognized"; } - if (expandedSetting !== settingVal) { - logger.message(`Expanding from '${settingVal}' to '${expandedSetting}' for setting '${settingId}'.`); + telemetry.logEvent("varexp", telemetryProperties); + + // Because we replace at the same time as we evaluate possible consecutive $ patterns + // we need to start each time the search from the beginning (otherwise the lastIndex gets messed up). + // It is guaranteed we exit this loop because if we match, we replace with something. + // That is why we cannot leave the ${} as they are and we replace with "unknown" when they can't resolve. + // Replacing with empty string was not an option because we want unrecognized patterns to stand out quickly. + regexpVSCodeVar.lastIndex = 0; + + // Warn if the expanded value contains yet another expansion pattern and leave as is. + // We will address in future multiple passes. + if (regexpVSCodeVar.exec(toStr) !== null) { + logger.message( + `"${result[0]}" resolves to "${toStr}" which requires another expansion.` + + " We will support multiple expansion passes in the future. " + ); + expandedSetting = expandedSetting.replace(result[0], "unknown"); + } else { + expandedSetting = expandedSetting.replace(result[0], toStr); } - // Reverse the preprocessing done at the beginning, except that we don't keep the escape character. - preprocStr = expandedSetting.replace(/ESCAPED_VARIABLE_EXPANSION/mg, "${"); - return preprocStr; + regexpVSCodeVar.lastIndex = 0; + result = regexpVSCodeVar.exec(expandedSetting); + } + + if (expandedSetting !== settingVal) { + logger.message( + `Expanding from '${settingVal}' to '${expandedSetting}' for setting '${settingId}'.` + ); + } + + // Reverse the preprocessing done at the beginning, except that we don't keep the escape character. + preprocStr = expandedSetting.replace(/ESCAPED_VARIABLE_EXPANSION/gm, "${"); + return preprocStr; } // Function specialized to get properties with multiple dots in their names. @@ -887,77 +1058,86 @@ export async function expandVariablesInSetting(settingId: string, settingVal: st // } // getSettingMultipleDots will return "val2" for "makefile.set1.set2.set3.set4.set7.set8" // and workspaceConfiguration.get<> will not see it as a whole. -function getSettingMultipleDots(scope: any, settingId: string) : any { - let result: any; - if (scope) { - let rootProps: string[] = Object.getOwnPropertyNames(scope); - rootProps = rootProps.filter(item => (item && (settingId.startsWith(`${item}.`) || settingId === item))); - rootProps.forEach(prop => { - if (settingId === prop) { - result = scope[prop]; - } else { - result = getSettingMultipleDots(scope[prop], settingId.substring(prop.length + 1, settingId.length)); - } - }); - } +function getSettingMultipleDots(scope: any, settingId: string): any { + let result: any; + if (scope) { + let rootProps: string[] = Object.getOwnPropertyNames(scope); + rootProps = rootProps.filter( + (item) => item && (settingId.startsWith(`${item}.`) || settingId === item) + ); + rootProps.forEach((prop) => { + if (settingId === prop) { + result = scope[prop]; + } else { + result = getSettingMultipleDots( + scope[prop], + settingId.substring(prop.length + 1, settingId.length) + ); + } + }); + } - return result; + return result; } // Schedule a task to be run at some future time. This allows other pending tasks to // execute ahead of the scheduled task and provides a form of async behavior for TypeScript. export function scheduleTask(task: () => T): Promise { - return new Promise((resolve, reject) => { - setImmediate(() => { - try { - const result: T = task(); - resolve(result); - } catch (e) { - reject(e); - } - }); + return new Promise((resolve, reject) => { + setImmediate(() => { + try { + const result: T = task(); + resolve(result); + } catch (e) { + reject(e); + } }); + }); } // Async version of scheduleTask export async function scheduleAsyncTask(task: () => Promise): Promise { - return new Promise((resolve, reject) => { - setImmediate(async () => { - try { - const result: T = await task(); - resolve(result); - } catch (e) { - reject(e); - } - }); - }); + return new Promise((resolve, reject) => { + setImmediate(async () => { + try { + const result: T = await task(); + resolve(result); + } catch (e) { + reject(e); + } + }); + }); } export function thisExtension(): vscode.Extension { - const ext: vscode.Extension | undefined = vscode.extensions.getExtension('ms-vscode.makefile-tools'); - if (!ext) { - throw new Error("Our own extension is null."); - } + const ext: vscode.Extension | undefined = vscode.extensions.getExtension( + "ms-vscode.makefile-tools" + ); + if (!ext) { + throw new Error("Our own extension is null."); + } - return ext; + return ext; } export interface PackageJSON { - name: string; - publisher: string; - version: string; - contributes: any; + name: string; + publisher: string; + version: string; + contributes: any; } export function thisExtensionPackage(): PackageJSON { - const pkg: PackageJSON = thisExtension().packageJSON as PackageJSON; + const pkg: PackageJSON = thisExtension().packageJSON as PackageJSON; - return { - name: pkg.name, - publisher: pkg.publisher, - version: pkg.version, - contributes: pkg.contributes - }; + return { + name: pkg.name, + publisher: pkg.publisher, + version: pkg.version, + contributes: pkg.contributes, + }; } -export function thisExtensionPath(): string { return thisExtension().extensionPath; } +export function thisExtensionPath(): string { + return thisExtension().extensionPath; +} diff --git a/translations_auto_pr.js b/translations_auto_pr.js index dceb851..539d182 100644 --- a/translations_auto_pr.js +++ b/translations_auto_pr.js @@ -1,15 +1,15 @@ -'use strict' +"use strict"; const fs = require("fs-extra"); const cp = require("child_process"); -const Octokit = require('@octokit/rest') -const path = require('path'); -const parseGitConfig = require('parse-git-config'); +const Octokit = require("@octokit/rest"); +const path = require("path"); +const parseGitConfig = require("parse-git-config"); -const branchName = 'localization'; -const mergeTo = 'main'; -const commitComment = 'Localization - Translated Strings'; -const pullRequestTitle = '[Auto] Localization - Translated Strings'; +const branchName = "localization"; +const mergeTo = "main"; +const commitComment = "Localization - Translated Strings"; +const pullRequestTitle = "[Auto] Localization - Translated Strings"; let repoOwner = process.argv[2]; let repoName = process.argv[3]; @@ -20,17 +20,44 @@ let userEmail = process.argv[7]; let locRootPath = process.argv[8]; let locSubPath = process.argv[9]; -if (!repoOwner || !repoName || !authUser || !authToken || !userFullName || !userEmail || !locRootPath || !locSubPath) { - console.error(`ERROR: Usage: ${path.parse(process.argv[0]).base} ${path.parse(process.argv[1]).base} repo_owner repo_name auth_token user_full_name user_email loc_root_path loc_sub_path`); - console.error(` repo_owner - The owner of the repo on GitHub. i.e. microsoft`); - console.error(` repo_name - The name of the repo on GitHub. i.e. vscode-makefile-tools`); - console.error(` auth_user - User account wiith permission to post a pull request against the GitHub repo.`); - console.error(` auth_token - A PAT associated with auth_user.`); - console.error(` user_full_name - A full name to associate with a git commit. (This is replaced by the PR account if commit is squashed.)`); - console.error(` user_email - An email to associate with a git commit. (This is replaced by the PR account if commit is squashed.)`); - console.error(` loc_root_path - The path to the folder with language-specific directories (containing localized xlf files).`); - console.error(` loc_sub_path - A sub-path after the language-specific directory, where the xlf to import is located. This should not include the name of the xlf file to import.)`); - return; +if ( + !repoOwner || + !repoName || + !authUser || + !authToken || + !userFullName || + !userEmail || + !locRootPath || + !locSubPath +) { + console.error( + `ERROR: Usage: ${path.parse(process.argv[0]).base} ${ + path.parse(process.argv[1]).base + } repo_owner repo_name auth_token user_full_name user_email loc_root_path loc_sub_path` + ); + console.error( + ` repo_owner - The owner of the repo on GitHub. i.e. microsoft` + ); + console.error( + ` repo_name - The name of the repo on GitHub. i.e. vscode-makefile-tools` + ); + console.error( + ` auth_user - User account wiith permission to post a pull request against the GitHub repo.` + ); + console.error(` auth_token - A PAT associated with auth_user.`); + console.error( + ` user_full_name - A full name to associate with a git commit. (This is replaced by the PR account if commit is squashed.)` + ); + console.error( + ` user_email - An email to associate with a git commit. (This is replaced by the PR account if commit is squashed.)` + ); + console.error( + ` loc_root_path - The path to the folder with language-specific directories (containing localized xlf files).` + ); + console.error( + ` loc_sub_path - A sub-path after the language-specific directory, where the xlf to import is located. This should not include the name of the xlf file to import.)` + ); + return; } console.log(`repoOwner=${repoOwner}`); @@ -42,86 +69,112 @@ console.log(`locRootPath=${locRootPath}`); console.log(`locSubPath=${locSubPath}`); function hasBranch(branchName) { - console.log(`Checking for existence of branch "${branchName}" (git branch --list ${branchName})`); - let output = cp.execSync(`git branch --list ${branchName}`); - let lines = output.toString().split("\n"); - let found = false; - lines.forEach(line => { - found = found || (line === ` ${branchName}`); - }); + console.log( + `Checking for existence of branch "${branchName}" (git branch --list ${branchName})` + ); + let output = cp.execSync(`git branch --list ${branchName}`); + let lines = output.toString().split("\n"); + let found = false; + lines.forEach((line) => { + found = found || line === ` ${branchName}`; + }); - return found; + return found; } function hasAnyChanges() { - console.log("Checking if any files have changed (git status --porcelain)"); - let output = cp.execSync('git status --porcelain'); - let lines = output.toString().split("\n"); - let anyChanges = false; - lines.forEach(line => { - if (line != '') { - console.log("Change detected: " + line); - anyChanges = true; - } - }); + console.log("Checking if any files have changed (git status --porcelain)"); + let output = cp.execSync("git status --porcelain"); + let lines = output.toString().split("\n"); + let anyChanges = false; + lines.forEach((line) => { + if (line != "") { + console.log("Change detected: " + line); + anyChanges = true; + } + }); - return anyChanges; + return anyChanges; } // When invoked on build server, we should already be in a repo freshly synced to the mergeTo branch if (hasAnyChanges()) { - console.log(`Changes already present in this repo! This script is intended to be run against a freshly synced ${mergeTo} branch!`); - return; + console.log( + `Changes already present in this repo! This script is intended to be run against a freshly synced ${mergeTo} branch!` + ); + return; } function sleep(ms) { - var unixtime_ms = new Date().getTime(); - while(new Date().getTime() < unixtime_ms + ms) {} + var unixtime_ms = new Date().getTime(); + while (new Date().getTime() < unixtime_ms + ms) {} } -console.log("This script is potentially DESTRUCTIVE! Cancel now, or it will proceed in 10 seconds."); +console.log( + "This script is potentially DESTRUCTIVE! Cancel now, or it will proceed in 10 seconds." +); sleep(10000); -let directories = [ "cs", "de", "es", "fr", "it", "ja", "ko", "pl", "pt-BR", "ru", "tr", "zh-Hans", "zh-Hant" ]; -directories.forEach(languageId => { - let sourcePath = `${locRootPath}\\${languageId}\\${locSubPath}\\${repoName}.${languageId}.xlf`; - let destinationPath = `./vscode-translations-import/${languageId}/vscode-extensions/${repoName}.xlf`; - console.log(`Copying "${sourcePath}" to "${destinationPath}"`); - fs.copySync(sourcePath, destinationPath); +let directories = [ + "cs", + "de", + "es", + "fr", + "it", + "ja", + "ko", + "pl", + "pt-BR", + "ru", + "tr", + "zh-Hans", + "zh-Hant", +]; +directories.forEach((languageId) => { + let sourcePath = `${locRootPath}\\${languageId}\\${locSubPath}\\${repoName}.${languageId}.xlf`; + let destinationPath = `./vscode-translations-import/${languageId}/vscode-extensions/${repoName}.xlf`; + console.log(`Copying "${sourcePath}" to "${destinationPath}"`); + fs.copySync(sourcePath, destinationPath); }); console.log("Import translations into i18n directory"); cp.execSync("npm run translations-import"); if (!hasAnyChanges()) { - console.log("No changes detected"); - return; + console.log("No changes detected"); + return; } console.log("Changes detected"); console.log(`Ensure main ref is up to date locally (git fetch)`); -cp.execSync('git fetch'); +cp.execSync("git fetch"); // Remove old localization branch, if any if (hasBranch("localization")) { - console.log(`Remove old localization branch, if any (git branch -D localization)`); - cp.execSync('git branch -D localization'); + console.log( + `Remove old localization branch, if any (git branch -D localization)` + ); + cp.execSync("git branch -D localization"); } // Check out local branch -console.log(`Creating local branch for changes (git checkout -b ${branchName})`); -cp.execSync('git checkout -b localization'); +console.log( + `Creating local branch for changes (git checkout -b ${branchName})` +); +cp.execSync("git checkout -b localization"); // Add changed files. console.log("Adding changed file (git add .)"); -cp.execSync('git add .'); +cp.execSync("git add ."); // git add may have resolves CR/LF's and there may not be anything to commit if (!hasAnyChanges()) { - console.log("No changes detected. The only changes must have been due to CR/LF's, and have been corrected."); - return; + console.log( + "No changes detected. The only changes must have been due to CR/LF's, and have been corrected." + ); + return; } // Set up user and permissions @@ -129,24 +182,24 @@ if (!hasAnyChanges()) { // Save existing user name and email, in case already set. var existingUserName; var existingUserEmail; -var gitConfigPath = path.resolve(process.cwd(), './.git/config'); +var gitConfigPath = path.resolve(process.cwd(), "./.git/config"); var config = parseGitConfig.sync({ path: gitConfigPath }); -if (typeof config === 'object' && config.hasOwnProperty('user')) { - existingUserName = config.user.name; - existingUserEmail = config.user.email; +if (typeof config === "object" && config.hasOwnProperty("user")) { + existingUserName = config.user.name; + existingUserEmail = config.user.email; } if (existingUserName === undefined) { - console.log(`Existing user name: undefined`); + console.log(`Existing user name: undefined`); } else { - console.log(`Existing user name: "${existingUserName}"`); - cp.execSync(`git config --local --unset user.name`); + console.log(`Existing user name: "${existingUserName}"`); + cp.execSync(`git config --local --unset user.name`); } if (existingUserEmail === undefined) { - console.log(`Existing user email: undefined`); + console.log(`Existing user email: undefined`); } else { - console.log(`Existing user email: "${existingUserEmail}"`); - cp.execSync(`git config --local --unset user.email`); + console.log(`Existing user email: "${existingUserEmail}"`); + cp.execSync(`git config --local --unset user.email`); } console.log(`Setting local user name to: "${userFullName}"`); @@ -155,61 +208,75 @@ cp.execSync(`git config --local user.name "${userFullName}"`); console.log(`Setting local user email to: "${userEmail}"`); cp.execSync(`git config --local user.email "${userEmail}"`); -console.log(`Configuring git with permission to push and to create pull requests (git remote remove origin && git remote add origin https://${authUser}:${authToken}@github.com/${repoOwner}/${repoName}.git`); -cp.execSync('git remote remove origin'); -cp.execSync(`git remote add origin https://${authUser}:${authToken}@github.com/${repoOwner}/${repoName}.git`); +console.log( + `Configuring git with permission to push and to create pull requests (git remote remove origin && git remote add origin https://${authUser}:${authToken}@github.com/${repoOwner}/${repoName}.git` +); +cp.execSync("git remote remove origin"); +cp.execSync( + `git remote add origin https://${authUser}:${authToken}@github.com/${repoOwner}/${repoName}.git` +); // Commit changed files. console.log(`Commiting changes (git commit -m "${commitComment}")`); cp.execSync(`git commit -m "${commitComment}"`); if (existingUserName === undefined) { - console.log(`Restoring original user name: undefined`); - cp.execSync(`git config --local --unset user.name`); + console.log(`Restoring original user name: undefined`); + cp.execSync(`git config --local --unset user.name`); } else { - console.log(`Restoring original user name: "${existingUserName}"`); - cp.execSync(`git config --local user.name "${existingUserName}"`); + console.log(`Restoring original user name: "${existingUserName}"`); + cp.execSync(`git config --local user.name "${existingUserName}"`); } if (existingUserEmail === undefined) { - console.log(`Restoring original user email: undefined`); - cp.execSync(`git config --local --unset user.email`); + console.log(`Restoring original user email: undefined`); + cp.execSync(`git config --local --unset user.email`); } else { - console.log(`Restoring original user email: "${existingUserEmail}"`); - cp.execSync(`git config --local user.email "${existingUserEmail}"`); + console.log(`Restoring original user email: "${existingUserEmail}"`); + cp.execSync(`git config --local user.email "${existingUserEmail}"`); } console.log(`pushing to remove branch (git push -f origin ${branchName})`); cp.execSync(`git push -f origin ${branchName}`); console.log("Checking if there is already a pull request..."); -const octokit = new Octokit.Octokit({auth: authToken}); -octokit.pulls.list({ owner: repoOwner, repo: repoName }).then(({data}) => { - let alreadyHasPullRequest = false; - if (data) { - data.forEach((pr) => { - alreadyHasPullRequest = alreadyHasPullRequest || (pr.title === pullRequestTitle); - }); - } +const octokit = new Octokit.Octokit({ auth: authToken }); +octokit.pulls.list({ owner: repoOwner, repo: repoName }).then(({ data }) => { + let alreadyHasPullRequest = false; + if (data) { + data.forEach((pr) => { + alreadyHasPullRequest = + alreadyHasPullRequest || pr.title === pullRequestTitle; + }); + } - // If not already present, create a PR against our remote branch. - if (!alreadyHasPullRequest) { - console.log("There is not already a pull request. Creating one."); - octokit.pulls.create({ body:"", owner: repoOwner, repo: repoName, title: pullRequestTitle, head: branchName, base: mergeTo }); - } else { - console.log("There is already a pull request."); - } + // If not already present, create a PR against our remote branch. + if (!alreadyHasPullRequest) { + console.log("There is not already a pull request. Creating one."); + octokit.pulls.create({ + body: "", + owner: repoOwner, + repo: repoName, + title: pullRequestTitle, + head: branchName, + base: mergeTo, + }); + } else { + console.log("There is already a pull request."); + } - console.log(`Restoring default git permissions`); - cp.execSync('git remote remove origin'); - cp.execSync(`git remote add origin https://github.com/${repoOwner}/${repoName}.git`); + console.log(`Restoring default git permissions`); + cp.execSync("git remote remove origin"); + cp.execSync( + `git remote add origin https://github.com/${repoOwner}/${repoName}.git` + ); - console.log(`Run 'git fetch' against updated remote`); - cp.execSync('git fetch'); + console.log(`Run 'git fetch' against updated remote`); + cp.execSync("git fetch"); - console.log(`Switching back to ${mergeTo} (git checkout ${mergeTo})`); - cp.execSync(`git checkout ${mergeTo}`); + console.log(`Switching back to ${mergeTo} (git checkout ${mergeTo})`); + cp.execSync(`git checkout ${mergeTo}`); - console.log(`Remove localization branch (git branch -D localization)`); - cp.execSync('git branch -D localization'); + console.log(`Remove localization branch (git branch -D localization)`); + cp.execSync("git branch -D localization"); }); diff --git a/tsconfig.json b/tsconfig.json index 63a2d97..e88303e 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,28 +1,21 @@ { - "compilerOptions": { - "module": "commonjs", - "target": "es2019", - "outDir": "out", - "alwaysStrict":true, - "baseUrl": "./", - "lib": [ - "es2019", - ], - "sourceMap": true, - "rootDir": ".", - "useUnknownInCatchVariables": false, - "strict": true, /* enable all strict type-checking options */ - "forceConsistentCasingInFileNames": true - /* Additional Checks */ - // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ - // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ - // "noUnusedParameters": true, /* Report errors on unused parameters. */, - }, - "exclude": [ - "node_modules", - ".vscode-test" - ], - "include": [ - "**/*.ts", - ] + "compilerOptions": { + "module": "commonjs", + "target": "es2019", + "outDir": "out", + "alwaysStrict": true, + "baseUrl": "./", + "lib": ["es2019"], + "sourceMap": true, + "rootDir": ".", + "useUnknownInCatchVariables": false, + "strict": true /* enable all strict type-checking options */, + "forceConsistentCasingInFileNames": true + /* Additional Checks */ + // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ + // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ + // "noUnusedParameters": true, /* Report errors on unused parameters. */, + }, + "exclude": ["node_modules", ".vscode-test"], + "include": ["**/*.ts"] } diff --git a/tslint.json b/tslint.json index 0e9880b..221b418 100644 --- a/tslint.json +++ b/tslint.json @@ -1,121 +1,100 @@ { - "rules": { - "adjacent-overload-signatures": true, - "align": true, - "array-type": [true, "array"], - "arrow-return-shorthand": true, - "ban-comma-operator": true, - "binary-expression-operand-order": true, - "callable-types": true, - "class-name": true, - "comment-format": true, - "curly": true, - "encoding": true, - "eofline": true, - "ext-variable-name": [ - true, - [ - "class", - "pascal" - ], - [ - "function", - "camel" - ] - ], - "file-header": [ - true, - ".*" - ], - "import-spacing": true, - "indent": [ - true, - "spaces", - 4 - ], - "label-position": true, - "match-default-export-name": true, - "member-ordering": true, - "new-parens": true, - "no-arg": true, - "no-bitwise": true, - "no-boolean-literal-compare": true, - "no-conditional-assignment": true, - "no-consecutive-blank-lines": true, - "no-construct": true, - "no-debugger": true, - "no-default-export": true, - "no-duplicate-imports": true, - "no-duplicate-super": true, - "no-duplicate-switch-case": true, - "no-duplicate-variable": true, - "no-unused-expression-chai": true, - "no-eval": true, - "no-import-side-effect": true, - "no-internal-module": true, - "no-invalid-this": true, - "no-irregular-whitespace": true, - "no-mergeable-namespace": true, - "no-misused-new": true, - "no-namespace": true, - "no-non-null-assertion": true, - "no-redundant-jsdoc": true, - "no-reference": true, - "no-reference-import": true, - "no-return-await": true, - "no-sparse-arrays": true, - "no-switch-case-fall-through": true, - "no-this-assignment": true, - "no-trailing-whitespace": true, - "no-unnecessary-callback-wrapper": true, - "no-unnecessary-initializer": true, - "no-unnecessary-qualifier": true, - "no-unsafe-finally": true, - "no-unused-expression": true, - "no-unused-variable": true, - "no-var-keyword": true, - "no-var-requires": true, - "number-literal-format": true, - "one-line": [ - true, - "check-catch", - "check-finally", - "check-else", - "check-open-brace", - "check-whitespace" - ], - "one-variable-per-declaration": true, - "no-floating-promises": [true, "PromisedAssertion"], - "prefer-method-signature": true, - "prefer-object-spread": true, - "prefer-while": true, - "promise-must-complete": true, - "semicolon": true, - "space-within-parens": true, - "trailing-comma": true, - "triple-equals": true, - "type-literal-delimiter": true, - "typedef": [ - true, - "variable-declaration", - "call-signature" - ], - "typedef-whitespace": true, - "unified-signatures": true, - "use-default-type-parameter": true, - "use-isnan": true, - "whitespace": [ - true, - "check-branch", - "check-operator", - "check-separator", - "check-preblock", - "check-type" - ] - }, - "rulesDirectory": [ - "node_modules/tslint-microsoft-contrib", - "node_modules/tslint-no-unused-expression-chai/rules", - "node_modules/vrsource-tslint-rules/rules" + "rules": { + "adjacent-overload-signatures": true, + "align": true, + "array-type": [true, "array"], + "arrow-return-shorthand": true, + "ban-comma-operator": true, + "binary-expression-operand-order": true, + "callable-types": true, + "class-name": true, + "comment-format": true, + "curly": true, + "encoding": true, + "eofline": true, + "ext-variable-name": [true, ["class", "pascal"], ["function", "camel"]], + "file-header": [true, ".*"], + "import-spacing": true, + "indent": [true, "spaces", 4], + "label-position": true, + "match-default-export-name": true, + "member-ordering": true, + "new-parens": true, + "no-arg": true, + "no-bitwise": true, + "no-boolean-literal-compare": true, + "no-conditional-assignment": true, + "no-consecutive-blank-lines": true, + "no-construct": true, + "no-debugger": true, + "no-default-export": true, + "no-duplicate-imports": true, + "no-duplicate-super": true, + "no-duplicate-switch-case": true, + "no-duplicate-variable": true, + "no-unused-expression-chai": true, + "no-eval": true, + "no-import-side-effect": true, + "no-internal-module": true, + "no-invalid-this": true, + "no-irregular-whitespace": true, + "no-mergeable-namespace": true, + "no-misused-new": true, + "no-namespace": true, + "no-non-null-assertion": true, + "no-redundant-jsdoc": true, + "no-reference": true, + "no-reference-import": true, + "no-return-await": true, + "no-sparse-arrays": true, + "no-switch-case-fall-through": true, + "no-this-assignment": true, + "no-trailing-whitespace": true, + "no-unnecessary-callback-wrapper": true, + "no-unnecessary-initializer": true, + "no-unnecessary-qualifier": true, + "no-unsafe-finally": true, + "no-unused-expression": true, + "no-unused-variable": true, + "no-var-keyword": true, + "no-var-requires": true, + "number-literal-format": true, + "one-line": [ + true, + "check-catch", + "check-finally", + "check-else", + "check-open-brace", + "check-whitespace" + ], + "one-variable-per-declaration": true, + "no-floating-promises": [true, "PromisedAssertion"], + "prefer-method-signature": true, + "prefer-object-spread": true, + "prefer-while": true, + "promise-must-complete": true, + "semicolon": true, + "space-within-parens": true, + "trailing-comma": true, + "triple-equals": true, + "type-literal-delimiter": true, + "typedef": [true, "variable-declaration", "call-signature"], + "typedef-whitespace": true, + "unified-signatures": true, + "use-default-type-parameter": true, + "use-isnan": true, + "whitespace": [ + true, + "check-branch", + "check-operator", + "check-separator", + "check-preblock", + "check-type" ] -} \ No newline at end of file + }, + "rulesDirectory": [ + "node_modules/tslint-microsoft-contrib", + "node_modules/tslint-no-unused-expression-chai/rules", + "node_modules/vrsource-tslint-rules/rules" + ] +} diff --git a/webpack.config.js b/webpack.config.js index 957e6cc..c5b8390 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -5,70 +5,77 @@ //@ts-check -'use strict'; +"use strict"; -const path = require('path'); +const path = require("path"); /**@type {import('webpack').Configuration}*/ const config = { - target: 'node', // vscode extensions run in a Node.js-context 📖 -> https://webpack.js.org/configuration/node/ + target: "node", // vscode extensions run in a Node.js-context 📖 -> https://webpack.js.org/configuration/node/ - entry: './src/extension.ts', // the entry point of this extension, 📖 -> https://webpack.js.org/configuration/entry-context/ - output: { // the bundle is stored in the 'dist' folder (check package.json), 📖 -> https://webpack.js.org/configuration/output/ - path: path.resolve(__dirname, 'dist'), - filename: 'main.js', - libraryTarget: "commonjs2", - devtoolModuleFilenameTemplate: "../[resource-path]", - }, - node: { - __dirname: false, - }, - devtool: 'source-map', - externals: { - vscode: "commonjs vscode" // the vscode-module is created on-the-fly and must be excluded. Add other modules that cannot be webpack'ed, 📖 -> https://webpack.js.org/configuration/externals/ - }, - resolve: { // support reading TypeScript and JavaScript files, 📖 -> https://github.com/TypeStrong/ts-loader - extensions: ['.ts', '.js'], - mainFields: ['main', 'module'] - }, - module: { - rules: [{ - test: /\.ts$/, - exclude: /node_modules/, - use: [{ - // configure TypeScript loader: - // * enable sources maps for end-to-end source maps - loader: 'ts-loader', - options: { - compilerOptions: { - "sourceMap": true, - } - } - }] - },{ - test: /.node$/, - loader: 'node-loader', - }] - }, - optimization: { - minimize: false - }, - stats: { - warnings: false - } -} + entry: "./src/extension.ts", // the entry point of this extension, 📖 -> https://webpack.js.org/configuration/entry-context/ + output: { + // the bundle is stored in the 'dist' folder (check package.json), 📖 -> https://webpack.js.org/configuration/output/ + path: path.resolve(__dirname, "dist"), + filename: "main.js", + libraryTarget: "commonjs2", + devtoolModuleFilenameTemplate: "../[resource-path]", + }, + node: { + __dirname: false, + }, + devtool: "source-map", + externals: { + vscode: "commonjs vscode", // the vscode-module is created on-the-fly and must be excluded. Add other modules that cannot be webpack'ed, 📖 -> https://webpack.js.org/configuration/externals/ + }, + resolve: { + // support reading TypeScript and JavaScript files, 📖 -> https://github.com/TypeStrong/ts-loader + extensions: [".ts", ".js"], + mainFields: ["main", "module"], + }, + module: { + rules: [ + { + test: /\.ts$/, + exclude: /node_modules/, + use: [ + { + // configure TypeScript loader: + // * enable sources maps for end-to-end source maps + loader: "ts-loader", + options: { + compilerOptions: { + sourceMap: true, + }, + }, + }, + ], + }, + { + test: /.node$/, + loader: "node-loader", + }, + ], + }, + optimization: { + minimize: false, + }, + stats: { + warnings: false, + }, +}; module.exports = (env) => { - if (env.BUILD_VSCODE_NLS) { - // rewrite nls call when being asked for - // @ts-ignore - config.module.rules.unshift({ - loader: 'vscode-nls-dev/lib/webpack-loader', - options: { - base: __dirname - } - }) - } + if (env.BUILD_VSCODE_NLS) { + // rewrite nls call when being asked for + // @ts-ignore + config.module.rules.unshift({ + loader: "vscode-nls-dev/lib/webpack-loader", + options: { + base: __dirname, + }, + }); + } - return config; + return config; };