diff --git a/CHANGELOG.md b/CHANGELOG.md index 5fa562fd..f0be0e1a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,6 @@ ### Unreleased +* Support rust-analyzer as an alternate LSP server * Bump required VSCode version to 1.43, use language server protocol (LSP) v3.15 ### 0.7.5 - 2020-05-06 diff --git a/README.md b/README.md index e99d39c3..a17ebb3b 100644 --- a/README.md +++ b/README.md @@ -14,37 +14,41 @@ Adds language support for Rust to Visual Studio Code. Supports: * snippets * build tasks +Rust support is powered by a separate [language server](https://microsoft.github.io/language-server-protocol/overviews/lsp/overview/) +- either by the official [Rust Language Server](https://github.com/rust-lang/rls) (RLS) or +[rust-analyzer](https://github.com/rust-lang/rls), depending on the user's +preference. If you don't have it installed, the extension will install it for +you (with permission). -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. - -This extension is built and maintained by the RLS team, part of the Rust +This extension is built and maintained by the Rust [IDEs and editors team](https://www.rust-lang.org/en-US/team.html#Dev-tools-team). -It is the reference client implementation for the RLS. Our focus is on providing -a stable, high quality extension that makes best use of the RLS. We aim to -support as many features as possible, but our priority is supporting the -essential features as well as possible. +Our focus is on providing +a stable, high quality extension that makes the best use of the respective language +server. We aim to support as many features as possible, but our priority is +supporting the essential features as well as possible. + +For support, please file an +[issue on the repo](https://github.com/rust-lang/rls-vscode/issues/new) +or talk to us [on Discord](https://discordapp.com/invite/rust-lang). +For RLS, there is also some [troubleshooting and debugging](https://github.com/rust-lang/rls/blob/master/debugging.md) advice. -For support, please file an [issue on the repo](https://github.com/rust-lang/rls-vscode/issues/new) -or talk to us [on Discord](https://discordapp.com/invite/rust-lang). There is also some -[troubleshooting and debugging](https://github.com/rust-lang/rls/blob/master/debugging.md) -advice. +## Contribution Contributing code, tests, documentation, and bug reports is appreciated! For -more details on building and debugging, etc., see [contributing.md](contributing.md). +more details 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) +1. Install [rustup](https://www.rustup.rs/) (Rust toolchain manager). +2. 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.) +3. (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 - 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 - building your project. +4. 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). +5. You'll be prompted to install the Rust server. Once installed, it should start + analyzing your project (RLS will also have to to build the project). ## Configuration @@ -53,24 +57,25 @@ This extension provides options in VSCode's configuration settings. These include `rust.*`, which are passed directly to RLS, and the `rust-client.*` , which mostly deal with how to spawn it or debug it. You can find the settings under `File > Preferences > Settings`; they all -have Intellisense help. +have IntelliSense help. -Some highlights: +Examples: * `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 +> **_TIP:_** To select the underlying language server, set `rust-client.engine` accordingly! + ## Features ### Snippets -Snippets are code templates which expand into common boilerplate. Intellisense -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: +Snippets are code templates which expand into common boilerplate. IntelliSense +includes snippet names as options when you type; select one by pressing +enter. You can move to the next snippet 'hole' in the template by +pressing tab. We provide the following snippets: * `for` - a for loop * `macro_rules` - declare a macro @@ -102,18 +107,25 @@ 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 - permission), -* `rls`, `rust-src`, and `rust-analysis` components (the - extension will install these for you, with permission). +* A Rust toolchain (the extension will configure this for you, with permission), +* `rls`, `rust-src`, and `rust-analysis` components (the extension will install + these for you, with permission). Only `rust-src` is required when using + rust-analyzer. ## Implementation -This extension almost exclusively uses the RLS for its feature support (syntax -highlighting, snippets, and build tasks are provided client-side). The RLS uses -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. +Both language servers can use Cargo to get more information about Rust projects +and both use [`rustfmt`](https://github.com/rust-lang/rustfmt/) extensively to +format the code. + +[RLS](https://github.com/rust-lang/rls) uses Cargo and also the Rust compiler +([`rustc`](https://github.com/rust-lang/rust/)) in a more direct fashion, where +it builds the project and reuses the data computed by the compiler itself. To +provide code completion it uses a separate tool called +[`racer`](https://github.com/racer-rust/racer). +[Rust Analyzer](https://github.com/rust-analyzer/rust-analyzer) is a separate +compiler frontend for the Rust language that doesn't use the Rust compiler +([`rustc`](https://github.com/rust-lang/rust/)) directly but rather performs its +own analysis that's tailor-fitted to the editor/IDE use case. diff --git a/package-lock.json b/package-lock.json index ee80899e..aeeb1ff8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -65,6 +65,16 @@ "integrity": "sha512-75eLjX0pFuTcUXnnWmALMzzkYorjND0ezNEycaKesbUBg9eGZp4GHPuDmkRc4mQQvIpe29zrzATNRA6hkYqwmA==", "dev": true }, + "@types/node-fetch": { + "version": "2.5.7", + "resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.5.7.tgz", + "integrity": "sha512-o2WVNf5UhWRkxlf6eq+jMZDu7kjgpgJfl4xVNlvryc95O/6F2ld8ztKX+qu+Rjyet93WAWm5LjeX9H5FGkODvw==", + "dev": true, + "requires": { + "@types/node": "*", + "form-data": "^3.0.0" + } + }, "@types/vscode": { "version": "1.44.0", "resolved": "https://registry.npmjs.org/@types/vscode/-/vscode-1.44.0.tgz", @@ -116,6 +126,12 @@ "integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==", "dev": true }, + "asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=", + "dev": true + }, "azure-devops-node-api": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/azure-devops-node-api/-/azure-devops-node-api-7.2.0.tgz", @@ -284,6 +300,15 @@ "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", "dev": true }, + "combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dev": true, + "requires": { + "delayed-stream": "~1.0.0" + } + }, "commander": { "version": "2.20.0", "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.0.tgz", @@ -347,6 +372,12 @@ "object-keys": "^1.0.12" } }, + "delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=", + "dev": true + }, "denodeify": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/denodeify/-/denodeify-1.2.1.tgz", @@ -502,6 +533,17 @@ "is-buffer": "~2.0.3" } }, + "form-data": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-3.0.0.tgz", + "integrity": "sha512-CKMFDglpbMi6PyN+brwB9Q/GOw0eAnsrEZDgcsH5Krhz5Od/haKHAX0NmQfha2zPPz0JpWzA7GJHGSnvCRLWsg==", + "dev": true, + "requires": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + } + }, "fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", @@ -763,6 +805,21 @@ "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", "dev": true }, + "mime-db": { + "version": "1.44.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.44.0.tgz", + "integrity": "sha512-/NOTfLrsPBVeH7YtFPgsVWveuL+4SjjYxaQ1xtM1KMFj7HdxlBlxeyNLzhyJVx7r4rZGJAZ/6lkKCitSc/Nmpg==", + "dev": true + }, + "mime-types": { + "version": "2.1.27", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.27.tgz", + "integrity": "sha512-JIhqnCasI9yD+SsmkquHBxTSEuZdQX5BuQnS2Vc7puQQQ+8yiP5AY5uWhpdv4YL4VM5c6iliiYWPgJ/nJQLp7w==", + "dev": true, + "requires": { + "mime-db": "1.44.0" + } + }, "minimatch": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", @@ -873,6 +930,11 @@ "semver": "^5.7.0" } }, + "node-fetch": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.0.tgz", + "integrity": "sha512-8dG4H5ujfvFiqDmVu9fQ5bOHUC15JMjMY/Zumv26oOvvVJjM67KF8koCWIabKQ1GJIa9r2mMZscBq/TbdOcmNA==" + }, "nth-check": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-1.0.2.tgz", diff --git a/package.json b/package.json index a328642d..fe065cc8 100644 --- a/package.json +++ b/package.json @@ -50,6 +50,7 @@ "installDevExtension": "npm install && ./node_modules/.bin/vsce package -o ./out/rls-vscode-dev.vsix && code --install-extension ./out/rls-vscode-dev.vsix" }, "dependencies": { + "node-fetch": "^2.6.0", "vscode-languageclient": "^6.0.0" }, "devDependencies": { @@ -57,6 +58,7 @@ "@types/glob": "^7.1.1", "@types/mocha": "^5.2.6", "@types/node": "^12.8.1", + "@types/node-fetch": "^2.5.7", "@types/vscode": "^1.43.0", "chai": "^4.2.0", "glob": "^7.1.4", @@ -87,26 +89,26 @@ "commands": [ { "command": "rls.update", - "title": "Update the RLS", - "description": "Use Rustup to update Rust, the RLS, and required data", + "title": "Update the current Rust toolchain", + "description": "Use Rustup to the current Rust toolchain, along with its components", "category": "Rust" }, { "command": "rls.restart", - "title": "Restart the RLS", + "title": "Restart the Rust server", "description": "Sometimes, it's just best to try turning it off and on again", "category": "Rust" }, { "command": "rls.start", - "title": "Start the RLS", - "description": "Start the RLS (when rust-client.autoStartRls is false or when manually stopped)", + "title": "Start the Rust server", + "description": "Start the Rust server (when rust-client.autoStartRls is false or when manually stopped)", "category": "Rust" }, { "command": "rls.stop", - "title": "Stop the RLS", - "description": "Stop the RLS for a workspace until manually started again", + "title": "Stop the Rust server", + "description": "Stop the Rust server for a workspace until manually started again", "category": "Rust" } ], @@ -160,6 +162,20 @@ "type": "object", "title": "Rust configuration", "properties": { + "rust-client.engine": { + "type": "string", + "enum": [ + "rls", + "rust-analyzer" + ], + "enumDescriptions": [ + "Use the Rust Language Server (RLS)", + "Use the rust-analyzer language server (NOTE: not fully supported yet)" + ], + "default": "rls", + "description": "The underlying LSP server used to provide IDE support for Rust projects.", + "scope": "window" + }, "rust-client.logToFile": { "type": "boolean", "default": false, @@ -448,6 +464,22 @@ "default": true, "description": "Show additional context in hover tooltips when available. This is often the type local variable declaration.", "scope": "resource" + }, + "rust.rust-analyzer": { + "type": "object", + "default": {}, + "description": "Settings passed down to rust-analyzer server", + "scope": "resource" + }, + "rust.rust-analyzer.releaseTag": { + "type": "string", + "default": "nightly", + "description": "Which binary release to download and use" + }, + "rust.rust-analyzer.path": { + "type": ["string", "null"], + "default": null, + "description": "When specified, uses the rust-analyzer binary at a given path" } } } diff --git a/src/configuration.ts b/src/configuration.ts index edfb0cf6..dc8f5d22 100644 --- a/src/configuration.ts +++ b/src/configuration.ts @@ -50,13 +50,13 @@ export class RLSConfiguration { */ private static readChannel( wsPath: string, - rustupConfiguration: RustupConfig, + rustupPath: string, configuration: WorkspaceConfiguration, ): string { const channel = configuration.get('rust-client.channel'); if (channel === 'default' || !channel) { try { - return getActiveChannel(wsPath, rustupConfiguration); + return getActiveChannel(wsPath, rustupPath); } catch (e) { // rustup might not be installed at the time the configuration is // initially loaded, so silently ignore the error and return a default value @@ -83,6 +83,13 @@ export class RLSConfiguration { ); } + public get rustAnalyzer(): { path?: string; releaseTag: string } { + const cfg = this.configuration; + const releaseTag = cfg.get('rust.rust-analyzer.releaseTag', 'nightly'); + const path = cfg.get('rust.rust-analyzer.path'); + return { releaseTag, ...{ path } }; + } + public get revealOutputChannelOn(): RevealOutputChannelOn { return RLSConfiguration.readRevealOutputChannelOn(this.configuration); } @@ -94,7 +101,7 @@ export class RLSConfiguration { public get channel(): string { return RLSConfiguration.readChannel( this.wsPath, - this.rustupConfig(true), + this.rustupPath, this.configuration, ); } @@ -106,17 +113,22 @@ export class RLSConfiguration { return this.configuration.get('rust-client.rlsPath'); } + /** Returns the language analysis engine to be used for the workspace */ + public get engine(): 'rls' | 'rust-analyzer' { + return this.configuration.get('rust-client.engine') || 'rls'; + } + /** - * Whether RLS should be automaticallystarted when opening a relevant Rust project. + * Whether a language server should be automatically started when opening + * a relevant Rust project. */ public get autoStartRls(): boolean { return this.configuration.get('rust-client.autoStartRls', true); } - // Added ignoreChannel for readChannel function. Otherwise we end in an infinite loop. - public rustupConfig(ignoreChannel: boolean = false): RustupConfig { + public rustupConfig(): RustupConfig { return { - channel: ignoreChannel ? '' : this.channel, + channel: this.channel, path: this.rustupPath, }; } diff --git a/src/extension.ts b/src/extension.ts index dc886d74..a235e0a5 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -1,7 +1,3 @@ -import * as child_process from 'child_process'; -import * as fs from 'fs'; -import * as path from 'path'; -import * as util from 'util'; import { commands, ConfigurationTarget, @@ -16,35 +12,17 @@ import { WorkspaceFolder, WorkspaceFoldersChangeEvent, } from 'vscode'; -import { - LanguageClient, - LanguageClientOptions, - NotificationType, - ServerOptions, -} from 'vscode-languageclient'; +import * as lc from 'vscode-languageclient'; import { RLSConfiguration } from './configuration'; -import { SignatureHelpProvider } from './providers/signatureHelpProvider'; -import { checkForRls, ensureToolchain, rustupUpdate } from './rustup'; +import * as rls from './rls'; +import * as rustAnalyzer from './rustAnalyzer'; +import { rustupUpdate } from './rustup'; import { startSpinner, stopSpinner } from './spinner'; import { activateTaskProvider, Execution, runRlsCommand } from './tasks'; import { Observable } from './utils/observable'; import { nearestParentWorkspace } from './utils/workspace'; -const exec = util.promisify(child_process.exec); - -/** - * Parameter type to `window/progress` request as issued by the RLS. - * https://github.com/rust-lang/rls/blob/17a439440e6b00b1f014a49c6cf47752ecae5bb7/rls/src/lsp_data.rs#L395-L419 - */ -interface ProgressParams { - id: string; - title?: string; - message?: string; - percentage?: number; - done?: boolean; -} - /** * External API as exposed by the extension. Can be queried by other extensions * or by the integration test runner for VSCode extensions. @@ -179,19 +157,19 @@ function clientWorkspaceForUri( } /** Denotes the state or progress the workspace is currently in. */ -type WorkspaceProgress = +export type WorkspaceProgress = | { state: 'progress'; message: string } | { state: 'ready' | 'standby' }; -// We run one RLS and one corresponding language client per workspace folder -// (VSCode workspace, not Cargo workspace). This class contains all the per-client -// and per-workspace stuff. +// We run a single server/client pair per workspace folder (VSCode workspace, +// not Cargo workspace). This class contains all the per-client and +// per-workspace stuff. export class ClientWorkspace { public readonly folder: WorkspaceFolder; // FIXME(#233): Don't only rely on lazily initializing it once on startup, // handle possible `rust-client.*` value changes while extension is running private readonly config: RLSConfiguration; - private lc: LanguageClient | null = null; + private lc: lc.LanguageClient | null = null; private disposables: Disposable[]; private _progress: Observable; get progress() { @@ -215,63 +193,43 @@ export class ClientWorkspace { } public async start() { - this._progress.value = { state: 'progress', message: 'Starting' }; - - const serverOptions: ServerOptions = async () => { - await this.autoUpdate(); - return this.makeRlsProcess(); - }; - - // This accepts `vscode.GlobPattern` under the hood, which requires only - // forward slashes. It's worth mentioning that RelativePattern does *NOT* - // work in remote scenarios (?), so rely on normalized fs path from VSCode URIs. - const pattern = `${this.folder.uri.fsPath.replace(path.sep, '/')}/**`; - - const clientOptions: LanguageClientOptions = { - // Register the server for Rust files - documentSelector: [ - { language: 'rust', scheme: 'file', pattern }, - { language: 'rust', scheme: 'untitled', pattern }, - ], - diagnosticCollectionName: `rust-${this.folder.uri}`, - synchronize: { configurationSection: 'rust' }, - // Controls when to focus the channel rather than when to reveal it in the drop-down list + const { createLanguageClient, setupClient, setupProgress } = + this.config.engine === 'rls' ? rls : rustAnalyzer; + + const client = await createLanguageClient(this.folder, { + updateOnStartup: this.config.updateOnStartup, revealOutputChannelOn: this.config.revealOutputChannelOn, - initializationOptions: { - omitInitBuild: true, - cmdRun: true, + logToFile: this.config.logToFile, + rustup: { + channel: this.config.channel, + path: this.config.rustupPath, + disabled: this.config.rustupDisabled, }, - workspaceFolder: this.folder, - }; + rls: { path: this.config.rlsPath }, + rustAnalyzer: this.config.rustAnalyzer, + }); - // Create the language client and start the client. - this.lc = new LanguageClient( - 'rust-client', - 'Rust Language Server', - serverOptions, - clientOptions, - ); + client.onDidChangeState(({ newState }) => { + if (newState === lc.State.Starting) { + this._progress.value = { state: 'progress', message: 'Starting' }; + } + if (newState === lc.State.Stopped) { + this._progress.value = { state: 'standby' }; + } + }); - const selector = { language: 'rust', scheme: 'file', pattern }; + setupProgress(client, this._progress); - this.setupProgressCounter(); this.disposables.push(activateTaskProvider(this.folder)); - this.disposables.push(this.lc.start()); - this.disposables.push( - languages.registerSignatureHelpProvider( - selector, - new SignatureHelpProvider(this.lc), - '(', - ',', - ), - ); + this.disposables.push(...setupClient(client, this.folder)); + if (client.needsStart()) { + this.disposables.push(client.start()); + } } public async stop() { if (this.lc) { await this.lc.stop(); - this.lc = null; - this._progress.value = { state: 'standby' }; } this.disposables.forEach(d => d.dispose()); @@ -289,161 +247,6 @@ export class ClientWorkspace { public rustupUpdate() { return rustupUpdate(this.config.rustupConfig()); } - - private async setupProgressCounter() { - if (!this.lc) { - return; - } - - const runningProgress: Set = new Set(); - await this.lc.onReady(); - - this.lc.onNotification( - new NotificationType('window/progress'), - progress => { - if (progress.done) { - runningProgress.delete(progress.id); - } else { - runningProgress.add(progress.id); - } - if (runningProgress.size) { - let status = ''; - if (typeof progress.percentage === 'number') { - status = `${Math.round(progress.percentage * 100)}%`; - } else if (progress.message) { - status = progress.message; - } else if (progress.title) { - status = `[${progress.title.toLowerCase()}]`; - } - this._progress.value = { state: 'progress', message: status }; - } else { - this._progress.value = { state: 'ready' }; - } - }, - ); - } - - private async getSysroot(env: typeof process.env): Promise { - const rustcPrintSysroot = () => - this.config.rustupDisabled - ? exec('rustc --print sysroot', { env }) - : exec( - `${this.config.rustupPath} run ${this.config.channel} rustc --print sysroot`, - { env }, - ); - - const { stdout } = await rustcPrintSysroot(); - return stdout - .toString() - .replace('\n', '') - .replace('\r', ''); - } - - // Make an evironment to run the RLS. - private async makeRlsEnv( - args = { - setLibPath: false, - }, - ): Promise { - // Shallow clone, we don't want to modify this process' $PATH or - // $(DY)LD_LIBRARY_PATH - const env = { ...process.env }; - - let sysroot: string | undefined; - try { - sysroot = await this.getSysroot(env); - } catch (err) { - console.info(err.message); - console.info(`Let's retry with extended $PATH`); - env.PATH = `${env.HOME || '~'}/.cargo/bin:${env.PATH || ''}`; - try { - sysroot = await this.getSysroot(env); - } catch (e) { - console.warn('Error reading sysroot (second try)', e); - window.showWarningMessage(`Error reading sysroot: ${e.message}`); - return env; - } - } - - console.info(`Setting sysroot to`, sysroot); - if (args.setLibPath) { - function appendEnv(envVar: string, newComponent: string) { - const old = process.env[envVar]; - return old ? `${newComponent}:${old}` : newComponent; - } - const newComponent = path.join(sysroot, 'lib'); - env.DYLD_LIBRARY_PATH = appendEnv('DYLD_LIBRARY_PATH', newComponent); - env.LD_LIBRARY_PATH = appendEnv('LD_LIBRARY_PATH', newComponent); - } - - return env; - } - - private async makeRlsProcess(): Promise { - // Run "rls" from the PATH unless there's an override. - const rlsPath = this.config.rlsPath || '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. - // NOTE: Needs an installed toolchain when using rustup, hence we don't call - // it immediately here. - const makeRlsEnv = () => - this.makeRlsEnv({ - setLibPath: this.config.rustupDisabled, - }); - const cwd = this.folder.uri.fsPath; - - let childProcess: child_process.ChildProcess; - if (this.config.rustupDisabled) { - console.info(`running without rustup: ${rlsPath}`); - const env = await makeRlsEnv(); - - childProcess = child_process.spawn(rlsPath, [], { - env, - cwd, - shell: true, - }); - } else { - console.info(`running with rustup: ${rlsPath}`); - const config = this.config.rustupConfig(); - - await ensureToolchain(config); - if (!this.config.rlsPath) { - // We only need a rustup-installed RLS if we weren't given a - // custom RLS path. - console.info('will use a rustup-installed RLS; ensuring present'); - await checkForRls(config); - } - - const env = await makeRlsEnv(); - childProcess = child_process.spawn( - config.path, - ['run', config.channel, rlsPath], - { env, cwd, shell: true }, - ); - } - - childProcess.on('error', (err: { code?: string; message: string }) => { - if (err.code === 'ENOENT') { - console.error(`Could not spawn RLS: ${err.message}`); - window.showWarningMessage(`Could not spawn RLS: \`${err.message}\``); - } - }); - - if (this.config.logToFile) { - const logPath = path.join(this.folder.uri.fsPath, `rls${Date.now()}.log`); - const logStream = fs.createWriteStream(logPath, { flags: 'w+' }); - childProcess.stderr?.pipe(logStream); - } - - return childProcess; - } - - private async autoUpdate() { - if (this.config.updateOnStartup && !this.config.rustupDisabled) { - await rustupUpdate(this.config.rustupConfig()); - } - } } /** @@ -459,27 +262,17 @@ const activeWorkspace = new Observable(null); */ function registerCommands(): Disposable[] { return [ - commands.registerCommand( - 'rls.update', - () => activeWorkspace.value && activeWorkspace.value.rustupUpdate(), - ), - commands.registerCommand( - 'rls.restart', - async () => activeWorkspace.value && activeWorkspace.value.restart(), - ), - commands.registerCommand( - 'rls.run', - (cmd: Execution) => - activeWorkspace.value && activeWorkspace.value.runRlsCommand(cmd), + commands.registerCommand('rls.update', () => + activeWorkspace.value?.rustupUpdate(), ), - commands.registerCommand( - 'rls.start', - () => activeWorkspace.value && activeWorkspace.value.start(), + commands.registerCommand('rls.restart', async () => + activeWorkspace.value?.restart(), ), - commands.registerCommand( - 'rls.stop', - () => activeWorkspace.value && activeWorkspace.value.stop(), + commands.registerCommand('rls.run', (cmd: Execution) => + activeWorkspace.value?.runRlsCommand(cmd), ), + commands.registerCommand('rls.start', () => activeWorkspace.value?.start()), + commands.registerCommand('rls.stop', () => activeWorkspace.value?.stop()), ]; } diff --git a/src/net.ts b/src/net.ts new file mode 100644 index 00000000..26cd05fb --- /dev/null +++ b/src/net.ts @@ -0,0 +1,148 @@ +import * as assert from 'assert'; +import * as fs from 'fs'; +import fetch from 'node-fetch'; +import * as stream from 'stream'; +import * as util from 'util'; +import * as vscode from 'vscode'; + +const pipeline = util.promisify(stream.pipeline); + +const GITHUB_API_ENDPOINT_URL = 'https://api.github.com'; + +export async function fetchRelease( + owner: string, + repository: string, + releaseTag: string, +): Promise { + const apiEndpointPath = `/repos/${owner}/${repository}/releases/tags/${releaseTag}`; + + const requestUrl = GITHUB_API_ENDPOINT_URL + apiEndpointPath; + + console.debug( + 'Issuing request for released artifacts metadata to', + requestUrl, + ); + + const response = await fetch(requestUrl, { + headers: { Accept: 'application/vnd.github.v3+json' }, + }); + + if (!response.ok) { + console.error('Error fetching artifact release info', { + requestUrl, + releaseTag, + response: { + headers: response.headers, + status: response.status, + body: await response.text(), + }, + }); + + throw new Error( + `Got response ${response.status} when trying to fetch ` + + `release info for ${releaseTag} release`, + ); + } + + // We skip runtime type checks for simplicity (here we cast from `any` to `GithubRelease`) + const release: GithubRelease = await response.json(); + return release; +} + +// We omit declaration of tremendous amount of fields that we are not using here +export interface GithubRelease { + name: string; + id: number; + // eslint-disable-next-line camelcase + published_at: string; + assets: Array<{ + name: string; + // eslint-disable-next-line camelcase + browser_download_url: string; + }>; +} + +export async function download( + downloadUrl: string, + destinationPath: string, + progressTitle: string, + { mode }: { mode?: number } = {}, +) { + await vscode.window.withProgress( + { + location: vscode.ProgressLocation.Notification, + cancellable: false, + title: progressTitle, + }, + async (progress, _cancellationToken) => { + let lastPercentage = 0; + await downloadFile( + downloadUrl, + destinationPath, + mode, + (readBytes, totalBytes) => { + const newPercentage = (readBytes / totalBytes) * 100; + progress.report({ + message: newPercentage.toFixed(0) + '%', + increment: newPercentage - lastPercentage, + }); + + lastPercentage = newPercentage; + }, + ); + }, + ); +} + +/** + * Downloads file from `url` and stores it at `destFilePath` with `destFilePermissions`. + * `onProgress` callback is called on recieveing each chunk of bytes + * to track the progress of downloading, it gets the already read and total + * amount of bytes to read as its parameters. + */ +async function downloadFile( + url: string, + destFilePath: fs.PathLike, + mode: number | undefined, + onProgress: (readBytes: number, totalBytes: number) => void, +): Promise { + const res = await fetch(url); + + if (!res.ok) { + console.error('Error', res.status, 'while downloading file from', url); + console.error({ body: await res.text(), headers: res.headers }); + + throw new Error( + `Got response ${res.status} when trying to download a file.`, + ); + } + + const totalBytes = Number(res.headers.get('content-length')); + assert(!Number.isNaN(totalBytes), 'Sanity check of content-length protocol'); + + console.debug( + 'Downloading file of', + totalBytes, + 'bytes size from', + url, + 'to', + destFilePath, + ); + + let readBytes = 0; + res.body.on('data', (chunk: Buffer) => { + readBytes += chunk.length; + onProgress(readBytes, totalBytes); + }); + + const destFileStream = fs.createWriteStream(destFilePath, { mode }); + + await pipeline(res.body, destFileStream); + return new Promise(resolve => { + destFileStream.on('close', resolve); + destFileStream.destroy(); + + // Details on workaround: https://github.com/rust-analyzer/rust-analyzer/pull/3092#discussion_r378191131 + // Issue at nodejs repo: https://github.com/nodejs/node/issues/31776 + }); +} diff --git a/src/rls.ts b/src/rls.ts new file mode 100644 index 00000000..60b965c9 --- /dev/null +++ b/src/rls.ts @@ -0,0 +1,244 @@ +import * as child_process from 'child_process'; +import * as fs from 'fs'; +import * as path from 'path'; +import { promisify } from 'util'; + +import * as vs from 'vscode'; +import * as lc from 'vscode-languageclient'; + +import { WorkspaceProgress } from './extension'; +import { SignatureHelpProvider } from './providers/signatureHelpProvider'; +import { ensureComponents, ensureToolchain, rustupUpdate } from './rustup'; +import { Observable } from './utils/observable'; + +const exec = promisify(child_process.exec); + +/** Rustup components required for the RLS to work correctly. */ +const REQUIRED_COMPONENTS = ['rust-analysis', 'rust-src', 'rls']; + +/** + * Parameter type to `window/progress` request as issued by the RLS. + * https://github.com/rust-lang/rls/blob/17a439440e6b00b1f014a49c6cf47752ecae5bb7/rls/src/lsp_data.rs#L395-L419 + */ +interface ProgressParams { + id: string; + title?: string; + message?: string; + percentage?: number; + done?: boolean; +} + +export function createLanguageClient( + folder: vs.WorkspaceFolder, + config: { + updateOnStartup?: boolean; + revealOutputChannelOn?: lc.RevealOutputChannelOn; + logToFile?: boolean; + rustup: { disabled: boolean; path: string; channel: string }; + rls: { path?: string }; + }, +): lc.LanguageClient { + const serverOptions: lc.ServerOptions = async () => { + if (config.updateOnStartup && !config.rustup.disabled) { + await rustupUpdate(config.rustup); + } + return makeRlsProcess( + config.rustup, + { + path: config.rls.path, + cwd: folder.uri.fsPath, + }, + { logToFile: config.logToFile }, + ); + }; + + const clientOptions: lc.LanguageClientOptions = { + // Register the server for Rust files + documentSelector: [ + { language: 'rust', scheme: 'untitled' }, + documentFilter(folder), + ], + diagnosticCollectionName: `rust-${folder.uri}`, + synchronize: { configurationSection: 'rust' }, + // Controls when to focus the channel rather than when to reveal it in the drop-down list + revealOutputChannelOn: config.revealOutputChannelOn, + initializationOptions: { + omitInitBuild: true, + cmdRun: true, + }, + workspaceFolder: folder, + }; + + return new lc.LanguageClient( + 'rust-client', + 'Rust Language Server', + serverOptions, + clientOptions, + ); +} + +export function setupClient( + client: lc.LanguageClient, + folder: vs.WorkspaceFolder, +): vs.Disposable[] { + return [ + vs.languages.registerSignatureHelpProvider( + documentFilter(folder), + new SignatureHelpProvider(client), + '(', + ',', + ), + ]; +} + +export function setupProgress( + client: lc.LanguageClient, + observableProgress: Observable, +) { + const runningProgress: Set = new Set(); + // We can only register notification handler after the client is ready + client.onReady().then(() => + client.onNotification( + new lc.NotificationType('window/progress'), + progress => { + if (progress.done) { + runningProgress.delete(progress.id); + } else { + runningProgress.add(progress.id); + } + if (runningProgress.size) { + let status = ''; + if (typeof progress.percentage === 'number') { + status = `${Math.round(progress.percentage * 100)}%`; + } else if (progress.message) { + status = progress.message; + } else if (progress.title) { + status = `[${progress.title.toLowerCase()}]`; + } + observableProgress.value = { state: 'progress', message: status }; + } else { + observableProgress.value = { state: 'ready' }; + } + }, + ), + ); +} + +function documentFilter(folder: vs.WorkspaceFolder): lc.DocumentFilter { + // This accepts `vscode.GlobPattern` under the hood, which requires only + // forward slashes. It's worth mentioning that RelativePattern does *NOT* + // work in remote scenarios (?), so rely on normalized fs path from VSCode URIs. + const pattern = `${folder.uri.fsPath.replace(path.sep, '/')}/**`; + + return { language: 'rust', scheme: 'file', pattern }; +} + +async function getSysroot( + rustup: { disabled: boolean; path: string; channel: string }, + env: typeof process.env, +): Promise { + const printSysrootCmd = rustup.disabled + ? 'rustc --print sysroot' + : `${rustup.path} run ${rustup.channel} rustc --print sysroot`; + + const { stdout } = await exec(printSysrootCmd, { env }); + return stdout.toString().trim(); +} + +// Make an evironment to run the RLS. +async function makeRlsEnv( + rustup: { disabled: boolean; path: string; channel: string }, + opts = { + setLibPath: false, + }, +): Promise { + // Shallow clone, we don't want to modify this process' $PATH or + // $(DY)LD_LIBRARY_PATH + const env = { ...process.env }; + + let sysroot: string | undefined; + try { + sysroot = await getSysroot(rustup, env); + } catch (err) { + console.info(err.message); + console.info(`Let's retry with extended $PATH`); + env.PATH = `${env.HOME || '~'}/.cargo/bin:${env.PATH || ''}`; + try { + sysroot = await getSysroot(rustup, env); + } catch (e) { + console.warn('Error reading sysroot (second try)', e); + vs.window.showWarningMessage(`Error reading sysroot: ${e.message}`); + return env; + } + } + + console.info(`Setting sysroot to`, sysroot); + if (opts.setLibPath) { + function appendEnv(envVar: string, newComponent: string) { + const old = process.env[envVar]; + return old ? `${newComponent}:${old}` : newComponent; + } + const newComponent = path.join(sysroot, 'lib'); + env.DYLD_LIBRARY_PATH = appendEnv('DYLD_LIBRARY_PATH', newComponent); + env.LD_LIBRARY_PATH = appendEnv('LD_LIBRARY_PATH', newComponent); + } + + return env; +} + +async function makeRlsProcess( + rustup: { disabled: boolean; path: string; channel: string }, + rls: { path?: string; cwd: string }, + options: { logToFile?: boolean } = {}, +): Promise { + // Run "rls" from the PATH unless there's an override. + const rlsPath = rls.path || 'rls'; + const cwd = rls.cwd; + + let childProcess: child_process.ChildProcess; + if (rustup.disabled) { + console.info(`running without rustup: ${rlsPath}`); + // Set [DY]LD_LIBRARY_PATH ourselves, since that's usually done automatically + // by rustup when it chooses a toolchain + const env = await makeRlsEnv(rustup, { setLibPath: true }); + + childProcess = child_process.spawn(rlsPath, [], { + env, + cwd, + shell: true, + }); + } else { + console.info(`running with rustup: ${rlsPath}`); + const config = rustup; + + await ensureToolchain(config); + if (!rls.path) { + // We only need a rustup-installed RLS if we weren't given a + // custom RLS path. + console.info('will use a rustup-installed RLS; ensuring present'); + await ensureComponents(config, REQUIRED_COMPONENTS); + } + + const env = await makeRlsEnv(rustup, { setLibPath: false }); + childProcess = child_process.spawn( + config.path, + ['run', config.channel, rlsPath], + { env, cwd, shell: true }, + ); + } + + childProcess.on('error', (err: { code?: string; message: string }) => { + if (err.code === 'ENOENT') { + console.error(`Could not spawn RLS: ${err.message}`); + vs.window.showWarningMessage(`Could not spawn RLS: \`${err.message}\``); + } + }); + + if (options.logToFile) { + const logPath = path.join(rls.cwd, `rls${Date.now()}.log`); + const logStream = fs.createWriteStream(logPath, { flags: 'w+' }); + childProcess.stderr?.pipe(logStream); + } + + return childProcess; +} diff --git a/src/rustAnalyzer.ts b/src/rustAnalyzer.ts new file mode 100644 index 00000000..459312ef --- /dev/null +++ b/src/rustAnalyzer.ts @@ -0,0 +1,326 @@ +import * as child_process from 'child_process'; +import * as fs from 'fs'; +import * as path from 'path'; +import { promisify } from 'util'; + +import * as vs from 'vscode'; +import * as lc from 'vscode-languageclient'; + +import { WorkspaceProgress } from './extension'; +import { download, fetchRelease } from './net'; +import * as rustup from './rustup'; +import { Observable } from './utils/observable'; + +const stat = promisify(fs.stat); +const mkdir = promisify(fs.mkdir); +const readFile = promisify(fs.readFile); +const writeFile = promisify(fs.writeFile); + +const REQUIRED_COMPONENTS = ['rust-src']; + +/** Returns a path where rust-analyzer should be installed. */ +function installDir(): string | undefined { + if (process.platform === 'linux' || process.platform === 'darwin') { + // Prefer, in this order: + // 1. $XDG_BIN_HOME (proposed addition to XDG spec) + // 2. $XDG_DATA_HOME/../bin/ + // 3. $HOME/.local/bin/ + const { HOME, XDG_DATA_HOME, XDG_BIN_HOME } = process.env; + if (XDG_BIN_HOME) { + return path.resolve(XDG_BIN_HOME); + } + + const baseDir = XDG_DATA_HOME + ? path.join(XDG_DATA_HOME, '..') + : HOME && path.join(HOME, '.local'); + return baseDir && path.resolve(path.join(baseDir, 'bin')); + } else if (process.platform === 'win32') { + // %LocalAppData%\rust-analyzer\ + const { LocalAppData } = process.env; + return ( + LocalAppData && path.resolve(path.join(LocalAppData, 'rust-analyzer')) + ); + } + + return undefined; +} + +/** Returns a path where persistent data for rust-analyzer should be installed. */ +function metadataDir(): string | undefined { + if (process.platform === 'linux' || process.platform === 'darwin') { + // Prefer, in this order: + // 1. $XDG_CONFIG_HOME/rust-analyzer + // 2. $HOME/.config/rust-analyzer + const { HOME, XDG_CONFIG_HOME } = process.env; + const baseDir = XDG_CONFIG_HOME || (HOME && path.join(HOME, '.config')); + + return baseDir && path.resolve(path.join(baseDir, 'rust-analyzer')); + } else if (process.platform === 'win32') { + // %LocalAppData%\rust-analyzer\ + const { LocalAppData } = process.env; + return ( + LocalAppData && path.resolve(path.join(LocalAppData, 'rust-analyzer')) + ); + } + + return undefined; +} + +function ensureDir(path: string) { + return !!path && stat(path).catch(() => mkdir(path, { recursive: true })); +} + +interface RustAnalyzerConfig { + askBeforeDownload?: boolean; + package: { + releaseTag: string; + }; +} + +interface Metadata { + releaseTag: string; +} + +async function readMetadata(): Promise { + const stateDir = metadataDir(); + if (!stateDir) { + return { kind: 'error', code: 'NotSupported' }; + } + + const filePath = path.join(stateDir, 'metadata.json'); + if (!(await stat(filePath).catch(() => false))) { + return { kind: 'error', code: 'FileMissing' }; + } + + const contents = await readFile(filePath, 'utf8'); + const obj = JSON.parse(contents); + return typeof obj === 'object' ? obj : {}; +} + +async function writeMetadata(config: Metadata) { + const stateDir = metadataDir(); + if (!stateDir) { + return false; + } + + if (!(await ensureDir(stateDir))) { + return false; + } + + const filePath = path.join(stateDir, 'metadata.json'); + return writeFile(filePath, JSON.stringify(config)).then(() => true); +} + +export async function getServer({ + askBeforeDownload, + package: pkg, +}: RustAnalyzerConfig): Promise { + let binaryName: string | undefined; + if (process.arch === 'x64' || process.arch === 'ia32') { + if (process.platform === 'linux') { + binaryName = 'rust-analyzer-linux'; + } + if (process.platform === 'darwin') { + binaryName = 'rust-analyzer-mac'; + } + if (process.platform === 'win32') { + binaryName = 'rust-analyzer-windows.exe'; + } + } + if (binaryName === undefined) { + vs.window.showErrorMessage( + "Unfortunately we don't ship binaries for your platform yet. " + + 'You need to manually clone rust-analyzer repository and ' + + 'run `cargo xtask install --server` to build the language server from sources. ' + + 'If you feel that your platform should be supported, please create an issue ' + + 'about that [here](https://github.com/rust-analyzer/rust-analyzer/issues) and we ' + + 'will consider it.', + ); + return undefined; + } + + const dir = installDir(); + if (!dir) { + return; + } + await ensureDir(dir); + + const metadata: Partial = await readMetadata().catch(() => ({})); + + const dest = path.join(dir, binaryName); + const exists = await stat(dest).catch(() => false); + if (exists && metadata.releaseTag === pkg.releaseTag) { + return dest; + } + + if (askBeforeDownload) { + const userResponse = await vs.window.showInformationMessage( + `${ + metadata.releaseTag && metadata.releaseTag !== pkg.releaseTag + ? `You seem to have installed release \`${metadata.releaseTag}\` but requested a different one.` + : '' + } + Release \`${pkg.releaseTag}\` of rust-analyzer is not installed.\n + Install to ${dir}?`, + 'Download', + ); + if (userResponse !== 'Download') { + return dest; + } + } + + const release = await fetchRelease( + 'rust-analyzer', + 'rust-analyzer', + pkg.releaseTag, + ); + const artifact = release.assets.find(asset => asset.name === binaryName); + if (!artifact) { + throw new Error(`Bad release: ${JSON.stringify(release)}`); + } + + await download( + artifact.browser_download_url, + dest, + 'Downloading rust-analyzer server', + { mode: 0o755 }, + ); + + await writeMetadata({ releaseTag: pkg.releaseTag }).catch(() => { + vs.window.showWarningMessage(`Couldn't save rust-analyzer metadata`); + }); + + return dest; +} + +/** + * Rust Analyzer does not work in an isolated environment and greedily analyzes + * the workspaces itself, so make sure to spawn only a single instance. + */ +let INSTANCE: lc.LanguageClient | undefined; + +/** + * TODO: + * Global observable progress + */ +const PROGRESS: Observable = new Observable< + WorkspaceProgress +>({ state: 'standby' }); + +export async function createLanguageClient( + folder: vs.WorkspaceFolder, + config: { + revealOutputChannelOn?: lc.RevealOutputChannelOn; + logToFile?: boolean; + rustup: { disabled: boolean; path: string; channel: string }; + rustAnalyzer: { path?: string; releaseTag: string }; + }, +): Promise { + await rustup.ensureToolchain(config.rustup); + await rustup.ensureComponents(config.rustup, REQUIRED_COMPONENTS); + if (!config.rustAnalyzer.path) { + await getServer({ + askBeforeDownload: true, + package: { releaseTag: config.rustAnalyzer.releaseTag }, + }); + } + + if (INSTANCE) { + return INSTANCE; + } + + const serverOptions: lc.ServerOptions = async () => { + const binPath = + config.rustAnalyzer.path || + (await getServer({ + package: { releaseTag: config.rustAnalyzer.releaseTag }, + })); + + if (!binPath) { + throw new Error("Couldn't fetch Rust Analyzer binary"); + } + + const childProcess = child_process.exec(binPath); + if (config.logToFile) { + const logPath = path.join(folder.uri.fsPath, `ra-${Date.now()}.log`); + const logStream = fs.createWriteStream(logPath, { flags: 'w+' }); + childProcess.stderr?.pipe(logStream); + } + + return childProcess; + }; + + const clientOptions: lc.LanguageClientOptions = { + // Register the server for Rust files + documentSelector: [ + { language: 'rust', scheme: 'file' }, + { language: 'rust', scheme: 'untitled' }, + ], + diagnosticCollectionName: `rust`, + // synchronize: { configurationSection: 'rust' }, + // Controls when to focus the channel rather than when to reveal it in the drop-down list + revealOutputChannelOn: config.revealOutputChannelOn, + // TODO: Support and type out supported settings by the rust-analyzer + initializationOptions: vs.workspace.getConfiguration('rust.rust-analyzer'), + }; + + INSTANCE = new lc.LanguageClient( + 'rust-client', + 'Rust Analyzer', + serverOptions, + clientOptions, + ); + + // Enable semantic highlighting which is available in stable VSCode + INSTANCE.registerProposedFeatures(); + // We can install only one progress handler so make sure to do that when + // setting up the singleton instance + setupGlobalProgress(INSTANCE); + + return INSTANCE; +} + +async function setupGlobalProgress(client: lc.LanguageClient) { + client.onDidChangeState(async ({ newState }) => { + if (newState === lc.State.Starting) { + await client.onReady(); + + const RUST_ANALYZER_PROGRESS = 'rustAnalyzer/startup'; + client.onProgress( + new lc.ProgressType<{ + kind: 'begin' | 'report' | 'end'; + message?: string; + }>(), + RUST_ANALYZER_PROGRESS, + ({ kind, message: msg }) => { + if (kind === 'report') { + PROGRESS.value = { state: 'progress', message: msg || '' }; + } + if (kind === 'end') { + PROGRESS.value = { state: 'ready' }; + } + }, + ); + } + }); +} + +export function setupClient( + _client: lc.LanguageClient, + _folder: vs.WorkspaceFolder, +): vs.Disposable[] { + return []; +} + +export function setupProgress( + _client: lc.LanguageClient, + workspaceProgress: Observable, +) { + workspaceProgress.value = PROGRESS.value; + // We can only ever install one progress handler per language client and since + // we can only ever have one instance of Rust Analyzer, fake the global + // progress as a workspace one. + PROGRESS.observe(progress => { + workspaceProgress.value = progress; + }); +} diff --git a/src/rustup.ts b/src/rustup.ts index 9b812f73..34663a5f 100644 --- a/src/rustup.ts +++ b/src/rustup.ts @@ -1,6 +1,7 @@ /** - * @file This module handles running the RLS via rustup, including checking that - * rustup is installed and installing any required components/toolchains. + * @file This module wraps the most commonly used rustup interface, e.g. + * seeing if rustup is installed or probing for/installing the Rust toolchain + * components. */ import * as child_process from 'child_process'; import * as util from 'util'; @@ -11,8 +12,6 @@ import { runTaskCommand } from './tasks'; const exec = util.promisify(child_process.exec); -const REQUIRED_COMPONENTS = ['rust-analysis', 'rust-src', 'rls']; - function isInstalledRegex(componentName: string): RegExp { return new RegExp(`^(${componentName}.*) \\((default|installed)\\)$`); } @@ -62,20 +61,26 @@ export async function ensureToolchain(config: RustupConfig) { } /** - * Checks for required RLS components and prompts the user to install if it's - * not already. + * Checks for the required toolchain components and prompts the user to install + * them if they're missing. */ -export async function checkForRls(config: RustupConfig) { - if (await hasRlsComponents(config)) { +export async function ensureComponents( + config: RustupConfig, + components: string[], +) { + if (await hasComponents(config, components)) { return; } const clicked = await Promise.resolve( - window.showInformationMessage('RLS not installed. Install?', 'Yes'), + window.showInformationMessage( + 'Some Rust components not installed. Install?', + 'Yes', + ), ); if (clicked) { - await installRlsComponents(config); - window.showInformationMessage('RLS successfully installed! Enjoy! ❤️'); + await installComponents(config, components); + window.showInformationMessage('Rust components successfully installed!'); } else { throw new Error(); } @@ -137,25 +142,31 @@ async function listComponents(config: RustupConfig): Promise { ); } -async function hasRlsComponents(config: RustupConfig): Promise { +export async function hasComponents( + config: RustupConfig, + components: string[], +): Promise { try { - const components = await listComponents(config); + const existingComponents = await listComponents(config); - return REQUIRED_COMPONENTS.map(isInstalledRegex).every(isInstalledRegex => - components.some(c => isInstalledRegex.test(c)), - ); + return components + .map(isInstalledRegex) + .every(isInstalledRegex => + existingComponents.some(c => isInstalledRegex.test(c)), + ); } catch (e) { console.log(e); - window.showErrorMessage(`Can't detect RLS components: ${e.message}`); - stopSpinner("Can't detect RLS components"); + window.showErrorMessage(`Can't detect components: ${e.message}`); + stopSpinner("Can't detect components"); throw e; } } -async function installRlsComponents(config: RustupConfig) { - startSpinner('Installing components…'); - - for (const component of REQUIRED_COMPONENTS) { +export async function installComponents( + config: RustupConfig, + components: string[], +) { + for (const component of components) { try { const command = config.path; const args = [ @@ -183,8 +194,6 @@ async function installRlsComponents(config: RustupConfig) { throw e; } } - - stopSpinner('RLS components installed successfully'); } /** @@ -248,7 +257,7 @@ export function hasRustup(config: RustupConfig): Promise { * Returns active (including local overrides) toolchain, as specified by rustup. * May throw if rustup at specified path can't be executed. */ -export function getActiveChannel(wsPath: string, config: RustupConfig): string { +export function getActiveChannel(wsPath: string, rustupPath: string): string { // rustup info might differ depending on where it's executed // (e.g. when a toolchain is locally overriden), so executing it // under our current workspace root should give us close enough result @@ -257,7 +266,7 @@ export function getActiveChannel(wsPath: string, config: RustupConfig): string { try { // `rustup show active-toolchain` is available since rustup 1.12.0 activeChannel = child_process - .execSync(`${config.path} show active-toolchain`, { + .execSync(`${rustupPath} show active-toolchain`, { cwd: wsPath, }) .toString() @@ -270,7 +279,7 @@ export function getActiveChannel(wsPath: string, config: RustupConfig): string { } catch (e) { // Possibly an old rustup version, so try rustup show const showOutput = child_process - .execSync(`${config.path} show`, { + .execSync(`${rustupPath} show`, { cwd: wsPath, }) .toString(); diff --git a/src/spinner.ts b/src/spinner.ts index 6900a857..6c7c4272 100644 --- a/src/spinner.ts +++ b/src/spinner.ts @@ -1,9 +1,9 @@ import { window } from 'vscode'; export function startSpinner(message: string) { - window.setStatusBarMessage(`RLS $(settings-gear~spin) ${message}`); + window.setStatusBarMessage(`Rust: $(settings-gear~spin) ${message}`); } export function stopSpinner(message?: string) { - window.setStatusBarMessage(`RLS ${message || ''}`); + window.setStatusBarMessage(message ? `Rust: ${message}` : 'Rust'); } diff --git a/test/suite/rustup.test.ts b/test/suite/rustup.test.ts index ae8b80d6..4167d83c 100644 --- a/test/suite/rustup.test.ts +++ b/test/suite/rustup.test.ts @@ -18,6 +18,6 @@ suite('Rustup Tests', () => { assert(rustupVersion.includes(`rustup ${version}`)); }); test('getActiveChannel', async () => { - rustup.getActiveChannel('.', config); + rustup.getActiveChannel('.', config.path); }); });