diff options
Diffstat (limited to 'editors/code')
-rw-r--r-- | editors/code/package.json | 2 | ||||
-rw-r--r-- | editors/code/src/client.ts | 2 | ||||
-rw-r--r-- | editors/code/src/config.ts | 17 | ||||
-rw-r--r-- | editors/code/src/ctx.ts | 4 | ||||
-rw-r--r-- | editors/code/src/installation/download_artifact.ts | 58 | ||||
-rw-r--r-- | editors/code/src/installation/fetch_artifact_release_info.ts (renamed from editors/code/src/installation/fetch_latest_artifact_release_info.ts) | 16 | ||||
-rw-r--r-- | editors/code/src/installation/interfaces.ts | 13 | ||||
-rw-r--r-- | editors/code/src/installation/server.ts | 168 |
8 files changed, 176 insertions, 104 deletions
diff --git a/editors/code/package.json b/editors/code/package.json index a607c2148..96b8e9eb0 100644 --- a/editors/code/package.json +++ b/editors/code/package.json | |||
@@ -6,7 +6,7 @@ | |||
6 | "private": true, | 6 | "private": true, |
7 | "icon": "icon.png", | 7 | "icon": "icon.png", |
8 | "//": "The real version is in release.yaml, this one just needs to be bigger", | 8 | "//": "The real version is in release.yaml, this one just needs to be bigger", |
9 | "version": "0.2.0-dev", | 9 | "version": "0.2.20200211-dev", |
10 | "publisher": "matklad", | 10 | "publisher": "matklad", |
11 | "repository": { | 11 | "repository": { |
12 | "url": "https://github.com/rust-analyzer/rust-analyzer.git", | 12 | "url": "https://github.com/rust-analyzer/rust-analyzer.git", |
diff --git a/editors/code/src/client.ts b/editors/code/src/client.ts index 12c97be2f..efef820ab 100644 --- a/editors/code/src/client.ts +++ b/editors/code/src/client.ts | |||
@@ -11,7 +11,7 @@ export async function createClient(config: Config): Promise<null | lc.LanguageCl | |||
11 | // It might be a good idea to test if the uri points to a file. | 11 | // It might be a good idea to test if the uri points to a file. |
12 | const workspaceFolderPath = vscode.workspace.workspaceFolders?.[0]?.uri.fsPath ?? '.'; | 12 | const workspaceFolderPath = vscode.workspace.workspaceFolders?.[0]?.uri.fsPath ?? '.'; |
13 | 13 | ||
14 | const serverPath = await ensureServerBinary(config.serverBinarySource); | 14 | const serverPath = await ensureServerBinary(config.serverSource); |
15 | if (!serverPath) return null; | 15 | if (!serverPath) return null; |
16 | 16 | ||
17 | const run: lc.Executable = { | 17 | const run: lc.Executable = { |
diff --git a/editors/code/src/config.ts b/editors/code/src/config.ts index 8c033052b..70cb0a612 100644 --- a/editors/code/src/config.ts +++ b/editors/code/src/config.ts | |||
@@ -24,6 +24,19 @@ export class Config { | |||
24 | ] | 24 | ] |
25 | .map(opt => `${Config.rootSection}.${opt}`); | 25 | .map(opt => `${Config.rootSection}.${opt}`); |
26 | 26 | ||
27 | private static readonly extensionVersion: string = (() => { | ||
28 | const packageJsonVersion = vscode | ||
29 | .extensions | ||
30 | .getExtension("matklad.rust-analyzer")! | ||
31 | .packageJSON | ||
32 | .version as string; // n.n.YYYYMMDD | ||
33 | |||
34 | const realVersionRegexp = /^\d+\.\d+\.(\d{4})(\d{2})(\d{2})/; | ||
35 | const [, yyyy, mm, dd] = packageJsonVersion.match(realVersionRegexp)!; | ||
36 | |||
37 | return `${yyyy}-${mm}-${dd}`; | ||
38 | })(); | ||
39 | |||
27 | private cfg!: vscode.WorkspaceConfiguration; | 40 | private cfg!: vscode.WorkspaceConfiguration; |
28 | 41 | ||
29 | constructor(private readonly ctx: vscode.ExtensionContext) { | 42 | constructor(private readonly ctx: vscode.ExtensionContext) { |
@@ -98,7 +111,7 @@ export class Config { | |||
98 | } | 111 | } |
99 | } | 112 | } |
100 | 113 | ||
101 | get serverBinarySource(): null | BinarySource { | 114 | get serverSource(): null | BinarySource { |
102 | const serverPath = RA_LSP_DEBUG ?? this.cfg.get<null | string>("raLspServerPath"); | 115 | const serverPath = RA_LSP_DEBUG ?? this.cfg.get<null | string>("raLspServerPath"); |
103 | 116 | ||
104 | if (serverPath) { | 117 | if (serverPath) { |
@@ -116,6 +129,8 @@ export class Config { | |||
116 | type: BinarySource.Type.GithubRelease, | 129 | type: BinarySource.Type.GithubRelease, |
117 | dir: this.ctx.globalStoragePath, | 130 | dir: this.ctx.globalStoragePath, |
118 | file: prebuiltBinaryName, | 131 | file: prebuiltBinaryName, |
132 | storage: this.ctx.globalState, | ||
133 | version: Config.extensionVersion, | ||
119 | repo: { | 134 | repo: { |
120 | name: "rust-analyzer", | 135 | name: "rust-analyzer", |
121 | owner: "rust-analyzer", | 136 | owner: "rust-analyzer", |
diff --git a/editors/code/src/ctx.ts b/editors/code/src/ctx.ts index 70042a479..9fcf2ec38 100644 --- a/editors/code/src/ctx.ts +++ b/editors/code/src/ctx.ts | |||
@@ -60,6 +60,10 @@ export class Ctx { | |||
60 | this.pushCleanup(d); | 60 | this.pushCleanup(d); |
61 | } | 61 | } |
62 | 62 | ||
63 | get globalState(): vscode.Memento { | ||
64 | return this.extCtx.globalState; | ||
65 | } | ||
66 | |||
63 | get subscriptions(): Disposable[] { | 67 | get subscriptions(): Disposable[] { |
64 | return this.extCtx.subscriptions; | 68 | return this.extCtx.subscriptions; |
65 | } | 69 | } |
diff --git a/editors/code/src/installation/download_artifact.ts b/editors/code/src/installation/download_artifact.ts new file mode 100644 index 000000000..de655f8f4 --- /dev/null +++ b/editors/code/src/installation/download_artifact.ts | |||
@@ -0,0 +1,58 @@ | |||
1 | import * as vscode from "vscode"; | ||
2 | import * as path from "path"; | ||
3 | import { promises as fs } from "fs"; | ||
4 | import { strict as assert } from "assert"; | ||
5 | |||
6 | import { ArtifactReleaseInfo } from "./interfaces"; | ||
7 | import { downloadFile } from "./download_file"; | ||
8 | import { throttle } from "throttle-debounce"; | ||
9 | |||
10 | /** | ||
11 | * Downloads artifact from given `downloadUrl`. | ||
12 | * Creates `installationDir` if it is not yet created and put the artifact under | ||
13 | * `artifactFileName`. | ||
14 | * Displays info about the download progress in an info message printing the name | ||
15 | * of the artifact as `displayName`. | ||
16 | */ | ||
17 | export async function downloadArtifact( | ||
18 | {downloadUrl, releaseName}: ArtifactReleaseInfo, | ||
19 | artifactFileName: string, | ||
20 | installationDir: string, | ||
21 | displayName: string, | ||
22 | ) { | ||
23 | await fs.mkdir(installationDir).catch(err => assert.strictEqual( | ||
24 | err?.code, | ||
25 | "EEXIST", | ||
26 | `Couldn't create directory "${installationDir}" to download `+ | ||
27 | `${artifactFileName} artifact: ${err.message}` | ||
28 | )); | ||
29 | |||
30 | const installationPath = path.join(installationDir, artifactFileName); | ||
31 | |||
32 | console.time(`Downloading ${artifactFileName}`); | ||
33 | await vscode.window.withProgress( | ||
34 | { | ||
35 | location: vscode.ProgressLocation.Notification, | ||
36 | cancellable: false, // FIXME: add support for canceling download? | ||
37 | title: `Downloading ${displayName} (${releaseName})` | ||
38 | }, | ||
39 | async (progress, _cancellationToken) => { | ||
40 | let lastPrecentage = 0; | ||
41 | const filePermissions = 0o755; // (rwx, r_x, r_x) | ||
42 | await downloadFile(downloadUrl, installationPath, filePermissions, throttle( | ||
43 | 200, | ||
44 | /* noTrailing: */ true, | ||
45 | (readBytes, totalBytes) => { | ||
46 | const newPercentage = (readBytes / totalBytes) * 100; | ||
47 | progress.report({ | ||
48 | message: newPercentage.toFixed(0) + "%", | ||
49 | increment: newPercentage - lastPrecentage | ||
50 | }); | ||
51 | |||
52 | lastPrecentage = newPercentage; | ||
53 | }) | ||
54 | ); | ||
55 | } | ||
56 | ); | ||
57 | console.timeEnd(`Downloading ${artifactFileName}`); | ||
58 | } | ||
diff --git a/editors/code/src/installation/fetch_latest_artifact_release_info.ts b/editors/code/src/installation/fetch_artifact_release_info.ts index 29ee029a7..7d497057a 100644 --- a/editors/code/src/installation/fetch_latest_artifact_release_info.ts +++ b/editors/code/src/installation/fetch_artifact_release_info.ts | |||
@@ -3,24 +3,30 @@ import { GithubRepo, ArtifactReleaseInfo } from "./interfaces"; | |||
3 | 3 | ||
4 | const GITHUB_API_ENDPOINT_URL = "https://api.github.com"; | 4 | const GITHUB_API_ENDPOINT_URL = "https://api.github.com"; |
5 | 5 | ||
6 | |||
6 | /** | 7 | /** |
7 | * Fetches the latest release from GitHub `repo` and returns metadata about | 8 | * Fetches the release with `releaseTag` (or just latest release when not specified) |
8 | * `artifactFileName` shipped with this release or `null` if no such artifact was published. | 9 | * from GitHub `repo` and returns metadata about `artifactFileName` shipped with |
10 | * this release or `null` if no such artifact was published. | ||
9 | */ | 11 | */ |
10 | export async function fetchLatestArtifactReleaseInfo( | 12 | export async function fetchArtifactReleaseInfo( |
11 | repo: GithubRepo, artifactFileName: string | 13 | repo: GithubRepo, artifactFileName: string, releaseTag?: string |
12 | ): Promise<null | ArtifactReleaseInfo> { | 14 | ): Promise<null | ArtifactReleaseInfo> { |
13 | 15 | ||
14 | const repoOwner = encodeURIComponent(repo.owner); | 16 | const repoOwner = encodeURIComponent(repo.owner); |
15 | const repoName = encodeURIComponent(repo.name); | 17 | const repoName = encodeURIComponent(repo.name); |
16 | 18 | ||
17 | const apiEndpointPath = `/repos/${repoOwner}/${repoName}/releases/latest`; | 19 | const apiEndpointPath = releaseTag |
20 | ? `/repos/${repoOwner}/${repoName}/releases/tags/${releaseTag}` | ||
21 | : `/repos/${repoOwner}/${repoName}/releases/latest`; | ||
22 | |||
18 | const requestUrl = GITHUB_API_ENDPOINT_URL + apiEndpointPath; | 23 | const requestUrl = GITHUB_API_ENDPOINT_URL + apiEndpointPath; |
19 | 24 | ||
20 | // We skip runtime type checks for simplicity (here we cast from `any` to `GithubRelease`) | 25 | // We skip runtime type checks for simplicity (here we cast from `any` to `GithubRelease`) |
21 | 26 | ||
22 | console.log("Issuing request for released artifacts metadata to", requestUrl); | 27 | console.log("Issuing request for released artifacts metadata to", requestUrl); |
23 | 28 | ||
29 | // FIXME: handle non-ok response | ||
24 | const response: GithubRelease = await fetch(requestUrl, { | 30 | const response: GithubRelease = await fetch(requestUrl, { |
25 | headers: { Accept: "application/vnd.github.v3+json" } | 31 | headers: { Accept: "application/vnd.github.v3+json" } |
26 | }) | 32 | }) |
diff --git a/editors/code/src/installation/interfaces.ts b/editors/code/src/installation/interfaces.ts index 93ea577d4..e40839e4b 100644 --- a/editors/code/src/installation/interfaces.ts +++ b/editors/code/src/installation/interfaces.ts | |||
@@ -1,3 +1,5 @@ | |||
1 | import * as vscode from "vscode"; | ||
2 | |||
1 | export interface GithubRepo { | 3 | export interface GithubRepo { |
2 | name: string; | 4 | name: string; |
3 | owner: string; | 5 | owner: string; |
@@ -50,6 +52,17 @@ export namespace BinarySource { | |||
50 | * and in local `.dir`. | 52 | * and in local `.dir`. |
51 | */ | 53 | */ |
52 | file: string; | 54 | file: string; |
55 | |||
56 | /** | ||
57 | * Tag of github release that denotes a version required by this extension. | ||
58 | */ | ||
59 | version: string; | ||
60 | |||
61 | /** | ||
62 | * Object that provides `get()/update()` operations to store metadata | ||
63 | * about the actual binary, e.g. its actual version. | ||
64 | */ | ||
65 | storage: vscode.Memento; | ||
53 | } | 66 | } |
54 | 67 | ||
55 | } | 68 | } |
diff --git a/editors/code/src/installation/server.ts b/editors/code/src/installation/server.ts index 406e2299c..80cb719e3 100644 --- a/editors/code/src/installation/server.ts +++ b/editors/code/src/installation/server.ts | |||
@@ -1,63 +1,15 @@ | |||
1 | import * as vscode from "vscode"; | 1 | import * as vscode from "vscode"; |
2 | import * as path from "path"; | 2 | import * as path from "path"; |
3 | import { strict as assert } from "assert"; | 3 | import { strict as assert } from "assert"; |
4 | import { promises as fs } from "fs"; | ||
5 | import { promises as dns } from "dns"; | 4 | import { promises as dns } from "dns"; |
6 | import { spawnSync } from "child_process"; | 5 | import { spawnSync } from "child_process"; |
7 | import { throttle } from "throttle-debounce"; | ||
8 | 6 | ||
9 | import { BinarySource } from "./interfaces"; | 7 | import { BinarySource } from "./interfaces"; |
10 | import { fetchLatestArtifactReleaseInfo } from "./fetch_latest_artifact_release_info"; | 8 | import { fetchArtifactReleaseInfo } from "./fetch_artifact_release_info"; |
11 | import { downloadFile } from "./download_file"; | 9 | import { downloadArtifact } from "./download_artifact"; |
12 | |||
13 | export async function downloadLatestServer( | ||
14 | {file: artifactFileName, dir: installationDir, repo}: BinarySource.GithubRelease | ||
15 | ) { | ||
16 | const { releaseName, downloadUrl } = (await fetchLatestArtifactReleaseInfo( | ||
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 ensureServerBinary( | ||
57 | serverSource: null | BinarySource | ||
58 | ): Promise<null | string> { | ||
59 | 10 | ||
60 | if (!serverSource) { | 11 | export async function ensureServerBinary(source: null | BinarySource): Promise<null | string> { |
12 | if (!source) { | ||
61 | vscode.window.showErrorMessage( | 13 | vscode.window.showErrorMessage( |
62 | "Unfortunately we don't ship binaries for your platform yet. " + | 14 | "Unfortunately we don't ship binaries for your platform yet. " + |
63 | "You need to manually clone rust-analyzer repository and " + | 15 | "You need to manually clone rust-analyzer repository and " + |
@@ -69,80 +21,104 @@ export async function ensureServerBinary( | |||
69 | return null; | 21 | return null; |
70 | } | 22 | } |
71 | 23 | ||
72 | switch (serverSource.type) { | 24 | switch (source.type) { |
73 | case BinarySource.Type.ExplicitPath: { | 25 | case BinarySource.Type.ExplicitPath: { |
74 | if (isBinaryAvailable(serverSource.path)) { | 26 | if (isBinaryAvailable(source.path)) { |
75 | return serverSource.path; | 27 | return source.path; |
76 | } | 28 | } |
77 | 29 | ||
78 | vscode.window.showErrorMessage( | 30 | vscode.window.showErrorMessage( |
79 | `Unable to run ${serverSource.path} binary. ` + | 31 | `Unable to run ${source.path} binary. ` + |
80 | `To use the pre-built language server, set "rust-analyzer.raLspServerPath" ` + | 32 | `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." | 33 | "value to `null` or remove it from the settings to use it by default." |
82 | ); | 34 | ); |
83 | return null; | 35 | return null; |
84 | } | 36 | } |
85 | case BinarySource.Type.GithubRelease: { | 37 | case BinarySource.Type.GithubRelease: { |
86 | const prebuiltBinaryPath = path.join(serverSource.dir, serverSource.file); | 38 | const prebuiltBinaryPath = path.join(source.dir, source.file); |
39 | |||
40 | const installedVersion: null | string = getServerVersion(source.storage); | ||
41 | const requiredVersion: string = source.version; | ||
87 | 42 | ||
88 | if (isBinaryAvailable(prebuiltBinaryPath)) { | 43 | console.log("Installed version:", installedVersion, "required:", requiredVersion); |
44 | |||
45 | if (isBinaryAvailable(prebuiltBinaryPath) && installedVersion == requiredVersion) { | ||
46 | // FIXME: check for new releases and notify the user to update if possible | ||
89 | return prebuiltBinaryPath; | 47 | return prebuiltBinaryPath; |
90 | } | 48 | } |
91 | 49 | ||
92 | const userResponse = await vscode.window.showInformationMessage( | 50 | const userResponse = await vscode.window.showInformationMessage( |
93 | "Language server binary for rust-analyzer was not found. " + | 51 | `Language server version ${source.version} for rust-analyzer is not installed. ` + |
94 | "Do you want to download it now?", | 52 | "Do you want to download it now?", |
95 | "Download now", "Cancel" | 53 | "Download now", "Cancel" |
96 | ); | 54 | ); |
97 | if (userResponse !== "Download now") return null; | 55 | if (userResponse !== "Download now") return null; |
98 | 56 | ||
99 | try { | 57 | if (!await downloadServer(source)) return null; |
100 | await downloadLatestServer(serverSource); | ||
101 | } catch (err) { | ||
102 | vscode.window.showErrorMessage( | ||
103 | `Failed to download language server from ${serverSource.repo.name} ` + | ||
104 | `GitHub repository: ${err.message}` | ||
105 | ); | ||
106 | 58 | ||
107 | console.error(err); | 59 | return prebuiltBinaryPath; |
60 | } | ||
61 | } | ||
62 | } | ||
108 | 63 | ||
109 | dns.resolve('example.com').then( | 64 | async function downloadServer(source: BinarySource.GithubRelease): Promise<boolean> { |
110 | addrs => console.log("DNS resolution for example.com was successful", addrs), | 65 | try { |
111 | err => { | 66 | const releaseInfo = (await fetchArtifactReleaseInfo(source.repo, source.file, source.version))!; |
112 | console.error( | 67 | |
113 | "DNS resolution for example.com failed, " + | 68 | await downloadArtifact(releaseInfo, source.file, source.dir, "language server"); |
114 | "there might be an issue with Internet availability" | 69 | await setServerVersion(source.storage, releaseInfo.releaseName); |
115 | ); | 70 | } catch (err) { |
116 | console.error(err); | 71 | vscode.window.showErrorMessage( |
117 | } | 72 | `Failed to download language server from ${source.repo.name} ` + |
118 | ); | 73 | `GitHub repository: ${err.message}` |
74 | ); | ||
75 | |||
76 | console.error(err); | ||
119 | 77 | ||
120 | return null; | 78 | dns.resolve('example.com').then( |
79 | addrs => console.log("DNS resolution for example.com was successful", addrs), | ||
80 | err => { | ||
81 | console.error( | ||
82 | "DNS resolution for example.com failed, " + | ||
83 | "there might be an issue with Internet availability" | ||
84 | ); | ||
85 | console.error(err); | ||
121 | } | 86 | } |
87 | ); | ||
88 | return false; | ||
89 | } | ||
122 | 90 | ||
123 | if (!isBinaryAvailable(prebuiltBinaryPath)) assert(false, | 91 | if (!isBinaryAvailable(path.join(source.dir, source.file))) assert(false, |
124 | `Downloaded language server binary is not functional.` + | 92 | `Downloaded language server binary is not functional.` + |
125 | `Downloaded from: ${JSON.stringify(serverSource)}` | 93 | `Downloaded from: ${JSON.stringify(source, null, 4)}` |
126 | ); | 94 | ); |
127 | 95 | ||
96 | vscode.window.showInformationMessage( | ||
97 | "Rust analyzer language server was successfully installed 🦀" | ||
98 | ); | ||
128 | 99 | ||
129 | vscode.window.showInformationMessage( | 100 | return true; |
130 | "Rust analyzer language server was successfully installed 🦀" | 101 | } |
131 | ); | ||
132 | 102 | ||
133 | return prebuiltBinaryPath; | 103 | function isBinaryAvailable(binaryPath: string): boolean { |
134 | } | 104 | const res = spawnSync(binaryPath, ["--version"]); |
135 | } | ||
136 | 105 | ||
137 | function isBinaryAvailable(binaryPath: string) { | 106 | // ACHTUNG! `res` type declaration is inherently wrong, see |
138 | const res = spawnSync(binaryPath, ["--version"]); | 107 | // https://github.com/DefinitelyTyped/DefinitelyTyped/issues/42221 |
139 | 108 | ||
140 | // ACHTUNG! `res` type declaration is inherently wrong, see | 109 | console.log("Checked binary availablity via --version", res); |
141 | // https://github.com/DefinitelyTyped/DefinitelyTyped/issues/42221 | 110 | console.log(binaryPath, "--version output:", res.output?.map(String)); |
142 | 111 | ||
143 | console.log("Checked binary availablity via --version", res); | 112 | return res.status === 0; |
144 | console.log(binaryPath, "--version output:", res.output?.map(String)); | 113 | } |
145 | 114 | ||
146 | return res.status === 0; | 115 | function getServerVersion(storage: vscode.Memento): null | string { |
147 | } | 116 | const version = storage.get<null | string>("server-version", null); |
117 | console.log("Get server-version:", version); | ||
118 | return version; | ||
119 | } | ||
120 | |||
121 | async function setServerVersion(storage: vscode.Memento, version: string): Promise<void> { | ||
122 | console.log("Set server-version:", version); | ||
123 | await storage.update("server-version", version.toString()); | ||
148 | } | 124 | } |