diff --git a/docs/user/README.md b/docs/user/README.md
index da99a063c68..3da30a19371 100644
--- a/docs/user/README.md
+++ b/docs/user/README.md
@@ -31,7 +31,38 @@ a minimum version of 10 installed. Please refer to
You will also need the most recent version of VS Code: we don't try to
maintain compatibility with older versions yet.
-The experimental VS Code plugin can then be built and installed by executing the
+### Installation from prebuilt binaries
+
+We ship prebuilt binaries for Linux, Mac and Windows via
+[GitHub releases](https://github.com/rust-analyzer/rust-analyzer/releases).
+In order to use them you need to install the client VSCode extension.
+
+Publishing to VSCode marketplace is currently WIP. Thus, you need to clone the repository and install **only** the client extension via
+```
+$ git clone https://github.com/rust-analyzer/rust-analyzer.git --depth 1
+$ cd rust-analyzer
+$ cargo xtask install --client-code
+```
+Then open VSCode (or reload the window if it was already running), open some Rust project and you should
+see an info message pop-up.
+
+
+
+
+
+Click `Download now`, wait until the progress is 100% and you are ready to go.
+
+For updates you need to remove installed binary
+```
+rm -rf ${HOME}/.config/Code/User/globalStorage/matklad.rust-analyzer
+```
+
+`"Donwload latest language server"` command for VSCode and automatic updates detection is currently WIP.
+
+
+### Installation from sources
+
+The experimental VS Code plugin can be built and installed by executing the
following commands:
```
@@ -46,6 +77,7 @@ doesn't, report bugs!
**Note** [#1831](https://github.com/rust-analyzer/rust-analyzer/issues/1831): If you are using the popular
[Vim emulation plugin](https://github.com/VSCodeVim/Vim), you will likely
need to turn off the `rust-analyzer.enableEnhancedTyping` setting.
+(// TODO: This configuration is no longer available, enhanced typing shoud be disabled via removing Enter key binding, [see this issue](https://github.com/rust-analyzer/rust-analyzer/issues/3051))
If you have an unusual setup (for example, `code` is not in the `PATH`), you
should adapt these manual installation instructions:
diff --git a/editors/code/package-lock.json b/editors/code/package-lock.json
index 353af06bf73..5c056463e08 100644
--- a/editors/code/package-lock.json
+++ b/editors/code/package-lock.json
@@ -82,6 +82,15 @@
"integrity": "sha512-nf1LMGZvgFX186geVZR1xMZKKblJiRfiASTHw85zED2kI1yDKHDwTKMdkaCbTlXoRKlGKaDfYywt+V0As30q3w==",
"dev": true
},
+ "@types/node-fetch": {
+ "version": "2.5.4",
+ "resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.5.4.tgz",
+ "integrity": "sha512-Oz6id++2qAOFuOlE1j0ouk1dzl3mmI1+qINPNBhi9nt/gVOz0G+13Ao6qjhdF0Ys+eOkhu6JnFmt38bR3H0POQ==",
+ "dev": true,
+ "requires": {
+ "@types/node": "*"
+ }
+ },
"@types/resolve": {
"version": "0.0.8",
"resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-0.0.8.tgz",
@@ -91,6 +100,12 @@
"@types/node": "*"
}
},
+ "@types/throttle-debounce": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/@types/throttle-debounce/-/throttle-debounce-2.1.0.tgz",
+ "integrity": "sha512-5eQEtSCoESnh2FsiLTxE121IiE60hnMqcb435fShf4bpLRjEu1Eoekht23y6zXS9Ts3l+Szu3TARnTsA0GkOkQ==",
+ "dev": true
+ },
"@types/vscode": {
"version": "1.41.0",
"resolved": "https://registry.npmjs.org/@types/vscode/-/vscode-1.41.0.tgz",
@@ -536,6 +551,11 @@
"integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==",
"dev": true
},
+ "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",
@@ -719,6 +739,11 @@
"has-flag": "^3.0.0"
}
},
+ "throttle-debounce": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/throttle-debounce/-/throttle-debounce-2.1.0.tgz",
+ "integrity": "sha512-AOvyNahXQuU7NN+VVvOOX+uW6FPaWdAOdRP5HfwYxAfCzXTFKRMoIMk+n+po318+ktcChx+F1Dd91G3YHeMKyg=="
+ },
"tmp": {
"version": "0.0.29",
"resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.29.tgz",
diff --git a/editors/code/package.json b/editors/code/package.json
index 11d37053eb4..f687eb8d458 100644
--- a/editors/code/package.json
+++ b/editors/code/package.json
@@ -25,18 +25,22 @@
},
"dependencies": {
"jsonc-parser": "^2.1.0",
+ "node-fetch": "^2.6.0",
+ "throttle-debounce": "^2.1.0",
"vscode-languageclient": "^6.1.0"
},
"devDependencies": {
"@rollup/plugin-commonjs": "^11.0.2",
"@rollup/plugin-node-resolve": "^7.1.1",
"@types/node": "^12.12.25",
+ "@types/node-fetch": "^2.5.4",
+ "@types/throttle-debounce": "^2.1.0",
"@types/vscode": "^1.41.0",
"rollup": "^1.31.0",
"tslib": "^1.10.0",
"tslint": "^5.20.1",
- "typescript-formatter": "^7.2.2",
"typescript": "^3.7.5",
+ "typescript-formatter": "^7.2.2",
"vsce": "^1.71.0"
},
"activationEvents": [
@@ -169,10 +173,11 @@
},
"rust-analyzer.raLspServerPath": {
"type": [
+ "null",
"string"
],
- "default": "ra_lsp_server",
- "description": "Path to ra_lsp_server executable"
+ "default": null,
+ "description": "Path to ra_lsp_server executable (points to bundled binary by default)"
},
"rust-analyzer.excludeGlobs": {
"type": "array",
diff --git a/editors/code/src/client.ts b/editors/code/src/client.ts
index 7e7e909ddab..2e3d4aba2d8 100644
--- a/editors/code/src/client.ts
+++ b/editors/code/src/client.ts
@@ -1,24 +1,18 @@
-import { homedir } from 'os';
import * as lc from 'vscode-languageclient';
-import { spawnSync } from 'child_process';
import { window, workspace } from 'vscode';
import { Config } from './config';
+import { ensureLanguageServerBinary } from './installation/language_server';
-export function createClient(config: Config): lc.LanguageClient {
+export async function createClient(config: Config): Promise {
// '.' Is the fallback if no folder is open
// TODO?: Workspace folders support Uri's (eg: file://test.txt).
// It might be a good idea to test if the uri points to a file.
const workspaceFolderPath = workspace.workspaceFolders?.[0]?.uri.fsPath ?? '.';
- const raLspServerPath = expandPathResolving(config.raLspServerPath);
- if (spawnSync(raLspServerPath, ["--version"]).status !== 0) {
- window.showErrorMessage(
- `Unable to execute '${raLspServerPath} --version'\n\n` +
- `Perhaps it is not in $PATH?\n\n` +
- `PATH=${process.env.PATH}\n`
- );
- }
+ const raLspServerPath = await ensureLanguageServerBinary(config.langServerSource);
+ if (!raLspServerPath) return null;
+
const run: lc.Executable = {
command: raLspServerPath,
options: { cwd: workspaceFolderPath },
@@ -87,9 +81,3 @@ export function createClient(config: Config): lc.LanguageClient {
res.registerProposedFeatures();
return res;
}
-function expandPathResolving(path: string) {
- if (path.startsWith('~/')) {
- return path.replace('~', homedir());
- }
- return path;
-}
diff --git a/editors/code/src/config.ts b/editors/code/src/config.ts
index 52462043392..d5f3da2ed85 100644
--- a/editors/code/src/config.ts
+++ b/editors/code/src/config.ts
@@ -1,4 +1,6 @@
+import * as os from "os";
import * as vscode from 'vscode';
+import { BinarySource } from "./installation/interfaces";
const RA_LSP_DEBUG = process.env.__RA_LSP_SERVER_DEBUG;
@@ -16,10 +18,11 @@ export interface CargoFeatures {
}
export class Config {
+ langServerSource!: null | BinarySource;
+
highlightingOn = true;
rainbowHighlightingOn = false;
enableEnhancedTyping = true;
- raLspServerPath = RA_LSP_DEBUG || 'ra_lsp_server';
lruCapacity: null | number = null;
displayInlayHints = true;
maxInlayHintLength: null | number = null;
@@ -45,11 +48,72 @@ export class Config {
private prevCargoWatchOptions: null | CargoWatchOptions = null;
constructor(ctx: vscode.ExtensionContext) {
- vscode.workspace.onDidChangeConfiguration(_ => this.refresh(), null, ctx.subscriptions);
- this.refresh();
+ vscode.workspace.onDidChangeConfiguration(_ => this.refresh(ctx), null, ctx.subscriptions);
+ this.refresh(ctx);
}
- private refresh() {
+ private static expandPathResolving(path: string) {
+ if (path.startsWith('~/')) {
+ return path.replace('~', os.homedir());
+ }
+ return path;
+ }
+
+ /**
+ * Name of the binary artifact for `ra_lsp_server` that is published for
+ * `platform` on GitHub releases. (It is also stored under the same name when
+ * downloaded by the extension).
+ */
+ private static prebuiltLangServerFileName(platform: NodeJS.Platform): null | string {
+ switch (platform) {
+ case "linux": return "ra_lsp_server-linux";
+ case "darwin": return "ra_lsp_server-mac";
+ case "win32": return "ra_lsp_server-windows.exe";
+
+ // Users on these platforms yet need to manually build from sources
+ case "aix":
+ case "android":
+ case "freebsd":
+ case "openbsd":
+ case "sunos":
+ case "cygwin":
+ case "netbsd": return null;
+ // The list of platforms is exhaustive (see `NodeJS.Platform` type definition)
+ }
+ }
+
+ private static langServerBinarySource(
+ ctx: vscode.ExtensionContext,
+ config: vscode.WorkspaceConfiguration
+ ): null | BinarySource {
+ const langServerPath = RA_LSP_DEBUG ?? config.get("raLspServerPath");
+
+ if (langServerPath) {
+ return {
+ type: BinarySource.Type.ExplicitPath,
+ path: Config.expandPathResolving(langServerPath)
+ };
+ }
+
+ const prebuiltBinaryName = Config.prebuiltLangServerFileName(process.platform);
+
+ if (!prebuiltBinaryName) return null;
+
+ return {
+ type: BinarySource.Type.GithubRelease,
+ dir: ctx.globalStoragePath,
+ file: prebuiltBinaryName,
+ repo: {
+ name: "rust-analyzer",
+ owner: "rust-analyzer",
+ }
+ };
+ }
+
+
+ // FIXME: revisit the logic for `if (.has(...)) config.get(...)` set default
+ // values only in one place (i.e. remove default values from non-readonly members declarations)
+ private refresh(ctx: vscode.ExtensionContext) {
const config = vscode.workspace.getConfiguration('rust-analyzer');
let requireReloadMessage = null;
@@ -82,10 +146,7 @@ export class Config {
this.prevEnhancedTyping = this.enableEnhancedTyping;
}
- if (config.has('raLspServerPath')) {
- this.raLspServerPath =
- RA_LSP_DEBUG || (config.get('raLspServerPath') as string);
- }
+ this.langServerSource = Config.langServerBinarySource(ctx, config);
if (config.has('cargo-watch.enable')) {
this.cargoWatchOptions.enable = config.get(
diff --git a/editors/code/src/ctx.ts b/editors/code/src/ctx.ts
index aa75943bfdb..70042a479e9 100644
--- a/editors/code/src/ctx.ts
+++ b/editors/code/src/ctx.ts
@@ -11,6 +11,9 @@ export class Ctx {
// deal with it.
//
// Ideally, this should be replaced with async getter though.
+ // FIXME: this actually needs syncronization of some kind (check how
+ // vscode deals with `deactivate()` call when extension has some work scheduled
+ // on the event loop to get a better picture of what we can do here)
client: lc.LanguageClient | null = null;
private extCtx: vscode.ExtensionContext;
private onDidRestartHooks: Array<(client: lc.LanguageClient) => void> = [];
@@ -26,7 +29,14 @@ export class Ctx {
await old.stop();
}
this.client = null;
- const client = createClient(this.config);
+ const client = await createClient(this.config);
+ if (!client) {
+ throw new Error(
+ "Rust Analyzer Language Server is not available. " +
+ "Please, ensure its [proper installation](https://github.com/rust-analyzer/rust-analyzer/tree/master/docs/user#vs-code)."
+ );
+ }
+
this.pushCleanup(client.start());
await client.onReady();
diff --git a/editors/code/src/installation/download_file.ts b/editors/code/src/installation/download_file.ts
new file mode 100644
index 00000000000..b51602ef9fc
--- /dev/null
+++ b/editors/code/src/installation/download_file.ts
@@ -0,0 +1,34 @@
+import fetch from "node-fetch";
+import * as fs from "fs";
+import { strict as assert } from "assert";
+
+/**
+ * Downloads file from `url` and stores it at `destFilePath`.
+ * `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.
+ */
+export async function downloadFile(
+ url: string,
+ destFilePath: fs.PathLike,
+ onProgress: (readBytes: number, totalBytes: number) => void
+): Promise {
+ const response = await fetch(url);
+
+ const totalBytes = Number(response.headers.get('content-length'));
+ assert(!Number.isNaN(totalBytes), "Sanity check of content-length protocol");
+
+ let readBytes = 0;
+
+ console.log("Downloading file of", totalBytes, "bytes size from", url, "to", destFilePath);
+
+ return new Promise((resolve, reject) => response.body
+ .on("data", (chunk: Buffer) => {
+ readBytes += chunk.length;
+ onProgress(readBytes, totalBytes);
+ })
+ .on("end", resolve)
+ .on("error", reject)
+ .pipe(fs.createWriteStream(destFilePath))
+ );
+}
diff --git a/editors/code/src/installation/fetch_latest_artifact_metadata.ts b/editors/code/src/installation/fetch_latest_artifact_metadata.ts
new file mode 100644
index 00000000000..7e370060313
--- /dev/null
+++ b/editors/code/src/installation/fetch_latest_artifact_metadata.ts
@@ -0,0 +1,46 @@
+import fetch from "node-fetch";
+import { GithubRepo, ArtifactMetadata } from "./interfaces";
+
+const GITHUB_API_ENDPOINT_URL = "https://api.github.com";
+
+/**
+ * Fetches the latest release from GitHub `repo` and returns metadata about
+ * `artifactFileName` shipped with this release or `null` if no such artifact was published.
+ */
+export async function fetchLatestArtifactMetadata(
+ repo: GithubRepo, artifactFileName: string
+): Promise {
+
+ const repoOwner = encodeURIComponent(repo.owner);
+ const repoName = encodeURIComponent(repo.name);
+
+ const apiEndpointPath = `/repos/${repoOwner}/${repoName}/releases/latest`;
+ const requestUrl = GITHUB_API_ENDPOINT_URL + apiEndpointPath;
+
+ // We skip runtime type checks for simplicity (here we cast from `any` to `GithubRelease`)
+
+ console.log("Issuing request for released artifacts metadata to", requestUrl);
+
+ const response: GithubRelease = await fetch(requestUrl, {
+ headers: { Accept: "application/vnd.github.v3+json" }
+ })
+ .then(res => res.json());
+
+ const artifact = response.assets.find(artifact => artifact.name === artifactFileName);
+
+ if (!artifact) return null;
+
+ return {
+ releaseName: response.name,
+ downloadUrl: artifact.browser_download_url
+ };
+
+ // We omit declaration of tremendous amount of fields that we are not using here
+ interface GithubRelease {
+ name: string;
+ assets: Array<{
+ name: string;
+ browser_download_url: string;
+ }>;
+ }
+}
diff --git a/editors/code/src/installation/interfaces.ts b/editors/code/src/installation/interfaces.ts
new file mode 100644
index 00000000000..8039d0b90f6
--- /dev/null
+++ b/editors/code/src/installation/interfaces.ts
@@ -0,0 +1,55 @@
+export interface GithubRepo {
+ name: string;
+ owner: string;
+}
+
+/**
+ * Metadata about particular artifact retrieved from GitHub releases.
+ */
+export interface ArtifactMetadata {
+ releaseName: string;
+ downloadUrl: string;
+}
+
+/**
+ * Represents the source of a binary artifact which is either specified by the user
+ * explicitly, or bundled by this extension from GitHub releases.
+ */
+export type BinarySource = BinarySource.ExplicitPath | BinarySource.GithubRelease;
+
+export namespace BinarySource {
+ /**
+ * Type tag for `BinarySource` discriminated union.
+ */
+ export const enum Type { ExplicitPath, GithubRelease }
+
+ export interface ExplicitPath {
+ type: Type.ExplicitPath;
+
+ /**
+ * Filesystem path to the binary specified by the user explicitly.
+ */
+ path: string;
+ }
+
+ export interface GithubRelease {
+ type: Type.GithubRelease;
+
+ /**
+ * Repository where the binary is stored.
+ */
+ repo: GithubRepo;
+
+ /**
+ * Directory on the filesystem where the bundled binary is stored.
+ */
+ dir: string;
+
+ /**
+ * Name of the binary file. It is stored under the same name on GitHub releases
+ * and in local `.dir`.
+ */
+ file: string;
+ }
+
+}
diff --git a/editors/code/src/installation/language_server.ts b/editors/code/src/installation/language_server.ts
new file mode 100644
index 00000000000..1ce67b8b25b
--- /dev/null
+++ b/editors/code/src/installation/language_server.ts
@@ -0,0 +1,141 @@
+import * as vscode from "vscode";
+import * as path from "path";
+import { strict as assert } from "assert";
+import { promises as fs } from "fs";
+import { promises as dns } from "dns";
+import { spawnSync } from "child_process";
+import { throttle } from "throttle-debounce";
+
+import { BinarySource } from "./interfaces";
+import { fetchLatestArtifactMetadata } from "./fetch_latest_artifact_metadata";
+import { downloadFile } from "./download_file";
+
+export async function downloadLatestLanguageServer(
+ {file: artifactFileName, dir: installationDir, repo}: BinarySource.GithubRelease
+) {
+ const { releaseName, downloadUrl } = (await fetchLatestArtifactMetadata(
+ repo, artifactFileName
+ ))!;
+
+ await fs.mkdir(installationDir).catch(err => assert.strictEqual(
+ err?.code,
+ "EEXIST",
+ `Couldn't create directory "${installationDir}" to download `+
+ `language server binary: ${err.message}`
+ ));
+
+ const installationPath = path.join(installationDir, artifactFileName);
+
+ console.time("Downloading ra_lsp_server");
+ await vscode.window.withProgress(
+ {
+ location: vscode.ProgressLocation.Notification,
+ cancellable: false, // FIXME: add support for canceling download?
+ title: `Downloading language server (${releaseName})`
+ },
+ async (progress, _cancellationToken) => {
+ let lastPrecentage = 0;
+ await downloadFile(downloadUrl, installationPath, throttle(
+ 200,
+ /* noTrailing: */ true,
+ (readBytes, totalBytes) => {
+ const newPercentage = (readBytes / totalBytes) * 100;
+ progress.report({
+ message: newPercentage.toFixed(0) + "%",
+ increment: newPercentage - lastPrecentage
+ });
+
+ lastPrecentage = newPercentage;
+ })
+ );
+ }
+ );
+ console.timeEnd("Downloading ra_lsp_server");
+
+ await fs.chmod(installationPath, 0o755); // Set (rwx, r_x, r_x) permissions
+}
+export async function ensureLanguageServerBinary(
+ langServerSource: null | BinarySource
+): Promise {
+
+ if (!langServerSource) {
+ vscode.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 null;
+ }
+
+ switch (langServerSource.type) {
+ case BinarySource.Type.ExplicitPath: {
+ if (isBinaryAvailable(langServerSource.path)) {
+ return langServerSource.path;
+ }
+
+ vscode.window.showErrorMessage(
+ `Unable to run ${langServerSource.path} binary. ` +
+ `To use the pre-built language server, set "rust-analyzer.raLspServerPath" ` +
+ "value to `null` or remove it from the settings to use it by default."
+ );
+ return null;
+ }
+ case BinarySource.Type.GithubRelease: {
+ const prebuiltBinaryPath = path.join(langServerSource.dir, langServerSource.file);
+
+ if (isBinaryAvailable(prebuiltBinaryPath)) {
+ return prebuiltBinaryPath;
+ }
+
+ const userResponse = await vscode.window.showInformationMessage(
+ "Language server binary for rust-analyzer was not found. " +
+ "Do you want to download it now?",
+ "Download now", "Cancel"
+ );
+ if (userResponse !== "Download now") return null;
+
+ try {
+ await downloadLatestLanguageServer(langServerSource);
+ } catch (err) {
+ await vscode.window.showErrorMessage(
+ `Failed to download language server from ${langServerSource.repo.name} ` +
+ `GitHub repository: ${err.message}`
+ );
+
+ await dns.resolve('www.google.com').catch(err => {
+ console.error("DNS resolution failed, there might be an issue with Internet availability");
+ console.error(err);
+ });
+
+ return null;
+ }
+
+ if (!isBinaryAvailable(prebuiltBinaryPath)) assert(false,
+ `Downloaded language server binary is not functional.` +
+ `Downloaded from: ${JSON.stringify(langServerSource)}`
+ );
+
+
+ vscode.window.showInformationMessage(
+ "Rust analyzer language server was successfully installed 🦀"
+ );
+
+ return prebuiltBinaryPath;
+ }
+ }
+
+ function isBinaryAvailable(binaryPath: string) {
+ const res = spawnSync(binaryPath, ["--version"]);
+
+ // ACHTUNG! `res` type declaration is inherently wrong, see
+ // https://github.com/DefinitelyTyped/DefinitelyTyped/issues/42221
+
+ console.log("Checked binary availablity via --version", res);
+ console.log(binaryPath, "--version output:", res.output?.map(String));
+
+ return res.status === 0;
+ }
+}
diff --git a/editors/code/tsconfig.json b/editors/code/tsconfig.json
index e60eb8e5e5b..0c7702974a9 100644
--- a/editors/code/tsconfig.json
+++ b/editors/code/tsconfig.json
@@ -6,6 +6,8 @@
"lib": [
"es2019"
],
+ "esModuleInterop": true,
+ "allowSyntheticDefaultImports": true,
"sourceMap": true,
"rootDir": "src",
"strict": true,