diff options
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 | 94 | ||||
-rw-r--r-- | editors/code/src/ctx.ts | 12 | ||||
-rw-r--r-- | editors/code/src/installation/download_file.ts | 44 | ||||
-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 | 146 | ||||
-rw-r--r-- | editors/code/tsconfig.json | 2 |
10 files changed, 428 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..418845436 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,89 @@ 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; | ||
50 | } | 60 | } |
51 | 61 | ||
52 | private refresh() { | 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( | ||
68 | platform: NodeJS.Platform, | ||
69 | arch: string | ||
70 | ): null | string { | ||
71 | // See possible `arch` values here: | ||
72 | // https://nodejs.org/api/process.html#process_process_arch | ||
73 | |||
74 | switch (platform) { | ||
75 | |||
76 | case "linux": { | ||
77 | switch (arch) { | ||
78 | case "arm": | ||
79 | case "arm64": return null; | ||
80 | |||
81 | default: return "ra_lsp_server-linux"; | ||
82 | } | ||
83 | } | ||
84 | |||
85 | case "darwin": return "ra_lsp_server-mac"; | ||
86 | case "win32": return "ra_lsp_server-windows.exe"; | ||
87 | |||
88 | // Users on these platforms yet need to manually build from sources | ||
89 | case "aix": | ||
90 | case "android": | ||
91 | case "freebsd": | ||
92 | case "openbsd": | ||
93 | case "sunos": | ||
94 | case "cygwin": | ||
95 | case "netbsd": return null; | ||
96 | // The list of platforms is exhaustive (see `NodeJS.Platform` type definition) | ||
97 | } | ||
98 | } | ||
99 | |||
100 | private static langServerBinarySource( | ||
101 | ctx: vscode.ExtensionContext, | ||
102 | config: vscode.WorkspaceConfiguration | ||
103 | ): null | BinarySource { | ||
104 | const langServerPath = RA_LSP_DEBUG ?? config.get<null | string>("raLspServerPath"); | ||
105 | |||
106 | if (langServerPath) { | ||
107 | return { | ||
108 | type: BinarySource.Type.ExplicitPath, | ||
109 | path: Config.expandPathResolving(langServerPath) | ||
110 | }; | ||
111 | } | ||
112 | |||
113 | const prebuiltBinaryName = Config.prebuiltLangServerFileName( | ||
114 | process.platform, process.arch | ||
115 | ); | ||
116 | |||
117 | if (!prebuiltBinaryName) return null; | ||
118 | |||
119 | return { | ||
120 | type: BinarySource.Type.GithubRelease, | ||
121 | dir: ctx.globalStoragePath, | ||
122 | file: prebuiltBinaryName, | ||
123 | repo: { | ||
124 | name: "rust-analyzer", | ||
125 | owner: "rust-analyzer", | ||
126 | } | ||
127 | }; | ||
128 | } | ||
129 | |||
130 | |||
131 | // FIXME: revisit the logic for `if (.has(...)) config.get(...)` set default | ||
132 | // values only in one place (i.e. remove default values from non-readonly members declarations) | ||
133 | private refresh(ctx: vscode.ExtensionContext) { | ||
53 | const config = vscode.workspace.getConfiguration('rust-analyzer'); | 134 | const config = vscode.workspace.getConfiguration('rust-analyzer'); |
54 | 135 | ||
55 | let requireReloadMessage = null; | 136 | let requireReloadMessage = null; |
@@ -82,10 +163,7 @@ export class Config { | |||
82 | this.prevEnhancedTyping = this.enableEnhancedTyping; | 163 | this.prevEnhancedTyping = this.enableEnhancedTyping; |
83 | } | 164 | } |
84 | 165 | ||
85 | if (config.has('raLspServerPath')) { | 166 | this.langServerSource = Config.langServerBinarySource(ctx, config); |
86 | this.raLspServerPath = | ||
87 | RA_LSP_DEBUG || (config.get('raLspServerPath') as string); | ||
88 | } | ||
89 | 167 | ||
90 | if (config.has('cargo-watch.enable')) { | 168 | if (config.has('cargo-watch.enable')) { |
91 | this.cargoWatchOptions.enable = config.get<boolean>( | 169 | 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..f1f9f4a25 --- /dev/null +++ b/editors/code/src/installation/download_file.ts | |||
@@ -0,0 +1,44 @@ | |||
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` with `destFilePermissions`. | ||
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 | destFilePermissions: number, | ||
15 | onProgress: (readBytes: number, totalBytes: number) => void | ||
16 | ): Promise<void> { | ||
17 | const res = await fetch(url); | ||
18 | |||
19 | if (!res.ok) { | ||
20 | console.log("Error", res.status, "while downloading file from", url); | ||
21 | console.dir({ body: await res.text(), headers: res.headers }, { depth: 3 }); | ||
22 | |||
23 | throw new Error(`Got response ${res.status} when trying to download a file`); | ||
24 | } | ||
25 | |||
26 | const totalBytes = Number(res.headers.get('content-length')); | ||
27 | assert(!Number.isNaN(totalBytes), "Sanity check of content-length protocol"); | ||
28 | |||
29 | let readBytes = 0; | ||
30 | |||
31 | console.log("Downloading file of", totalBytes, "bytes size from", url, "to", destFilePath); | ||
32 | |||
33 | return new Promise<void>((resolve, reject) => res.body | ||
34 | .on("data", (chunk: Buffer) => { | ||
35 | readBytes += chunk.length; | ||
36 | onProgress(readBytes, totalBytes); | ||
37 | }) | ||
38 | .on("error", reject) | ||
39 | .pipe(fs | ||
40 | .createWriteStream(destFilePath, { mode: destFilePermissions }) | ||
41 | .on("close", resolve) | ||
42 | ) | ||
43 | ); | ||
44 | } | ||
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..52c5cbe7d --- /dev/null +++ b/editors/code/src/installation/language_server.ts | |||
@@ -0,0 +1,146 @@ | |||
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 | const filePermissions = 0o755; // (rwx, r_x, r_x) | ||
39 | await downloadFile(downloadUrl, installationPath, filePermissions, throttle( | ||
40 | 200, | ||
41 | /* noTrailing: */ true, | ||
42 | (readBytes, totalBytes) => { | ||
43 | const newPercentage = (readBytes / totalBytes) * 100; | ||
44 | progress.report({ | ||
45 | message: newPercentage.toFixed(0) + "%", | ||
46 | increment: newPercentage - lastPrecentage | ||
47 | }); | ||
48 | |||
49 | lastPrecentage = newPercentage; | ||
50 | }) | ||
51 | ); | ||
52 | } | ||
53 | ); | ||
54 | console.timeEnd("Downloading ra_lsp_server"); | ||
55 | } | ||
56 | export async function ensureLanguageServerBinary( | ||
57 | langServerSource: null | BinarySource | ||
58 | ): Promise<null | string> { | ||
59 | |||
60 | if (!langServerSource) { | ||
61 | vscode.window.showErrorMessage( | ||
62 | "Unfortunately we don't ship binaries for your platform yet. " + | ||
63 | "You need to manually clone rust-analyzer repository and " + | ||
64 | "run `cargo xtask install --server` to build the language server from sources. " + | ||
65 | "If you feel that your platform should be supported, please create an issue " + | ||
66 | "about that [here](https://github.com/rust-analyzer/rust-analyzer/issues) and we " + | ||
67 | "will consider it." | ||
68 | ); | ||
69 | return null; | ||
70 | } | ||
71 | |||
72 | switch (langServerSource.type) { | ||
73 | case BinarySource.Type.ExplicitPath: { | ||
74 | if (isBinaryAvailable(langServerSource.path)) { | ||
75 | return langServerSource.path; | ||
76 | } | ||
77 | |||
78 | vscode.window.showErrorMessage( | ||
79 | `Unable to run ${langServerSource.path} binary. ` + | ||
80 | `To use the pre-built language server, set "rust-analyzer.raLspServerPath" ` + | ||
81 | "value to `null` or remove it from the settings to use it by default." | ||
82 | ); | ||
83 | return null; | ||
84 | } | ||
85 | case BinarySource.Type.GithubRelease: { | ||
86 | const prebuiltBinaryPath = path.join(langServerSource.dir, langServerSource.file); | ||
87 | |||
88 | if (isBinaryAvailable(prebuiltBinaryPath)) { | ||
89 | return prebuiltBinaryPath; | ||
90 | } | ||
91 | |||
92 | const userResponse = await vscode.window.showInformationMessage( | ||
93 | "Language server binary for rust-analyzer was not found. " + | ||
94 | "Do you want to download it now?", | ||
95 | "Download now", "Cancel" | ||
96 | ); | ||
97 | if (userResponse !== "Download now") return null; | ||
98 | |||
99 | try { | ||
100 | await downloadLatestLanguageServer(langServerSource); | ||
101 | } catch (err) { | ||
102 | vscode.window.showErrorMessage( | ||
103 | `Failed to download language server from ${langServerSource.repo.name} ` + | ||
104 | `GitHub repository: ${err.message}` | ||
105 | ); | ||
106 | |||
107 | dns.resolve('example.com').then( | ||
108 | addrs => console.log("DNS resolution for example.com was successful", addrs), | ||
109 | err => { | ||
110 | console.error( | ||
111 | "DNS resolution for example.com failed, " + | ||
112 | "there might be an issue with Internet availability" | ||
113 | ); | ||
114 | console.error(err); | ||
115 | } | ||
116 | ); | ||
117 | |||
118 | return null; | ||
119 | } | ||
120 | |||
121 | if (!isBinaryAvailable(prebuiltBinaryPath)) assert(false, | ||
122 | `Downloaded language server binary is not functional.` + | ||
123 | `Downloaded from: ${JSON.stringify(langServerSource)}` | ||
124 | ); | ||
125 | |||
126 | |||
127 | vscode.window.showInformationMessage( | ||
128 | "Rust analyzer language server was successfully installed 🦀" | ||
129 | ); | ||
130 | |||
131 | return prebuiltBinaryPath; | ||
132 | } | ||
133 | } | ||
134 | |||
135 | function isBinaryAvailable(binaryPath: string) { | ||
136 | const res = spawnSync(binaryPath, ["--version"]); | ||
137 | |||
138 | // ACHTUNG! `res` type declaration is inherently wrong, see | ||
139 | // https://github.com/DefinitelyTyped/DefinitelyTyped/issues/42221 | ||
140 | |||
141 | console.log("Checked binary availablity via --version", res); | ||
142 | console.log(binaryPath, "--version output:", res.output?.map(String)); | ||
143 | |||
144 | return res.status === 0; | ||
145 | } | ||
146 | } | ||
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, |