diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..8e5962ee --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +out +node_modules \ No newline at end of file diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 00000000..c77b2adf --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,28 @@ +// 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=${workspaceRoot}" ], + "stopOnEntry": false, + "sourceMaps": true, + "outDir": "${workspaceRoot}/out/src", + "preLaunchTask": "npm" + }, + { + "name": "Launch Tests", + "type": "extensionHost", + "request": "launch", + "runtimeExecutable": "${execPath}", + "args": ["--extensionDevelopmentPath=${workspaceRoot}", "--extensionTestsPath=${workspaceRoot}/out/test" ], + "stopOnEntry": false, + "sourceMaps": true, + "outDir": "${workspaceRoot}/out/test", + "preLaunchTask": "npm" + } + ] +} diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 00000000..b0d48d29 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,11 @@ +// 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", // we want to use the TS server from our node_modules folder to control its version + "files.trimTrailingWhitespace": true +} \ No newline at end of file diff --git a/.vscode/tasks.json b/.vscode/tasks.json new file mode 100644 index 00000000..fb7f662e --- /dev/null +++ b/.vscode/tasks.json @@ -0,0 +1,30 @@ +// Available variables which can be used inside of strings. +// ${workspaceRoot}: the root folder of the team +// ${file}: the current opened file +// ${fileBasename}: the current opened file's basename +// ${fileDirname}: the current opened file's dirname +// ${fileExtname}: the current opened file's extension +// ${cwd}: the current working directory of the spawned process + +// A task runner that calls a custom npm script that compiles the extension. +{ + "version": "0.1.0", + + // we want to run npm + "command": "npm", + + // the command is a shell script + "isShellCommand": true, + + // show the output window only if unrecognized errors occur. + "showOutput": "silent", + + // we run the custom script "compile" as defined in package.json + "args": ["run", "compile", "--loglevel", "silent"], + + // The tsc compiler is started in watching mode + "isWatching": true, + + // use the standard tsc in watch mode problem matcher to find compile problems in the output. + "problemMatcher": "$tsc-watch" +} \ No newline at end of file diff --git a/.vscodeignore b/.vscodeignore new file mode 100644 index 00000000..93e28ff2 --- /dev/null +++ b/.vscodeignore @@ -0,0 +1,9 @@ +.vscode/** +typings/** +out/test/** +test/** +src/** +**/*.map +.gitignore +tsconfig.json +vsc-extension-quickstart.md diff --git a/README.md b/README.md new file mode 100644 index 00000000..4decea1d --- /dev/null +++ b/README.md @@ -0,0 +1,10 @@ +# VSCode CMake Tools + +This is a simple Visual Studio Code extension that offers CMake integration. This extension +itself *does not* provide language support. For that I recommend +[this extension](https://marketplace.visualstudio.com/items?itemName=twxs.cmake). + +This extension can be installed with ``ext install cmake-tools``. + +Issues? Questions? Feature requests? Create an issue on +[the github page](https://github.com/vector-of-bool/vscode-cmake-tools). \ No newline at end of file diff --git a/res/icon.svg b/res/icon.svg new file mode 100644 index 00000000..4d2d9bca --- /dev/null +++ b/res/icon.svg @@ -0,0 +1,264 @@ + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + diff --git a/src/extension.ts b/src/extension.ts new file mode 100644 index 00000000..a29d45e2 --- /dev/null +++ b/src/extension.ts @@ -0,0 +1,270 @@ +'use strict'; + +import * as vscode from 'vscode'; +import * as proc from 'child_process'; +import * as fs from 'fs'; +import * as path from 'path'; + + +export function activate(context: vscode.ExtensionContext) { + // Create an output channel where the configure and build output will go + const channel = vscode.window.createOutputChannel('CMake/Build'); + + // The diagnostics collection we talk to + let cmake_diagnostics = vscode.languages.createDiagnosticCollection('cmake-diags'); + + // Get the configuration + function cmakeConfig(): vscode.WorkspaceConfiguration { + return vscode.workspace.getConfiguration('cmake'); + } + // Get the value of a CMake configuration setting + function config(path: string, defaultValue?: T): T { + return cmakeConfig().get(path, defaultValue); + } + + // Wrap a Node-style function that takes a callback in a promise, making it awaitable + function doAsync(fn: Function, ...args): Promise { + return new Promise((resolve, reject) => { + fn(...args, resolve); + }); + } + + // Returns the build directory + function buildDirectory(): string { + const build_dir = config('buildDirectory'); + return build_dir.replace('${workspaceRoot}', vscode.workspace.rootPath); + } + + // Test that a command exists + const testHaveCommand = async function (command, args: string[] = ['--version']): Promise { + return await new Promise((resolve, _) => { + const pipe = proc.spawn(command, args); + pipe.on('error', () => resolve(false)); + pipe.on('exit', () => resolve(true)); + }); + } + + // Given a list of CMake generators, returns the first on available + const pickGenerator = async function (candidates: string[]): Promise { + for (const gen of candidates) { + const delegate = { + Ninja: async function () { return await testHaveCommand('ninja'); }, + "MinGW Makefiles": async function () { + return process.platform === 'win32' && await testHaveCommand('make'); + }, + "NMake Makefiles": async function () { + return process.platform === 'win32' && await testHaveCommand('nmake', ['/?']); + }, + 'Unix Makefiles': async function () { + return process.platform !== 'win32' && await testHaveCommand('make'); + } + }[gen]; + if (delegate === undefined) { + vscode.window.showErrorMessage('Unknown CMake generator "' + gen + '"'); + continue; + } + if (await delegate()) + return gen; + else + console.log('Genereator "' + gen + '" is not supported'); + } + return null; + } + + function executeCMake(args: string[]): void { + console.info('Execute cmake with arguments:', args); + const pipe = proc.spawn('cmake', args); + const status = vscode.window.setStatusBarMessage; + status('Executing CMake...', 1000); + channel.appendLine('[vscode] Executing cmake command: cmake ' + args.join(' ')); + let stderr_acc = ''; + pipe.stdout.on('data', (data: Uint8Array) => { + const str = data.toString(); + console.log('cmake [stdout]: ' + str.trim()); + channel.append(str); + status('cmake: ' + str.trim(), 1000); + }); + pipe.stderr.on('data', (data: Uint8Array) => { + const str = data.toString(); + console.log('cmake [stderr]: ' + str.trim()); + stderr_acc += str; + channel.append(str); + status('cmake: ' + str.trim(), 1000); + }); + pipe.on('close', (retc: Number) => { + console.log('cmake exited with return code ' + retc); + channel.appendLine('[vscode] CMake exited with status ' + retc); + status('CMake exited with status ' + retc, 3000); + if (retc !== 0) { + vscode.window.showErrorMessage('CMake exited with non-zero return code ' + retc + '. See CMake/Build output for details'); + } + + let rest = stderr_acc; + const diag_re = /CMake (.*?) at (.*?):(\d+) .*?:\s+(.*?)\s*\n\n\n((.|\n)*)/; + const diags: Object = {}; + while (true) { + if (!rest.length) break; + const found = diag_re.exec(rest); + if (!found) break; + const [level, filename, linestr, what, tail] = found.slice(1); + const filepath = + path.isAbsolute(filename) + ? filename + : path.join(vscode.workspace.rootPath, filename); + + const line = Number.parseInt(linestr) - 1; + if (!(filepath in diags)) { + diags[filepath] = []; + } + const file_diags: vscode.Diagnostic[] = diags[filepath]; + const diag = new vscode.Diagnostic( + new vscode.Range( + line, + 0, + line, + Number.POSITIVE_INFINITY + ), + what, + { + "Warning": vscode.DiagnosticSeverity.Warning, + "Error": vscode.DiagnosticSeverity.Error, + }[level] + ); + diag.source = 'CMake'; + file_diags.push(diag); + rest = tail; + } + + cmake_diagnostics.clear(); + for (const filepath in diags) { + cmake_diagnostics.set(vscode.Uri.file(filepath), diags[filepath]); + } + }); + } + + const configure = vscode.commands.registerCommand('cmake.configure', async function (extra_args: string[] = []) { + if (!(extra_args instanceof Array)) { + extra_args = []; + } + const source_dir = vscode.workspace.rootPath; + if (!source_dir) { + vscode.window.showErrorMessage('You do not have a source directory open'); + return; + } + const cmake_list = path.join(source_dir, 'CMakeLists.txt'); + const binary_dir = buildDirectory(); + + const cmake_cache = path.join(binary_dir, "CMakeCache.txt"); + channel.show(); + + const settings_args = ['--no-warn-unused-cli']; + if (!(await doAsync(fs.exists, cmake_cache))) { + channel.appendLine("[vscode] Setting up initial CMake configuration"); + const generator = await pickGenerator(config("preferredGenerators")); + if (generator) { + channel.appendLine('[vscode] Configuring using the "' + generator + '" CMake generator'); + settings_args.push("-G" + generator); + } + else { + console.error("None of the preferred generators was selected"); + } + + settings_args.push("-DCMAKE_BUILD_TYPE=" + config("initialBuildType")); + } + + const settings = config("configureSettings"); + for (const key in settings) { + let value = settings[key]; + if (value === true || value === false) + value = value ? "TRUE" : "FALSE"; + if (value instanceof Array) + value = value.join(';'); + settings_args.push("-D" + key + "=" + value); + } + + await executeCMake( + ['-H' + source_dir, '-B' + binary_dir] + .concat(settings_args) + .concat(extra_args) + ); + }); + + const build = vscode.commands.registerCommand('cmake.build', async function (target = 'all') { + if (target instanceof Object) { + target = 'all'; + } + const source_dir = vscode.workspace.rootPath; + if (!source_dir) { + vscode.window.showErrorMessage('You do not have a source directory open'); + return; + } + const binary_dir = buildDirectory(); + if (!(await doAsync(fs.exists, binary_dir))) { + vscode.window.showErrorMessage('You do not yet have a build directory. Configure your project first'); + return; + } + + channel.show(); + await executeCMake(['--build', binary_dir, '--target', target]); + }); + + const build_target = vscode.commands.registerCommand('cmake.buildWithTarget', async function () { + const target: string = await vscode.window.showInputBox({ + prompt: 'Enter the name of a target to build', + }); + vscode.commands.executeCommand('cmake.build', target); + }); + + const set_build_type = vscode.commands.registerCommand('cmake.setBuildType', async function () { + const build_type = await vscode.window.showQuickPick([{ + label: 'Release', + description: 'Optimized build with no debugging information', + }, { + label: 'Debug', + description: 'Default build type. No optimizations. Contains debug information', + }, { + label: 'MinSizeRel', + description: 'Release build tweaked for minimum binary code size', + }, { + label: 'RelWithDebInfo', + description: 'Same as "Release", but also generates debugging information', + }]); + + vscode.commands.executeCommand('cmake.configure', ['-DCMAKE_BUILD_TYPE=' + build_type.label]) + }) + + const clean_configure = vscode.commands.registerCommand('cmake.cleanConfigure', async function () { + const build_dir = buildDirectory(); + const cmake_cache = path.join(build_dir, 'CMakeCache.txt'); + const cmake_files = path.join(build_dir, 'CMakeFiles'); + if (await doAsync(fs.exists, cmake_cache)) { + channel.appendLine('[vscode] Removing ' + cmake_cache); + await doAsync(fs.unlink, cmake_cache); + } + if (await doAsync(fs.exists, cmake_files)) { + channel.appendLine('[vscode] Removing ' + cmake_files); + await doAsync(fs.unlink, cmake_files); + } + await vscode.commands.executeCommand('cmake.configure'); + }); + + const clean = vscode.commands.registerCommand("cmake.clean", async function () { + await vscode.commands.executeCommand('cmake.build', 'clean'); + }); + + const clean_rebuild = vscode.commands.registerCommand('cmake.cleanRebuild', async function () { + await vscode.commands.executeCommand('cmake.clean'); + await vscode.commands.executeCommand('cmake.build'); + }); + + const ctest = vscode.commands.registerCommand('cmake.ctest', async function () { + executeCMake(['-E', 'chdir', buildDirectory(), 'ctest', '-j8', '--output-on-failure']); + }); + + for (const item of [configure, build, build_target, set_build_type, clean_configure, clean, clean_rebuild]) + context.subscriptions.push(item); +} + +// this method is called when your extension is deactivated +export function deactivate() { +} \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 00000000..97c3ae37 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,13 @@ +{ + "compilerOptions": { + "module": "commonjs", + "target": "ES5", + "outDir": "out", + "noLib": true, + "sourceMap": true, + "rootDir": "." + }, + "exclude": [ + "node_modules" + ] +} \ No newline at end of file diff --git a/typings/node.d.ts b/typings/node.d.ts new file mode 100644 index 00000000..5ed7730b --- /dev/null +++ b/typings/node.d.ts @@ -0,0 +1 @@ +/// \ No newline at end of file diff --git a/typings/vscode-typings.d.ts b/typings/vscode-typings.d.ts new file mode 100644 index 00000000..5590dc8c --- /dev/null +++ b/typings/vscode-typings.d.ts @@ -0,0 +1 @@ +///