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);
});
});