Skip to content

Commit 360890f

Browse files
bors[bot]VeetahaVeetaha
authored
Merge #3053
3053: Feature: downloading lsp server from GitHub r=matklad a=Veetaha This is currently very WIP, I may need to change this and that, add "download language server command", logging stuff (for future bug reports), etc., but it already works. Also didn't test this on windows yet and mac (don't have the latter) The quirks: * Downloaded binary doesn't have executable permissions by default, that's why we ~~`chmod 111`~~ (**[UPD]** `chmod 755` as per @lnicola [suggestion](#3053 (comment))) for it. * To remove installed binary run `rm /${HOME}/.config/Code/User/globalStorage/matklad.rust-analyzer/ra_lsp_server-linux`, ~~note that `-f` flag is necessary, because of `111` permissions (I think this should be changed)~~ (**[UPD]** --force is no longer needed due to 755 permissions). I also tried to keep things simple and not to use too many dependencies, all the ones added have 0 dependencies, (`ts-not-nil` is my personal npm package, that imitates `unwrap()` in TypeScript) **[UPD]** I reduced throttle latency of progress indicator to 200ms for smoother UX // TODO: - [x] ~~Add `Rust Analyzer: Download latest language server` vscode command.~~ **[UPD]**: having reviewed the code and estimated available options I concluded that this feature requires too many code changes, I'd like to extract this into a separate PR after we merge this one. - [x] Add some logging for future debugging - [x] ~~Gracefully handle the case when language server is not available (e.g. no internet connection, user explicitly rejected the download, etc.)~~ **[UPD]** Decided to postpone better implementation of graceful degradation logic as per [conversation](https://rust-lang.zulipchat.com/#narrow/stream/185405-t-compiler.2Fwg-rls-2.2E0/topic/Deployment.20and.20installation/near/187758550). Demo (**[UPD]** this is a bit outdated, but still mainly reflects the feature): ![ra-github-release-download-mvp](https://user-images.githubusercontent.com/36276403/74077961-4f248a80-4a2d-11ea-962f-27c650fd6c4c.gif) Related issue: #2988 #3007 Co-authored-by: Veetaha <[email protected]> Co-authored-by: Veetaha <[email protected]>
2 parents 0db5525 + dfb81a8 commit 360890f

File tree

11 files changed

+429
-30
lines changed

11 files changed

+429
-30
lines changed

docs/user/README.md

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,38 @@ a minimum version of 10 installed. Please refer to
3131
You will also need the most recent version of VS Code: we don't try to
3232
maintain compatibility with older versions yet.
3333

34-
The experimental VS Code plugin can then be built and installed by executing the
34+
### Installation from prebuilt binaries
35+
36+
We ship prebuilt binaries for Linux, Mac and Windows via
37+
[GitHub releases](https://github.com/rust-analyzer/rust-analyzer/releases).
38+
In order to use them you need to install the client VSCode extension.
39+
40+
Publishing to VSCode marketplace is currently WIP. Thus, you need to clone the repository and install **only** the client extension via
41+
```
42+
$ git clone https://github.com/rust-analyzer/rust-analyzer.git --depth 1
43+
$ cd rust-analyzer
44+
$ cargo xtask install --client-code
45+
```
46+
Then open VSCode (or reload the window if it was already running), open some Rust project and you should
47+
see an info message pop-up.
48+
49+
50+
<img height="140px" src="https://user-images.githubusercontent.com/36276403/74103174-a40df100-4b52-11ea-81f4-372c70797924.png" alt="Download now message"/>
51+
52+
53+
Click `Download now`, wait until the progress is 100% and you are ready to go.
54+
55+
For updates you need to remove installed binary
56+
```
57+
rm -rf ${HOME}/.config/Code/User/globalStorage/matklad.rust-analyzer
58+
```
59+
60+
`"Donwload latest language server"` command for VSCode and automatic updates detection is currently WIP.
61+
62+
63+
### Installation from sources
64+
65+
The experimental VS Code plugin can be built and installed by executing the
3566
following commands:
3667

3768
```
@@ -46,6 +77,7 @@ doesn't, report bugs!
4677
**Note** [#1831](https://github.com/rust-analyzer/rust-analyzer/issues/1831): If you are using the popular
4778
[Vim emulation plugin](https://github.com/VSCodeVim/Vim), you will likely
4879
need to turn off the `rust-analyzer.enableEnhancedTyping` setting.
80+
(// 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))
4981

5082
If you have an unusual setup (for example, `code` is not in the `PATH`), you
5183
should adapt these manual installation instructions:

editors/code/package-lock.json

Lines changed: 25 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

editors/code/package.json

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,18 +25,22 @@
2525
},
2626
"dependencies": {
2727
"jsonc-parser": "^2.1.0",
28+
"node-fetch": "^2.6.0",
29+
"throttle-debounce": "^2.1.0",
2830
"vscode-languageclient": "^6.1.0"
2931
},
3032
"devDependencies": {
3133
"@rollup/plugin-commonjs": "^11.0.2",
3234
"@rollup/plugin-node-resolve": "^7.1.1",
3335
"@types/node": "^12.12.25",
36+
"@types/node-fetch": "^2.5.4",
37+
"@types/throttle-debounce": "^2.1.0",
3438
"@types/vscode": "^1.41.0",
3539
"rollup": "^1.31.0",
3640
"tslib": "^1.10.0",
3741
"tslint": "^5.20.1",
38-
"typescript-formatter": "^7.2.2",
3942
"typescript": "^3.7.5",
43+
"typescript-formatter": "^7.2.2",
4044
"vsce": "^1.71.0"
4145
},
4246
"activationEvents": [
@@ -169,10 +173,11 @@
169173
},
170174
"rust-analyzer.raLspServerPath": {
171175
"type": [
176+
"null",
172177
"string"
173178
],
174-
"default": "ra_lsp_server",
175-
"description": "Path to ra_lsp_server executable"
179+
"default": null,
180+
"description": "Path to ra_lsp_server executable (points to bundled binary by default)"
176181
},
177182
"rust-analyzer.excludeGlobs": {
178183
"type": "array",

editors/code/src/client.ts

Lines changed: 5 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,18 @@
1-
import { homedir } from 'os';
21
import * as lc from 'vscode-languageclient';
3-
import { spawnSync } from 'child_process';
42

53
import { window, workspace } from 'vscode';
64
import { Config } from './config';
5+
import { ensureLanguageServerBinary } from './installation/language_server';
76

8-
export function createClient(config: Config): lc.LanguageClient {
7+
export async function createClient(config: Config): Promise<null | lc.LanguageClient> {
98
// '.' Is the fallback if no folder is open
109
// TODO?: Workspace folders support Uri's (eg: file://test.txt).
1110
// It might be a good idea to test if the uri points to a file.
1211
const workspaceFolderPath = workspace.workspaceFolders?.[0]?.uri.fsPath ?? '.';
1312

14-
const raLspServerPath = expandPathResolving(config.raLspServerPath);
15-
if (spawnSync(raLspServerPath, ["--version"]).status !== 0) {
16-
window.showErrorMessage(
17-
`Unable to execute '${raLspServerPath} --version'\n\n` +
18-
`Perhaps it is not in $PATH?\n\n` +
19-
`PATH=${process.env.PATH}\n`
20-
);
21-
}
13+
const raLspServerPath = await ensureLanguageServerBinary(config.langServerSource);
14+
if (!raLspServerPath) return null;
15+
2216
const run: lc.Executable = {
2317
command: raLspServerPath,
2418
options: { cwd: workspaceFolderPath },
@@ -87,9 +81,3 @@ export function createClient(config: Config): lc.LanguageClient {
8781
res.registerProposedFeatures();
8882
return res;
8983
}
90-
function expandPathResolving(path: string) {
91-
if (path.startsWith('~/')) {
92-
return path.replace('~', homedir());
93-
}
94-
return path;
95-
}

editors/code/src/config.ts

Lines changed: 69 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1+
import * as os from "os";
12
import * as vscode from 'vscode';
3+
import { BinarySource } from "./installation/interfaces";
24

35
const RA_LSP_DEBUG = process.env.__RA_LSP_SERVER_DEBUG;
46

@@ -16,10 +18,11 @@ export interface CargoFeatures {
1618
}
1719

1820
export class Config {
21+
langServerSource!: null | BinarySource;
22+
1923
highlightingOn = true;
2024
rainbowHighlightingOn = false;
2125
enableEnhancedTyping = true;
22-
raLspServerPath = RA_LSP_DEBUG || 'ra_lsp_server';
2326
lruCapacity: null | number = null;
2427
displayInlayHints = true;
2528
maxInlayHintLength: null | number = null;
@@ -45,11 +48,72 @@ export class Config {
4548
private prevCargoWatchOptions: null | CargoWatchOptions = null;
4649

4750
constructor(ctx: vscode.ExtensionContext) {
48-
vscode.workspace.onDidChangeConfiguration(_ => this.refresh(), null, ctx.subscriptions);
49-
this.refresh();
51+
vscode.workspace.onDidChangeConfiguration(_ => this.refresh(ctx), null, ctx.subscriptions);
52+
this.refresh(ctx);
53+
}
54+
55+
private static expandPathResolving(path: string) {
56+
if (path.startsWith('~/')) {
57+
return path.replace('~', os.homedir());
58+
}
59+
return path;
60+
}
61+
62+
/**
63+
* Name of the binary artifact for `ra_lsp_server` that is published for
64+
* `platform` on GitHub releases. (It is also stored under the same name when
65+
* downloaded by the extension).
66+
*/
67+
private static prebuiltLangServerFileName(platform: NodeJS.Platform): null | string {
68+
switch (platform) {
69+
case "linux": return "ra_lsp_server-linux";
70+
case "darwin": return "ra_lsp_server-mac";
71+
case "win32": return "ra_lsp_server-windows.exe";
72+
73+
// Users on these platforms yet need to manually build from sources
74+
case "aix":
75+
case "android":
76+
case "freebsd":
77+
case "openbsd":
78+
case "sunos":
79+
case "cygwin":
80+
case "netbsd": return null;
81+
// The list of platforms is exhaustive (see `NodeJS.Platform` type definition)
82+
}
83+
}
84+
85+
private static langServerBinarySource(
86+
ctx: vscode.ExtensionContext,
87+
config: vscode.WorkspaceConfiguration
88+
): null | BinarySource {
89+
const langServerPath = RA_LSP_DEBUG ?? config.get<null | string>("raLspServerPath");
90+
91+
if (langServerPath) {
92+
return {
93+
type: BinarySource.Type.ExplicitPath,
94+
path: Config.expandPathResolving(langServerPath)
95+
};
96+
}
97+
98+
const prebuiltBinaryName = Config.prebuiltLangServerFileName(process.platform);
99+
100+
if (!prebuiltBinaryName) return null;
101+
102+
return {
103+
type: BinarySource.Type.GithubRelease,
104+
dir: ctx.globalStoragePath,
105+
file: prebuiltBinaryName,
106+
repo: {
107+
name: "rust-analyzer",
108+
owner: "rust-analyzer",
109+
}
110+
};
50111
}
51112

52-
private refresh() {
113+
114+
// FIXME: revisit the logic for `if (.has(...)) config.get(...)` set default
115+
// values only in one place (i.e. remove default values from non-readonly members declarations)
116+
private refresh(ctx: vscode.ExtensionContext) {
53117
const config = vscode.workspace.getConfiguration('rust-analyzer');
54118

55119
let requireReloadMessage = null;
@@ -82,10 +146,7 @@ export class Config {
82146
this.prevEnhancedTyping = this.enableEnhancedTyping;
83147
}
84148

85-
if (config.has('raLspServerPath')) {
86-
this.raLspServerPath =
87-
RA_LSP_DEBUG || (config.get('raLspServerPath') as string);
88-
}
149+
this.langServerSource = Config.langServerBinarySource(ctx, config);
89150

90151
if (config.has('cargo-watch.enable')) {
91152
this.cargoWatchOptions.enable = config.get<boolean>(

editors/code/src/ctx.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,9 @@ export class Ctx {
1111
// deal with it.
1212
//
1313
// Ideally, this should be replaced with async getter though.
14+
// FIXME: this actually needs syncronization of some kind (check how
15+
// vscode deals with `deactivate()` call when extension has some work scheduled
16+
// on the event loop to get a better picture of what we can do here)
1417
client: lc.LanguageClient | null = null;
1518
private extCtx: vscode.ExtensionContext;
1619
private onDidRestartHooks: Array<(client: lc.LanguageClient) => void> = [];
@@ -26,7 +29,14 @@ export class Ctx {
2629
await old.stop();
2730
}
2831
this.client = null;
29-
const client = createClient(this.config);
32+
const client = await createClient(this.config);
33+
if (!client) {
34+
throw new Error(
35+
"Rust Analyzer Language Server is not available. " +
36+
"Please, ensure its [proper installation](https://github.com/rust-analyzer/rust-analyzer/tree/master/docs/user#vs-code)."
37+
);
38+
}
39+
3040
this.pushCleanup(client.start());
3141
await client.onReady();
3242

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import fetch from "node-fetch";
2+
import * as fs from "fs";
3+
import { strict as assert } from "assert";
4+
5+
/**
6+
* Downloads file from `url` and stores it at `destFilePath`.
7+
* `onProgress` callback is called on recieveing each chunk of bytes
8+
* to track the progress of downloading, it gets the already read and total
9+
* amount of bytes to read as its parameters.
10+
*/
11+
export async function downloadFile(
12+
url: string,
13+
destFilePath: fs.PathLike,
14+
onProgress: (readBytes: number, totalBytes: number) => void
15+
): Promise<void> {
16+
const response = await fetch(url);
17+
18+
const totalBytes = Number(response.headers.get('content-length'));
19+
assert(!Number.isNaN(totalBytes), "Sanity check of content-length protocol");
20+
21+
let readBytes = 0;
22+
23+
console.log("Downloading file of", totalBytes, "bytes size from", url, "to", destFilePath);
24+
25+
return new Promise<void>((resolve, reject) => response.body
26+
.on("data", (chunk: Buffer) => {
27+
readBytes += chunk.length;
28+
onProgress(readBytes, totalBytes);
29+
})
30+
.on("end", resolve)
31+
.on("error", reject)
32+
.pipe(fs.createWriteStream(destFilePath))
33+
);
34+
}
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import fetch from "node-fetch";
2+
import { GithubRepo, ArtifactMetadata } from "./interfaces";
3+
4+
const GITHUB_API_ENDPOINT_URL = "https://api.github.com";
5+
6+
/**
7+
* Fetches the latest release from GitHub `repo` and returns metadata about
8+
* `artifactFileName` shipped with this release or `null` if no such artifact was published.
9+
*/
10+
export async function fetchLatestArtifactMetadata(
11+
repo: GithubRepo, artifactFileName: string
12+
): Promise<null | ArtifactMetadata> {
13+
14+
const repoOwner = encodeURIComponent(repo.owner);
15+
const repoName = encodeURIComponent(repo.name);
16+
17+
const apiEndpointPath = `/repos/${repoOwner}/${repoName}/releases/latest`;
18+
const requestUrl = GITHUB_API_ENDPOINT_URL + apiEndpointPath;
19+
20+
// We skip runtime type checks for simplicity (here we cast from `any` to `GithubRelease`)
21+
22+
console.log("Issuing request for released artifacts metadata to", requestUrl);
23+
24+
const response: GithubRelease = await fetch(requestUrl, {
25+
headers: { Accept: "application/vnd.github.v3+json" }
26+
})
27+
.then(res => res.json());
28+
29+
const artifact = response.assets.find(artifact => artifact.name === artifactFileName);
30+
31+
if (!artifact) return null;
32+
33+
return {
34+
releaseName: response.name,
35+
downloadUrl: artifact.browser_download_url
36+
};
37+
38+
// We omit declaration of tremendous amount of fields that we are not using here
39+
interface GithubRelease {
40+
name: string;
41+
assets: Array<{
42+
name: string;
43+
browser_download_url: string;
44+
}>;
45+
}
46+
}

0 commit comments

Comments
 (0)