diff options
author | bors[bot] <26634292+bors[bot]@users.noreply.github.com> | 2020-02-09 15:21:12 +0000 |
---|---|---|
committer | GitHub <[email protected]> | 2020-02-09 15:21:12 +0000 |
commit | 360890fcec3af854c4848ba7ed3511b4bae2ff5e (patch) | |
tree | 5820313364f04233fe6a36794bc370ff25407cc5 /editors | |
parent | 0db5525c445fb86a7fb7441267ffab2604d78a41 (diff) | |
parent | dfb81a8cd4b9a2efd8151b4ac36105c51df7d683 (diff) |
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](https://github.com/rust-analyzer/rust-analyzer/pull/3053#discussion_r376694456)) 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]>
Diffstat (limited to 'editors')
-rw-r--r-- | editors/code/package-lock.json | 25 | ||||
-rw-r--r-- | editors/code/package.json | 11 | ||||
-rw-r--r-- | editors/code/src/client.ts | 22 | ||||
-rw-r--r-- | editors/code/src/config.ts | 77 | ||||
-rw-r--r-- | editors/code/src/ctx.ts | 12 | ||||
-rw-r--r-- | editors/code/src/installation/download_file.ts | 34 | ||||
-rw-r--r-- | editors/code/src/installation/fetch_latest_artifact_metadata.ts | 46 | ||||
-rw-r--r-- | editors/code/src/installation/interfaces.ts | 55 | ||||
-rw-r--r-- | editors/code/src/installation/language_server.ts | 141 | ||||
-rw-r--r-- | editors/code/tsconfig.json | 2 |
10 files changed, 396 insertions, 29 deletions
diff --git a/editors/code/package-lock.json b/editors/code/package-lock.json index 353af06bf..5c056463e 100644 --- a/editors/code/package-lock.json +++ b/editors/code/package-lock.json | |||
@@ -82,6 +82,15 @@ | |||
82 | "integrity": "sha512-nf1LMGZvgFX186geVZR1xMZKKblJiRfiASTHw85zED2kI1yDKHDwTKMdkaCbTlXoRKlGKaDfYywt+V0As30q3w==", | 82 | "integrity": "sha512-nf1LMGZvgFX186geVZR1xMZKKblJiRfiASTHw85zED2kI1yDKHDwTKMdkaCbTlXoRKlGKaDfYywt+V0As30q3w==", |
83 | "dev": true | 83 | "dev": true |
84 | }, | 84 | }, |
85 | "@types/node-fetch": { | ||
86 | "version": "2.5.4", | ||
87 | "resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.5.4.tgz", | ||
88 | "integrity": "sha512-Oz6id++2qAOFuOlE1j0ouk1dzl3mmI1+qINPNBhi9nt/gVOz0G+13Ao6qjhdF0Ys+eOkhu6JnFmt38bR3H0POQ==", | ||
89 | "dev": true, | ||
90 | "requires": { | ||
91 | "@types/node": "*" | ||
92 | } | ||
93 | }, | ||
85 | "@types/resolve": { | 94 | "@types/resolve": { |
86 | "version": "0.0.8", | 95 | "version": "0.0.8", |
87 | "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-0.0.8.tgz", | 96 | "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-0.0.8.tgz", |
@@ -91,6 +100,12 @@ | |||
91 | "@types/node": "*" | 100 | "@types/node": "*" |
92 | } | 101 | } |
93 | }, | 102 | }, |
103 | "@types/throttle-debounce": { | ||
104 | "version": "2.1.0", | ||
105 | "resolved": "https://registry.npmjs.org/@types/throttle-debounce/-/throttle-debounce-2.1.0.tgz", | ||
106 | "integrity": "sha512-5eQEtSCoESnh2FsiLTxE121IiE60hnMqcb435fShf4bpLRjEu1Eoekht23y6zXS9Ts3l+Szu3TARnTsA0GkOkQ==", | ||
107 | "dev": true | ||
108 | }, | ||
94 | "@types/vscode": { | 109 | "@types/vscode": { |
95 | "version": "1.41.0", | 110 | "version": "1.41.0", |
96 | "resolved": "https://registry.npmjs.org/@types/vscode/-/vscode-1.41.0.tgz", | 111 | "resolved": "https://registry.npmjs.org/@types/vscode/-/vscode-1.41.0.tgz", |
@@ -536,6 +551,11 @@ | |||
536 | "integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==", | 551 | "integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==", |
537 | "dev": true | 552 | "dev": true |
538 | }, | 553 | }, |
554 | "node-fetch": { | ||
555 | "version": "2.6.0", | ||
556 | "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.0.tgz", | ||
557 | "integrity": "sha512-8dG4H5ujfvFiqDmVu9fQ5bOHUC15JMjMY/Zumv26oOvvVJjM67KF8koCWIabKQ1GJIa9r2mMZscBq/TbdOcmNA==" | ||
558 | }, | ||
539 | "nth-check": { | 559 | "nth-check": { |
540 | "version": "1.0.2", | 560 | "version": "1.0.2", |
541 | "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-1.0.2.tgz", | 561 | "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-1.0.2.tgz", |
@@ -719,6 +739,11 @@ | |||
719 | "has-flag": "^3.0.0" | 739 | "has-flag": "^3.0.0" |
720 | } | 740 | } |
721 | }, | 741 | }, |
742 | "throttle-debounce": { | ||
743 | "version": "2.1.0", | ||
744 | "resolved": "https://registry.npmjs.org/throttle-debounce/-/throttle-debounce-2.1.0.tgz", | ||
745 | "integrity": "sha512-AOvyNahXQuU7NN+VVvOOX+uW6FPaWdAOdRP5HfwYxAfCzXTFKRMoIMk+n+po318+ktcChx+F1Dd91G3YHeMKyg==" | ||
746 | }, | ||
722 | "tmp": { | 747 | "tmp": { |
723 | "version": "0.0.29", | 748 | "version": "0.0.29", |
724 | "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.29.tgz", | 749 | "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.29.tgz", |
diff --git a/editors/code/package.json b/editors/code/package.json index 11d37053e..f687eb8d4 100644 --- a/editors/code/package.json +++ b/editors/code/package.json | |||
@@ -25,18 +25,22 @@ | |||
25 | }, | 25 | }, |
26 | "dependencies": { | 26 | "dependencies": { |
27 | "jsonc-parser": "^2.1.0", | 27 | "jsonc-parser": "^2.1.0", |
28 | "node-fetch": "^2.6.0", | ||
29 | "throttle-debounce": "^2.1.0", | ||
28 | "vscode-languageclient": "^6.1.0" | 30 | "vscode-languageclient": "^6.1.0" |
29 | }, | 31 | }, |
30 | "devDependencies": { | 32 | "devDependencies": { |
31 | "@rollup/plugin-commonjs": "^11.0.2", | 33 | "@rollup/plugin-commonjs": "^11.0.2", |
32 | "@rollup/plugin-node-resolve": "^7.1.1", | 34 | "@rollup/plugin-node-resolve": "^7.1.1", |
33 | "@types/node": "^12.12.25", | 35 | "@types/node": "^12.12.25", |
36 | "@types/node-fetch": "^2.5.4", | ||
37 | "@types/throttle-debounce": "^2.1.0", | ||
34 | "@types/vscode": "^1.41.0", | 38 | "@types/vscode": "^1.41.0", |
35 | "rollup": "^1.31.0", | 39 | "rollup": "^1.31.0", |
36 | "tslib": "^1.10.0", | 40 | "tslib": "^1.10.0", |
37 | "tslint": "^5.20.1", | 41 | "tslint": "^5.20.1", |
38 | "typescript-formatter": "^7.2.2", | ||
39 | "typescript": "^3.7.5", | 42 | "typescript": "^3.7.5", |
43 | "typescript-formatter": "^7.2.2", | ||
40 | "vsce": "^1.71.0" | 44 | "vsce": "^1.71.0" |
41 | }, | 45 | }, |
42 | "activationEvents": [ | 46 | "activationEvents": [ |
@@ -169,10 +173,11 @@ | |||
169 | }, | 173 | }, |
170 | "rust-analyzer.raLspServerPath": { | 174 | "rust-analyzer.raLspServerPath": { |
171 | "type": [ | 175 | "type": [ |
176 | "null", | ||
172 | "string" | 177 | "string" |
173 | ], | 178 | ], |
174 | "default": "ra_lsp_server", | 179 | "default": null, |
175 | "description": "Path to ra_lsp_server executable" | 180 | "description": "Path to ra_lsp_server executable (points to bundled binary by default)" |
176 | }, | 181 | }, |
177 | "rust-analyzer.excludeGlobs": { | 182 | "rust-analyzer.excludeGlobs": { |
178 | "type": "array", | 183 | "type": "array", |
diff --git a/editors/code/src/client.ts b/editors/code/src/client.ts index 7e7e909dd..2e3d4aba2 100644 --- a/editors/code/src/client.ts +++ b/editors/code/src/client.ts | |||
@@ -1,24 +1,18 @@ | |||
1 | import { homedir } from 'os'; | ||
2 | import * as lc from 'vscode-languageclient'; | 1 | import * as lc from 'vscode-languageclient'; |
3 | import { spawnSync } from 'child_process'; | ||
4 | 2 | ||
5 | import { window, workspace } from 'vscode'; | 3 | import { window, workspace } from 'vscode'; |
6 | import { Config } from './config'; | 4 | import { Config } from './config'; |
5 | import { ensureLanguageServerBinary } from './installation/language_server'; | ||
7 | 6 | ||
8 | export function createClient(config: Config): lc.LanguageClient { | 7 | export async function createClient(config: Config): Promise<null | lc.LanguageClient> { |
9 | // '.' Is the fallback if no folder is open | 8 | // '.' Is the fallback if no folder is open |
10 | // TODO?: Workspace folders support Uri's (eg: file://test.txt). | 9 | // TODO?: Workspace folders support Uri's (eg: file://test.txt). |
11 | // It might be a good idea to test if the uri points to a file. | 10 | // It might be a good idea to test if the uri points to a file. |
12 | const workspaceFolderPath = workspace.workspaceFolders?.[0]?.uri.fsPath ?? '.'; | 11 | const workspaceFolderPath = workspace.workspaceFolders?.[0]?.uri.fsPath ?? '.'; |
13 | 12 | ||
14 | const raLspServerPath = expandPathResolving(config.raLspServerPath); | 13 | const raLspServerPath = await ensureLanguageServerBinary(config.langServerSource); |
15 | if (spawnSync(raLspServerPath, ["--version"]).status !== 0) { | 14 | if (!raLspServerPath) return null; |
16 | window.showErrorMessage( | 15 | |
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 | } | ||
22 | const run: lc.Executable = { | 16 | const run: lc.Executable = { |
23 | command: raLspServerPath, | 17 | command: raLspServerPath, |
24 | options: { cwd: workspaceFolderPath }, | 18 | options: { cwd: workspaceFolderPath }, |
@@ -87,9 +81,3 @@ export function createClient(config: Config): lc.LanguageClient { | |||
87 | res.registerProposedFeatures(); | 81 | res.registerProposedFeatures(); |
88 | return res; | 82 | return res; |
89 | } | 83 | } |
90 | function expandPathResolving(path: string) { | ||
91 | if (path.startsWith('~/')) { | ||
92 | return path.replace('~', homedir()); | ||
93 | } | ||
94 | return path; | ||
95 | } | ||
diff --git a/editors/code/src/config.ts b/editors/code/src/config.ts index 524620433..d5f3da2ed 100644 --- a/editors/code/src/config.ts +++ b/editors/code/src/config.ts | |||
@@ -1,4 +1,6 @@ | |||
1 | import * as os from "os"; | ||
1 | import * as vscode from 'vscode'; | 2 | import * as vscode from 'vscode'; |
3 | import { BinarySource } from "./installation/interfaces"; | ||
2 | 4 | ||
3 | const RA_LSP_DEBUG = process.env.__RA_LSP_SERVER_DEBUG; | 5 | const RA_LSP_DEBUG = process.env.__RA_LSP_SERVER_DEBUG; |
4 | 6 | ||
@@ -16,10 +18,11 @@ export interface CargoFeatures { | |||
16 | } | 18 | } |
17 | 19 | ||
18 | export class Config { | 20 | export class Config { |
21 | langServerSource!: null | BinarySource; | ||
22 | |||
19 | highlightingOn = true; | 23 | highlightingOn = true; |
20 | rainbowHighlightingOn = false; | 24 | rainbowHighlightingOn = false; |
21 | enableEnhancedTyping = true; | 25 | enableEnhancedTyping = true; |
22 | raLspServerPath = RA_LSP_DEBUG || 'ra_lsp_server'; | ||
23 | lruCapacity: null | number = null; | 26 | lruCapacity: null | number = null; |
24 | displayInlayHints = true; | 27 | displayInlayHints = true; |
25 | maxInlayHintLength: null | number = null; | 28 | maxInlayHintLength: null | number = null; |
@@ -45,11 +48,72 @@ export class Config { | |||
45 | private prevCargoWatchOptions: null | CargoWatchOptions = null; | 48 | private prevCargoWatchOptions: null | CargoWatchOptions = null; |
46 | 49 | ||
47 | constructor(ctx: vscode.ExtensionContext) { | 50 | constructor(ctx: vscode.ExtensionContext) { |
48 | vscode.workspace.onDidChangeConfiguration(_ => this.refresh(), null, ctx.subscriptions); | 51 | vscode.workspace.onDidChangeConfiguration(_ => this.refresh(ctx), null, ctx.subscriptions); |
49 | this.refresh(); | 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 | }; | ||
50 | } | 111 | } |
51 | 112 | ||
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) { | ||
53 | const config = vscode.workspace.getConfiguration('rust-analyzer'); | 117 | const config = vscode.workspace.getConfiguration('rust-analyzer'); |
54 | 118 | ||
55 | let requireReloadMessage = null; | 119 | let requireReloadMessage = null; |
@@ -82,10 +146,7 @@ export class Config { | |||
82 | this.prevEnhancedTyping = this.enableEnhancedTyping; | 146 | this.prevEnhancedTyping = this.enableEnhancedTyping; |
83 | } | 147 | } |
84 | 148 | ||
85 | if (config.has('raLspServerPath')) { | 149 | this.langServerSource = Config.langServerBinarySource(ctx, config); |
86 | this.raLspServerPath = | ||
87 | RA_LSP_DEBUG || (config.get('raLspServerPath') as string); | ||
88 | } | ||
89 | 150 | ||
90 | if (config.has('cargo-watch.enable')) { | 151 | if (config.has('cargo-watch.enable')) { |
91 | this.cargoWatchOptions.enable = config.get<boolean>( | 152 | this.cargoWatchOptions.enable = config.get<boolean>( |
diff --git a/editors/code/src/ctx.ts b/editors/code/src/ctx.ts index aa75943bf..70042a479 100644 --- a/editors/code/src/ctx.ts +++ b/editors/code/src/ctx.ts | |||
@@ -11,6 +11,9 @@ export class Ctx { | |||
11 | // deal with it. | 11 | // deal with it. |
12 | // | 12 | // |
13 | // Ideally, this should be replaced with async getter though. | 13 | // 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) | ||
14 | client: lc.LanguageClient | null = null; | 17 | client: lc.LanguageClient | null = null; |
15 | private extCtx: vscode.ExtensionContext; | 18 | private extCtx: vscode.ExtensionContext; |
16 | private onDidRestartHooks: Array<(client: lc.LanguageClient) => void> = []; | 19 | private onDidRestartHooks: Array<(client: lc.LanguageClient) => void> = []; |
@@ -26,7 +29,14 @@ export class Ctx { | |||
26 | await old.stop(); | 29 | await old.stop(); |
27 | } | 30 | } |
28 | this.client = null; | 31 | 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 | |||
30 | this.pushCleanup(client.start()); | 40 | this.pushCleanup(client.start()); |
31 | await client.onReady(); | 41 | await client.onReady(); |
32 | 42 | ||
diff --git a/editors/code/src/installation/download_file.ts b/editors/code/src/installation/download_file.ts new file mode 100644 index 000000000..b51602ef9 --- /dev/null +++ b/editors/code/src/installation/download_file.ts | |||
@@ -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 | } | ||
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 000000000..7e3700603 --- /dev/null +++ b/editors/code/src/installation/fetch_latest_artifact_metadata.ts | |||
@@ -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 | } | ||
diff --git a/editors/code/src/installation/interfaces.ts b/editors/code/src/installation/interfaces.ts new file mode 100644 index 000000000..8039d0b90 --- /dev/null +++ b/editors/code/src/installation/interfaces.ts | |||
@@ -0,0 +1,55 @@ | |||
1 | export interface GithubRepo { | ||
2 | name: string; | ||
3 | owner: string; | ||
4 | } | ||
5 | |||
6 | /** | ||
7 | * Metadata about particular artifact retrieved from GitHub releases. | ||
8 | */ | ||
9 | export interface ArtifactMetadata { | ||
10 | releaseName: string; | ||
11 | downloadUrl: string; | ||
12 | } | ||
13 | |||
14 | /** | ||
15 | * Represents the source of a binary artifact which is either specified by the user | ||
16 | * explicitly, or bundled by this extension from GitHub releases. | ||
17 | */ | ||
18 | export type BinarySource = BinarySource.ExplicitPath | BinarySource.GithubRelease; | ||
19 | |||
20 | export namespace BinarySource { | ||
21 | /** | ||
22 | * Type tag for `BinarySource` discriminated union. | ||
23 | */ | ||
24 | export const enum Type { ExplicitPath, GithubRelease } | ||
25 | |||
26 | export interface ExplicitPath { | ||
27 | type: Type.ExplicitPath; | ||
28 | |||
29 | /** | ||
30 | * Filesystem path to the binary specified by the user explicitly. | ||
31 | */ | ||
32 | path: string; | ||
33 | } | ||
34 | |||
35 | export interface GithubRelease { | ||
36 | type: Type.GithubRelease; | ||
37 | |||
38 | /** | ||
39 | * Repository where the binary is stored. | ||
40 | */ | ||
41 | repo: GithubRepo; | ||
42 | |||
43 | /** | ||
44 | * Directory on the filesystem where the bundled binary is stored. | ||
45 | */ | ||
46 | dir: string; | ||
47 | |||
48 | /** | ||
49 | * Name of the binary file. It is stored under the same name on GitHub releases | ||
50 | * and in local `.dir`. | ||
51 | */ | ||
52 | file: string; | ||
53 | } | ||
54 | |||
55 | } | ||
diff --git a/editors/code/src/installation/language_server.ts b/editors/code/src/installation/language_server.ts new file mode 100644 index 000000000..1ce67b8b2 --- /dev/null +++ b/editors/code/src/installation/language_server.ts | |||
@@ -0,0 +1,141 @@ | |||
1 | import * as vscode from "vscode"; | ||
2 | import * as path from "path"; | ||
3 | import { strict as assert } from "assert"; | ||
4 | import { promises as fs } from "fs"; | ||
5 | import { promises as dns } from "dns"; | ||
6 | import { spawnSync } from "child_process"; | ||
7 | import { throttle } from "throttle-debounce"; | ||
8 | |||
9 | import { BinarySource } from "./interfaces"; | ||
10 | import { fetchLatestArtifactMetadata } from "./fetch_latest_artifact_metadata"; | ||
11 | import { downloadFile } from "./download_file"; | ||
12 | |||
13 | export async function downloadLatestLanguageServer( | ||
14 | {file: artifactFileName, dir: installationDir, repo}: BinarySource.GithubRelease | ||
15 | ) { | ||
16 | const { releaseName, downloadUrl } = (await fetchLatestArtifactMetadata( | ||
17 | repo, artifactFileName | ||
18 | ))!; | ||
19 | |||
20 | await fs.mkdir(installationDir).catch(err => assert.strictEqual( | ||
21 | err?.code, | ||
22 | "EEXIST", | ||
23 | `Couldn't create directory "${installationDir}" to download `+ | ||
24 | `language server binary: ${err.message}` | ||
25 | )); | ||
26 | |||
27 | const installationPath = path.join(installationDir, artifactFileName); | ||
28 | |||
29 | console.time("Downloading ra_lsp_server"); | ||
30 | await vscode.window.withProgress( | ||
31 | { | ||
32 | location: vscode.ProgressLocation.Notification, | ||
33 | cancellable: false, // FIXME: add support for canceling download? | ||
34 | title: `Downloading language server (${releaseName})` | ||
35 | }, | ||
36 | async (progress, _cancellationToken) => { | ||
37 | let lastPrecentage = 0; | ||
38 | await downloadFile(downloadUrl, installationPath, throttle( | ||
39 | 200, | ||
40 | /* noTrailing: */ true, | ||
41 | (readBytes, totalBytes) => { | ||
42 | const newPercentage = (readBytes / totalBytes) * 100; | ||
43 | progress.report({ | ||
44 | message: newPercentage.toFixed(0) + "%", | ||
45 | increment: newPercentage - lastPrecentage | ||
46 | }); | ||
47 | |||
48 | lastPrecentage = newPercentage; | ||
49 | }) | ||
50 | ); | ||
51 | } | ||
52 | ); | ||
53 | console.timeEnd("Downloading ra_lsp_server"); | ||
54 | |||
55 | await fs.chmod(installationPath, 0o755); // Set (rwx, r_x, r_x) permissions | ||
56 | } | ||
57 | export async function ensureLanguageServerBinary( | ||
58 | langServerSource: null | BinarySource | ||
59 | ): Promise<null | string> { | ||
60 | |||
61 | if (!langServerSource) { | ||
62 | vscode.window.showErrorMessage( | ||
63 | "Unfortunately we don't ship binaries for your platform yet. " + | ||
64 | "You need to manually clone rust-analyzer repository and " + | ||
65 | "run `cargo xtask install --server` to build the language server from sources. " + | ||
66 | "If you feel that your platform should be supported, please create an issue " + | ||
67 | "about that [here](https://github.com/rust-analyzer/rust-analyzer/issues) and we " + | ||
68 | "will consider it." | ||
69 | ); | ||
70 | return null; | ||
71 | } | ||
72 | |||
73 | switch (langServerSource.type) { | ||
74 | case BinarySource.Type.ExplicitPath: { | ||
75 | if (isBinaryAvailable(langServerSource.path)) { | ||
76 | return langServerSource.path; | ||
77 | } | ||
78 | |||
79 | vscode.window.showErrorMessage( | ||
80 | `Unable to run ${langServerSource.path} binary. ` + | ||
81 | `To use the pre-built language server, set "rust-analyzer.raLspServerPath" ` + | ||
82 | "value to `null` or remove it from the settings to use it by default." | ||
83 | ); | ||
84 | return null; | ||
85 | } | ||
86 | case BinarySource.Type.GithubRelease: { | ||
87 | const prebuiltBinaryPath = path.join(langServerSource.dir, langServerSource.file); | ||
88 | |||
89 | if (isBinaryAvailable(prebuiltBinaryPath)) { | ||
90 | return prebuiltBinaryPath; | ||
91 | } | ||
92 | |||
93 | const userResponse = await vscode.window.showInformationMessage( | ||
94 | "Language server binary for rust-analyzer was not found. " + | ||
95 | "Do you want to download it now?", | ||
96 | "Download now", "Cancel" | ||
97 | ); | ||
98 | if (userResponse !== "Download now") return null; | ||
99 | |||
100 | try { | ||
101 | await downloadLatestLanguageServer(langServerSource); | ||
102 | } catch (err) { | ||
103 | await vscode.window.showErrorMessage( | ||
104 | `Failed to download language server from ${langServerSource.repo.name} ` + | ||
105 | `GitHub repository: ${err.message}` | ||
106 | ); | ||
107 | |||
108 | await dns.resolve('www.google.com').catch(err => { | ||
109 | console.error("DNS resolution failed, there might be an issue with Internet availability"); | ||
110 | console.error(err); | ||
111 | }); | ||
112 | |||
113 | return null; | ||
114 | } | ||
115 | |||
116 | if (!isBinaryAvailable(prebuiltBinaryPath)) assert(false, | ||
117 | `Downloaded language server binary is not functional.` + | ||
118 | `Downloaded from: ${JSON.stringify(langServerSource)}` | ||
119 | ); | ||
120 | |||
121 | |||
122 | vscode.window.showInformationMessage( | ||
123 | "Rust analyzer language server was successfully installed 🦀" | ||
124 | ); | ||
125 | |||
126 | return prebuiltBinaryPath; | ||
127 | } | ||
128 | } | ||
129 | |||
130 | function isBinaryAvailable(binaryPath: string) { | ||
131 | const res = spawnSync(binaryPath, ["--version"]); | ||
132 | |||
133 | // ACHTUNG! `res` type declaration is inherently wrong, see | ||
134 | // https://github.com/DefinitelyTyped/DefinitelyTyped/issues/42221 | ||
135 | |||
136 | console.log("Checked binary availablity via --version", res); | ||
137 | console.log(binaryPath, "--version output:", res.output?.map(String)); | ||
138 | |||
139 | return res.status === 0; | ||
140 | } | ||
141 | } | ||
diff --git a/editors/code/tsconfig.json b/editors/code/tsconfig.json index e60eb8e5e..0c7702974 100644 --- a/editors/code/tsconfig.json +++ b/editors/code/tsconfig.json | |||
@@ -6,6 +6,8 @@ | |||
6 | "lib": [ | 6 | "lib": [ |
7 | "es2019" | 7 | "es2019" |
8 | ], | 8 | ], |
9 | "esModuleInterop": true, | ||
10 | "allowSyntheticDefaultImports": true, | ||
9 | "sourceMap": true, | 11 | "sourceMap": true, |
10 | "rootDir": "src", | 12 | "rootDir": "src", |
11 | "strict": true, | 13 | "strict": true, |