aboutsummaryrefslogtreecommitdiff
path: root/editors/code/src
diff options
context:
space:
mode:
Diffstat (limited to 'editors/code/src')
-rw-r--r--editors/code/src/client.ts2
-rw-r--r--editors/code/src/config.ts17
-rw-r--r--editors/code/src/ctx.ts4
-rw-r--r--editors/code/src/installation/download_artifact.ts58
-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.ts13
-rw-r--r--editors/code/src/installation/server.ts168
7 files changed, 175 insertions, 103 deletions
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 @@
1import * as vscode from "vscode";
2import * as path from "path";
3import { promises as fs } from "fs";
4import { strict as assert } from "assert";
5
6import { ArtifactReleaseInfo } from "./interfaces";
7import { downloadFile } from "./download_file";
8import { 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 */
17export 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
4const GITHUB_API_ENDPOINT_URL = "https://api.github.com"; 4const 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 */
10export async function fetchLatestArtifactReleaseInfo( 12export 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 @@
1import * as vscode from "vscode";
2
1export interface GithubRepo { 3export 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 @@
1import * as vscode from "vscode"; 1import * as vscode from "vscode";
2import * as path from "path"; 2import * as path from "path";
3import { strict as assert } from "assert"; 3import { strict as assert } from "assert";
4import { promises as fs } from "fs";
5import { promises as dns } from "dns"; 4import { promises as dns } from "dns";
6import { spawnSync } from "child_process"; 5import { spawnSync } from "child_process";
7import { throttle } from "throttle-debounce";
8 6
9import { BinarySource } from "./interfaces"; 7import { BinarySource } from "./interfaces";
10import { fetchLatestArtifactReleaseInfo } from "./fetch_latest_artifact_release_info"; 8import { fetchArtifactReleaseInfo } from "./fetch_artifact_release_info";
11import { downloadFile } from "./download_file"; 9import { downloadArtifact } from "./download_artifact";
12
13export 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}
56export async function ensureServerBinary(
57 serverSource: null | BinarySource
58): Promise<null | string> {
59 10
60 if (!serverSource) { 11export 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( 64async 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; 103function 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; 115function 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
121async 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}