diff --git a/README.md b/README.md index d9b221e4..c601ed63 100644 --- a/README.md +++ b/README.md @@ -1,19 +1,18 @@ # Rust support for Visual Studio Code [![](https://vsmarketplacebadge.apphb.com/version/rust-lang.rust.svg)](https://marketplace.visualstudio.com/items?itemName=rust-lang.rust) -[![Build Status](https://travis-ci.com/rust-lang/rls-vscode.svg?branch=master)](https://travis-ci.com/rust-lang/rls-vscode) +[![Build Status](https://travis-ci.com/AndriesK/rls-vscode.svg?branch=master)](https://travis-ci.com/AndriesK/rls-vscode) Adds language support for Rust to Visual Studio Code. Supports: -* code completion -* jump to definition, peek definition, find all references, symbol search -* types and documentation on hover -* code formatting -* refactoring (rename, deglob) -* error squiggles and apply suggestions from errors -* snippets -* build tasks - +- code completion +- jump to definition, peek definition, find all references, symbol search +- types and documentation on hover +- code formatting +- refactoring (rename, deglob) +- error squiggles and apply suggestions from errors +- snippets +- build tasks Rust support is powered by the [Rust Language Server](https://github.com/rust-lang/rls) (RLS). If you don't have it installed, the extension will install it for you. @@ -33,20 +32,18 @@ advice. Contributing code, tests, documentation, and bug reports is appreciated! For more details on building and debugging, etc., see [contributing.md](contributing.md). - ## Quick start -* Install [rustup](https://www.rustup.rs/) (Rust toolchain manager). -* Install this extension from [the VSCode Marketplace](https://marketplace.visualstudio.com/items?itemName=rust-lang.rust) +- Install [rustup](https://www.rustup.rs/) (Rust toolchain manager). +- Install this extension from [the VSCode Marketplace](https://marketplace.visualstudio.com/items?itemName=rust-lang.rust) (or by entering `ext install rust-lang.rust` at the command palette Ctrl+P). -* (Skip this step if you already have Rust projects that you'd like to work on.) +- (Skip this step if you already have Rust projects that you'd like to work on.) Create a new Rust project by following [these instructions](https://doc.rust-lang.org/book/ch01-03-hello-cargo.html). -* Open a Rust project (`File > Add Folder to Workspace...`). Open the folder for the whole +- Open a Rust project (`File > Add Folder to Workspace...`). Open the folder for the whole project (i.e., the folder containing 'Cargo.toml'), not the 'src' folder. -* You'll be prompted to install the RLS. Once installed, the RLS should start +- You'll be prompted to install the RLS. Once installed, the RLS should start building your project. - ## Configuration This extension provides options in VSCode's configuration settings. These @@ -57,11 +54,11 @@ have Intellisense help. Some highlights: -* `rust.show_warnings` - set to false to silence warnings in the editor. -* `rust.all_targets` - build and index code for all targets (i.e., integration tests, examples, and benches) -* `rust.cfg_test` - build and index test code (i.e., code with `#[cfg(test)]`/`#[test]`) +- `rust.show_warnings` - set to false to silence warnings in the editor. +- `rust.all_targets` - build and index code for all targets (i.e., integration tests, examples, and benches) +- `rust.cfg_test` - build and index test code (i.e., code with `#[cfg(test)]`/`#[test]`) -* `rust-client.channel` - specifies from which toolchain the RLS should be spawned +- `rust-client.channel` - specifies from which toolchain the RLS should be spawned ## Features @@ -72,16 +69,16 @@ includes snippet names as options when you type; select one by pressing 'enter'. You can move to the next 'hole' in the template by pressing 'tab'. We provide the following snippets: -* `for` - a for loop -* `unimplemented` -* `unreachable` -* `print(ln)` -* `assert(_eq)` -* `macro_rules` - declare a macro -* `if let Option` - an `if let` statement for executing code only in the `Some` +- `for` - a for loop +- `unimplemented` +- `unreachable` +- `print(ln)` +- `assert(_eq)` +- `macro_rules` - declare a macro +- `if let Option` - an `if let` statement for executing code only in the `Some` case. -* `spawn` - spawn a thread -* `extern crate` - insert an `extern crate` statement +- `spawn` - spawn a thread +- `extern crate` - insert an `extern crate` statement This extension is deliberately conservative about snippets and doesn't include too many. If you want more, check out @@ -97,22 +94,19 @@ The plugin writes these into `tasks.json`. The plugin will not overwrite existing tasks, so you can customise these tasks. To refresh back to the defaults, delete `tasks.json` and restart VSCode. - ## Format on save To enable formatting on save, you need to set the `editor.formatOnSave` setting to `true`. Find it under `File > Preferences > Settings`. - ## Requirements -* [Rustup](https://www.rustup.rs/), -* A Rust toolchain (the extension will configure this for you, with +- [Rustup](https://www.rustup.rs/), +- A Rust toolchain (the extension will configure this for you, with permission), -* `rls`, `rust-src`, and `rust-analysis` components (the +- `rls`, `rust-src`, and `rust-analysis` components (the extension will install these for you, with permission). - ## Implementation This extension almost exclusively uses the RLS for its feature support (syntax @@ -121,4 +115,3 @@ the Rust compiler (`rustc`) to get data about Rust programs. It uses Cargo to manage building. Both Cargo and `rustc` are run in-process by the RLS. Formatting and code completion are provided by `rustfmt` and Racer, again both of these are run in-process by the RLS. - diff --git a/src/configuration.ts b/src/configuration.ts index e9afc840..27c1562c 100644 --- a/src/configuration.ts +++ b/src/configuration.ts @@ -79,6 +79,10 @@ export class RLSConfiguration { return this.configuration.get('rust-client.rustupPath', 'rustup'); } + public get rustupPathQuotes(): string { + return '"' + this.rustupPath + '"'; + } + public get useWSL(): boolean { return this.configuration.get('rust-client.useWSL', false); } @@ -118,6 +122,10 @@ export class RLSConfiguration { return this.configuration.get('rust-client.rlsPath'); } + public get rlsPathQuotes(): string | undefined { + return this.rlsPath ? '"' + this.rlsPath + '"' : undefined; + } + public get multiProjectEnabled(): boolean { return this.configuration.get( 'rust-client.enableMultiProjectSetup', @@ -130,6 +138,7 @@ export class RLSConfiguration { return { channel: ignoreChannel ? '' : this.channel, path: this.rustupPath, + pathQuotes: this.rustupPathQuotes, useWSL: this.useWSL, }; } diff --git a/src/extension.ts b/src/extension.ts index d66a2912..b24e3225 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -362,8 +362,9 @@ class ClientWorkspace { const rustcPrintSysroot = () => this.config.rustupDisabled ? wslWrapper.exec('rustc --print sysroot', { env }) - : wslWrapper.exec( - `${this.config.rustupPath} run ${this.config.channel} rustc --print sysroot`, + : wslWrapper.execFile( + this.config.rustupPath, + ['run', this.config.channel, 'rustc', '--print', 'sysroot'], { env }, ); @@ -416,7 +417,7 @@ class ClientWorkspace { private async makeRlsProcess(): Promise { // Run "rls" from the PATH unless there's an override. - const rlsPath = this.config.rlsPath || 'rls'; + const rlsPath = this.config.rlsPathQuotes || 'rls'; // We don't need to set [DY]LD_LIBRARY_PATH if we're using rustup, // as rustup will set it for us when it chooses a toolchain. @@ -451,8 +452,9 @@ class ClientWorkspace { } const env = await makeRlsEnv(); + childProcess = withWsl(config.useWSL).spawn( - config.path, + config.pathQuotes, ['run', config.channel, rlsPath], { env, cwd, shell: true }, ); diff --git a/src/rustup.ts b/src/rustup.ts index 7739de78..87beb015 100644 --- a/src/rustup.ts +++ b/src/rustup.ts @@ -17,6 +17,7 @@ function isInstalledRegex(componentName: string): RegExp { export interface RustupConfig { channel: string; path: string; + pathQuotes: string; useWSL: boolean; } @@ -24,9 +25,9 @@ export async function rustupUpdate(config: RustupConfig) { startSpinner('RLS', 'Updating…'); try { - const { stdout } = await withWsl(config.useWSL).exec( - `${config.path} update`, - ); + const { stdout } = await withWsl(config.useWSL).execFile(config.path, [ + 'update', + ]); // This test is imperfect because if the user has multiple toolchains installed, they // might have one updated and one unchanged. But I don't want to go too far down the @@ -65,13 +66,38 @@ export async function ensureToolchain(config: RustupConfig) { * Checks for required RLS components and prompts the user to install if it's * not already. */ + export async function checkForRls(config: RustupConfig) { if (await hasRlsComponents(config)) { return; } + // Format an easier to understand component install prompt + const componentsNeededUserOutput = REQUIRED_COMPONENTS.map((component, i) => { + if ( + REQUIRED_COMPONENTS.length > 1 && + i === REQUIRED_COMPONENTS.length - 1 + ) { + return ' and `' + component + '`'; + } else if ( + i !== 0 && + i !== REQUIRED_COMPONENTS.length - 1 && + REQUIRED_COMPONENTS.length > 2 + ) { + return ', `' + component + '`'; + } else { + return '`' + component + '`'; + } + }); const clicked = await Promise.resolve( - window.showInformationMessage('RLS not installed. Install?', 'Yes'), + window.showInformationMessage( + ` + Rustup + ${ + componentsNeededUserOutput.length > 1 ? 'components' : 'component' + } ${componentsNeededUserOutput.join('')} not installed. Install?`, + 'Yes', + ), ); if (clicked) { await installRlsComponents(config); @@ -83,9 +109,10 @@ export async function checkForRls(config: RustupConfig) { async function hasToolchain(config: RustupConfig): Promise { try { - const { stdout } = await withWsl(config.useWSL).exec( - `${config.path} toolchain list`, - ); + const { stdout } = await withWsl(config.useWSL).execFile(config.path, [ + 'toolchain', + 'list', + ]); return stdout.includes(config.channel); } catch (e) { console.log(e); @@ -129,7 +156,7 @@ async function tryToInstallToolchain(config: RustupConfig) { */ async function listComponents(config: RustupConfig): Promise { return withWsl(config.useWSL) - .exec(`${config.path} component list --toolchain ${config.channel}`) + .execFile(config.path, ['component', 'list', '--toolchain', config.channel]) .then(({ stdout }) => stdout .toString() @@ -142,9 +169,20 @@ async function hasRlsComponents(config: RustupConfig): Promise { try { const components = await listComponents(config); - return REQUIRED_COMPONENTS.map(isInstalledRegex).every(isInstalledRegex => - components.some(c => isInstalledRegex.test(c)), - ); + // Splice the components that are installed from the REQUIRED_COMPONENTS array + for (let i = REQUIRED_COMPONENTS.length; i >= 0; --i) { + const installedRegex = isInstalledRegex(REQUIRED_COMPONENTS[i]); + components.forEach(component => { + if (installedRegex.test(component)) { + REQUIRED_COMPONENTS.splice(i, 1); + } + }); + } + if (REQUIRED_COMPONENTS.length === 0) { + return true; + } else { + return false; + } } catch (e) { console.log(e); window.showErrorMessage(`Can't detect RLS components: ${e.message}`); @@ -153,7 +191,7 @@ async function hasRlsComponents(config: RustupConfig): Promise { } } -async function installRlsComponents(config: RustupConfig) { +export async function installRlsComponents(config: RustupConfig) { startSpinner('RLS', 'Installing components…'); for (const component of REQUIRED_COMPONENTS) { @@ -226,7 +264,9 @@ export function parseActiveToolchain(rustupOutput: string): string { export async function getVersion(config: RustupConfig): Promise { const versionRegex = /rustup ([0-9]+\.[0-9]+\.[0-9]+)/; - const output = await withWsl(config.useWSL).exec(`${config.path} --version`); + const output = await withWsl(config.useWSL).execFile(config.path, [ + '--version', + ]); const versionMatch = output.stdout.toString().match(versionRegex); if (versionMatch && versionMatch.length >= 2) { return versionMatch[1]; @@ -257,7 +297,7 @@ export function getActiveChannel(wsPath: string, config: RustupConfig): string { try { // `rustup show active-toolchain` is available since rustup 1.12.0 activeChannel = withWsl(config.useWSL) - .execSync(`${config.path} show active-toolchain`, { cwd: wsPath }) + .execSync(`${config.pathQuotes} show active-toolchain`, { cwd: wsPath }) .toString() .trim(); // Since rustup 1.17.0 if the active toolchain is the default, we're told @@ -268,7 +308,7 @@ export function getActiveChannel(wsPath: string, config: RustupConfig): string { } catch (e) { // Possibly an old rustup version, so try rustup show const showOutput = withWsl(config.useWSL) - .execSync(`${config.path} show`, { cwd: wsPath }) + .execSync(`${config.pathQuotes} show`, { cwd: wsPath }) .toString(); activeChannel = parseActiveToolchain(showOutput); } diff --git a/src/tasks.ts b/src/tasks.ts index 375f2ac2..b76017ef 100644 --- a/src/tasks.ts +++ b/src/tasks.ts @@ -1,6 +1,7 @@ import * as crypto from 'crypto'; import { Disposable, + ProcessExecution, ShellExecution, Task, TaskGroup, @@ -127,6 +128,9 @@ export async function runTaskCommand( displayName: string, folder?: WorkspaceFolder, ) { + if (!command) { + return; + } const uniqueId = crypto.randomBytes(20).toString(); const task = new Task( @@ -134,7 +138,7 @@ export async function runTaskCommand( folder ? folder : workspace.workspaceFolders![0], displayName, TASK_SOURCE, - new ShellExecution(`${command} ${args.join(' ')}`, { + new ProcessExecution(command, args, { cwd: cwd || (folder && folder.uri.fsPath), env, }), diff --git a/src/utils/child_process.ts b/src/utils/child_process.ts index 72d04376..489870f5 100644 --- a/src/utils/child_process.ts +++ b/src/utils/child_process.ts @@ -4,9 +4,11 @@ import * as util from 'util'; import { modifyParametersForWSL } from './wslpath'; const execAsync = util.promisify(child_process.exec); +const execFile = util.promisify(child_process.execFile); export interface SpawnFunctions { exec: typeof execAsync; + execFile: typeof execFile; execSync: typeof child_process.execSync; spawn: typeof child_process.spawn; modifyArgs: ( @@ -19,12 +21,14 @@ export function withWsl(withWsl: boolean): SpawnFunctions { return withWsl ? { exec: withWslModifiedParameters(execAsync), + execFile: withWslModifiedParameters(execFile), execSync: withWslModifiedParameters(child_process.execSync), spawn: withWslModifiedParameters(child_process.spawn), modifyArgs: modifyParametersForWSL, } : { exec: execAsync, + execFile, execSync: child_process.execSync, spawn: child_process.spawn, modifyArgs: (command: string, args: string[]) => ({ command, args }), diff --git a/test/suite/rustup.test.ts b/test/suite/rustup.test.ts index 1c672c00..a3017796 100644 --- a/test/suite/rustup.test.ts +++ b/test/suite/rustup.test.ts @@ -9,6 +9,7 @@ assert(rustupVersion); const config: rustup.RustupConfig = { path: 'rustup', + pathQuotes: 'rustup', channel: 'stable', // TODO: Detect if we're running in Windows and if wsl works? useWSL: false, @@ -22,4 +23,8 @@ suite('Rustup Tests', () => { test('getActiveChannel', async () => { rustup.getActiveChannel('.', config); }); + test('installRlsComponents', async function() { + this.timeout(30000); + await rustup.installRlsComponents(config); + }); });