aboutsummaryrefslogtreecommitdiff
path: root/editors/code
diff options
context:
space:
mode:
Diffstat (limited to 'editors/code')
-rw-r--r--editors/code/package-lock.json2
-rw-r--r--editors/code/package.json17
-rw-r--r--editors/code/src/commands/server_version.ts3
-rw-r--r--editors/code/src/config.ts117
-rw-r--r--editors/code/src/installation/download_artifact.ts50
-rw-r--r--editors/code/src/installation/downloads.ts (renamed from editors/code/src/installation/download_file.ts)46
-rw-r--r--editors/code/src/installation/extension.ts144
-rw-r--r--editors/code/src/installation/fetch_artifact_release_info.ts3
-rw-r--r--editors/code/src/installation/interfaces.ts18
-rw-r--r--editors/code/src/installation/server.ts104
-rw-r--r--editors/code/src/main.ts9
-rw-r--r--editors/code/src/util.ts69
12 files changed, 435 insertions, 147 deletions
diff --git a/editors/code/package-lock.json b/editors/code/package-lock.json
index b07964546..575dc7c4a 100644
--- a/editors/code/package-lock.json
+++ b/editors/code/package-lock.json
@@ -1,6 +1,6 @@
1{ 1{
2 "name": "rust-analyzer", 2 "name": "rust-analyzer",
3 "version": "0.2.20200211-dev", 3 "version": "0.2.20200309-nightly",
4 "lockfileVersion": 1, 4 "lockfileVersion": 1,
5 "requires": true, 5 "requires": true,
6 "dependencies": { 6 "dependencies": {
diff --git a/editors/code/package.json b/editors/code/package.json
index 3aaae357a..faf10528d 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.20200211-dev", 9 "version": "0.2.20200309-nightly",
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",
@@ -219,6 +219,19 @@
219 } 219 }
220 } 220 }
221 }, 221 },
222 "rust-analyzer.updates.channel": {
223 "type": "string",
224 "enum": [
225 "stable",
226 "nightly"
227 ],
228 "default": "stable",
229 "markdownEnumDescriptions": [
230 "`\"stable\"` updates are shipped weekly, they don't contain cutting-edge features from VSCode proposed APIs but have less bugs in general",
231 "`\"nightly\"` updates are shipped daily, they contain cutting-edge features and latest bug fixes. These releases help us get your feedback very quickly and speed up rust-analyzer development **drastically**"
232 ],
233 "markdownDescription": "Choose `\"nightly\"` updates to get the latest features and bug fixes every day. While `\"stable\"` releases occur weekly and don't contain cutting-edge features from VSCode proposed APIs"
234 },
222 "rust-analyzer.updates.askBeforeDownload": { 235 "rust-analyzer.updates.askBeforeDownload": {
223 "type": "boolean", 236 "type": "boolean",
224 "default": true, 237 "default": true,
@@ -235,7 +248,7 @@
235 "string" 248 "string"
236 ], 249 ],
237 "default": null, 250 "default": null,
238 "description": "Path to rust-analyzer executable (points to bundled binary by default)" 251 "description": "Path to rust-analyzer executable (points to bundled binary by default). If this is set, then \"rust-analyzer.updates.channel\" setting is not used"
239 }, 252 },
240 "rust-analyzer.excludeGlobs": { 253 "rust-analyzer.excludeGlobs": {
241 "type": "array", 254 "type": "array",
diff --git a/editors/code/src/commands/server_version.ts b/editors/code/src/commands/server_version.ts
index 421301b42..c4d84b443 100644
--- a/editors/code/src/commands/server_version.ts
+++ b/editors/code/src/commands/server_version.ts
@@ -5,7 +5,7 @@ import { spawnSync } from 'child_process';
5 5
6export function serverVersion(ctx: Ctx): Cmd { 6export function serverVersion(ctx: Ctx): Cmd {
7 return async () => { 7 return async () => {
8 const binaryPath = await ensureServerBinary(ctx.config.serverSource); 8 const binaryPath = await ensureServerBinary(ctx.config);
9 9
10 if (binaryPath == null) { 10 if (binaryPath == null) {
11 throw new Error( 11 throw new Error(
@@ -18,4 +18,3 @@ export function serverVersion(ctx: Ctx): Cmd {
18 vscode.window.showInformationMessage('rust-analyzer version : ' + version); 18 vscode.window.showInformationMessage('rust-analyzer version : ' + version);
19 }; 19 };
20} 20}
21
diff --git a/editors/code/src/config.ts b/editors/code/src/config.ts
index 6db073bec..f63e1d20e 100644
--- a/editors/code/src/config.ts
+++ b/editors/code/src/config.ts
@@ -1,7 +1,7 @@
1import * as os from "os"; 1import * as os from "os";
2import * as vscode from 'vscode'; 2import * as vscode from 'vscode';
3import { ArtifactSource } from "./installation/interfaces"; 3import { ArtifactSource } from "./installation/interfaces";
4import { log } from "./util"; 4import { log, vscodeReloadWindow } from "./util";
5 5
6const RA_LSP_DEBUG = process.env.__RA_LSP_SERVER_DEBUG; 6const RA_LSP_DEBUG = process.env.__RA_LSP_SERVER_DEBUG;
7 7
@@ -23,25 +23,40 @@ export interface CargoFeatures {
23 allFeatures: boolean; 23 allFeatures: boolean;
24 features: string[]; 24 features: string[];
25} 25}
26
27export const enum UpdatesChannel {
28 Stable = "stable",
29 Nightly = "nightly"
30}
31
32export const NIGHTLY_TAG = "nightly";
26export class Config { 33export class Config {
27 private static readonly rootSection = "rust-analyzer"; 34 readonly extensionId = "matklad.rust-analyzer";
28 private static readonly requiresReloadOpts = [ 35
36 private readonly rootSection = "rust-analyzer";
37 private readonly requiresReloadOpts = [
38 "serverPath",
29 "cargoFeatures", 39 "cargoFeatures",
30 "cargo-watch", 40 "cargo-watch",
31 "highlighting.semanticTokens", 41 "highlighting.semanticTokens",
32 "inlayHints", 42 "inlayHints",
33 ] 43 ]
34 .map(opt => `${Config.rootSection}.${opt}`); 44 .map(opt => `${this.rootSection}.${opt}`);
35 45
36 private static readonly extensionVersion: string = (() => { 46 readonly packageJsonVersion = vscode
37 const packageJsonVersion = vscode 47 .extensions
38 .extensions 48 .getExtension(this.extensionId)!
39 .getExtension("matklad.rust-analyzer")! 49 .packageJSON
40 .packageJSON 50 .version as string; // n.n.YYYYMMDD[-nightly]
41 .version as string; // n.n.YYYYMMDD 51
52 /**
53 * Either `nightly` or `YYYY-MM-DD` (i.e. `stable` release)
54 */
55 readonly extensionReleaseTag: string = (() => {
56 if (this.packageJsonVersion.endsWith(NIGHTLY_TAG)) return NIGHTLY_TAG;
42 57
43 const realVersionRegexp = /^\d+\.\d+\.(\d{4})(\d{2})(\d{2})/; 58 const realVersionRegexp = /^\d+\.\d+\.(\d{4})(\d{2})(\d{2})/;
44 const [, yyyy, mm, dd] = packageJsonVersion.match(realVersionRegexp)!; 59 const [, yyyy, mm, dd] = this.packageJsonVersion.match(realVersionRegexp)!;
45 60
46 return `${yyyy}-${mm}-${dd}`; 61 return `${yyyy}-${mm}-${dd}`;
47 })(); 62 })();
@@ -54,16 +69,19 @@ export class Config {
54 } 69 }
55 70
56 private refreshConfig() { 71 private refreshConfig() {
57 this.cfg = vscode.workspace.getConfiguration(Config.rootSection); 72 this.cfg = vscode.workspace.getConfiguration(this.rootSection);
58 const enableLogging = this.cfg.get("trace.extension") as boolean; 73 const enableLogging = this.cfg.get("trace.extension") as boolean;
59 log.setEnabled(enableLogging); 74 log.setEnabled(enableLogging);
60 log.debug("Using configuration:", this.cfg); 75 log.debug(
76 "Extension version:", this.packageJsonVersion,
77 "using configuration:", this.cfg
78 );
61 } 79 }
62 80
63 private async onConfigChange(event: vscode.ConfigurationChangeEvent) { 81 private async onConfigChange(event: vscode.ConfigurationChangeEvent) {
64 this.refreshConfig(); 82 this.refreshConfig();
65 83
66 const requiresReloadOpt = Config.requiresReloadOpts.find( 84 const requiresReloadOpt = this.requiresReloadOpts.find(
67 opt => event.affectsConfiguration(opt) 85 opt => event.affectsConfiguration(opt)
68 ); 86 );
69 87
@@ -75,7 +93,7 @@ export class Config {
75 ); 93 );
76 94
77 if (userResponse === "Reload now") { 95 if (userResponse === "Reload now") {
78 vscode.commands.executeCommand("workbench.action.reloadWindow"); 96 await vscodeReloadWindow();
79 } 97 }
80 } 98 }
81 99
@@ -121,8 +139,14 @@ export class Config {
121 } 139 }
122 } 140 }
123 141
142 get installedExtensionUpdateChannel(): UpdatesChannel {
143 return this.extensionReleaseTag === NIGHTLY_TAG
144 ? UpdatesChannel.Nightly
145 : UpdatesChannel.Stable;
146 }
147
124 get serverSource(): null | ArtifactSource { 148 get serverSource(): null | ArtifactSource {
125 const serverPath = RA_LSP_DEBUG ?? this.cfg.get<null | string>("serverPath"); 149 const serverPath = RA_LSP_DEBUG ?? this.serverPath;
126 150
127 if (serverPath) { 151 if (serverPath) {
128 return { 152 return {
@@ -135,13 +159,18 @@ export class Config {
135 159
136 if (!prebuiltBinaryName) return null; 160 if (!prebuiltBinaryName) return null;
137 161
162 return this.createGithubReleaseSource(
163 prebuiltBinaryName,
164 this.extensionReleaseTag
165 );
166 }
167
168 private createGithubReleaseSource(file: string, tag: string): ArtifactSource.GithubRelease {
138 return { 169 return {
139 type: ArtifactSource.Type.GithubRelease, 170 type: ArtifactSource.Type.GithubRelease,
171 file,
172 tag,
140 dir: this.ctx.globalStoragePath, 173 dir: this.ctx.globalStoragePath,
141 file: prebuiltBinaryName,
142 storage: this.ctx.globalState,
143 tag: Config.extensionVersion,
144 askBeforeDownload: this.cfg.get("updates.askBeforeDownload") as boolean,
145 repo: { 174 repo: {
146 name: "rust-analyzer", 175 name: "rust-analyzer",
147 owner: "rust-analyzer", 176 owner: "rust-analyzer",
@@ -149,9 +178,23 @@ export class Config {
149 }; 178 };
150 } 179 }
151 180
181 get nightlyVsixSource(): ArtifactSource.GithubRelease {
182 return this.createGithubReleaseSource("rust-analyzer.vsix", NIGHTLY_TAG);
183 }
184
185 readonly installedNightlyExtensionReleaseDate = new DateStorage(
186 "installed-nightly-extension-release-date",
187 this.ctx.globalState
188 );
189 readonly serverReleaseDate = new DateStorage("server-release-date", this.ctx.globalState);
190 readonly serverReleaseTag = new Storage<null | string>("server-release-tag", this.ctx.globalState, null);
191
152 // We don't do runtime config validation here for simplicity. More on stackoverflow: 192 // We don't do runtime config validation here for simplicity. More on stackoverflow:
153 // https://stackoverflow.com/questions/60135780/what-is-the-best-way-to-type-check-the-configuration-for-vscode-extension 193 // https://stackoverflow.com/questions/60135780/what-is-the-best-way-to-type-check-the-configuration-for-vscode-extension
154 194
195 private get serverPath() { return this.cfg.get("serverPath") as null | string; }
196 get updatesChannel() { return this.cfg.get("updates.channel") as UpdatesChannel; }
197 get askBeforeDownload() { return this.cfg.get("updates.askBeforeDownload") as boolean; }
155 get highlightingSemanticTokens() { return this.cfg.get("highlighting.semanticTokens") as boolean; } 198 get highlightingSemanticTokens() { return this.cfg.get("highlighting.semanticTokens") as boolean; }
156 get highlightingOn() { return this.cfg.get("highlightingOn") as boolean; } 199 get highlightingOn() { return this.cfg.get("highlightingOn") as boolean; }
157 get rainbowHighlightingOn() { return this.cfg.get("rainbowHighlightingOn") as boolean; } 200 get rainbowHighlightingOn() { return this.cfg.get("rainbowHighlightingOn") as boolean; }
@@ -189,3 +232,37 @@ export class Config {
189 // for internal use 232 // for internal use
190 get withSysroot() { return this.cfg.get("withSysroot", true) as boolean; } 233 get withSysroot() { return this.cfg.get("withSysroot", true) as boolean; }
191} 234}
235
236export class Storage<T> {
237 constructor(
238 private readonly key: string,
239 private readonly storage: vscode.Memento,
240 private readonly defaultVal: T
241 ) { }
242
243 get(): T {
244 const val = this.storage.get(this.key, this.defaultVal);
245 log.debug(this.key, "==", val);
246 return val;
247 }
248 async set(val: T) {
249 log.debug(this.key, "=", val);
250 await this.storage.update(this.key, val);
251 }
252}
253export class DateStorage {
254 inner: Storage<null | string>;
255
256 constructor(key: string, storage: vscode.Memento) {
257 this.inner = new Storage(key, storage, null);
258 }
259
260 get(): null | Date {
261 const dateStr = this.inner.get();
262 return dateStr ? new Date(dateStr) : null;
263 }
264
265 async set(date: null | Date) {
266 await this.inner.set(date ? date.toString() : null);
267 }
268}
diff --git a/editors/code/src/installation/download_artifact.ts b/editors/code/src/installation/download_artifact.ts
deleted file mode 100644
index 97e4d67c2..000000000
--- a/editors/code/src/installation/download_artifact.ts
+++ /dev/null
@@ -1,50 +0,0 @@
1import * as vscode from "vscode";
2import * as path from "path";
3import { promises as fs } from "fs";
4
5import { ArtifactReleaseInfo } from "./interfaces";
6import { downloadFile } from "./download_file";
7import { assert } from "../util";
8
9/**
10 * Downloads artifact from given `downloadUrl`.
11 * Creates `installationDir` if it is not yet created and put the artifact under
12 * `artifactFileName`.
13 * Displays info about the download progress in an info message printing the name
14 * of the artifact as `displayName`.
15 */
16export async function downloadArtifact(
17 { downloadUrl, releaseName }: ArtifactReleaseInfo,
18 artifactFileName: string,
19 installationDir: string,
20 displayName: string,
21) {
22 await fs.mkdir(installationDir).catch(err => assert(
23 err?.code === "EEXIST",
24 `Couldn't create directory "${installationDir}" to download ` +
25 `${artifactFileName} artifact: ${err?.message}`
26 ));
27
28 const installationPath = path.join(installationDir, artifactFileName);
29
30 await vscode.window.withProgress(
31 {
32 location: vscode.ProgressLocation.Notification,
33 cancellable: false, // FIXME: add support for canceling download?
34 title: `Downloading ${displayName} (${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, (readBytes, totalBytes) => {
40 const newPercentage = (readBytes / totalBytes) * 100;
41 progress.report({
42 message: newPercentage.toFixed(0) + "%",
43 increment: newPercentage - lastPrecentage
44 });
45
46 lastPrecentage = newPercentage;
47 });
48 }
49 );
50}
diff --git a/editors/code/src/installation/download_file.ts b/editors/code/src/installation/downloads.ts
index ee8949d61..7ce2e2960 100644
--- a/editors/code/src/installation/download_file.ts
+++ b/editors/code/src/installation/downloads.ts
@@ -1,8 +1,11 @@
1import fetch from "node-fetch"; 1import fetch from "node-fetch";
2import * as vscode from "vscode";
3import * as path from "path";
2import * as fs from "fs"; 4import * as fs from "fs";
3import * as stream from "stream"; 5import * as stream from "stream";
4import * as util from "util"; 6import * as util from "util";
5import { log, assert } from "../util"; 7import { log, assert } from "../util";
8import { ArtifactReleaseInfo } from "./interfaces";
6 9
7const pipeline = util.promisify(stream.pipeline); 10const pipeline = util.promisify(stream.pipeline);
8 11
@@ -49,3 +52,46 @@ export async function downloadFile(
49 // Issue at nodejs repo: https://github.com/nodejs/node/issues/31776 52 // Issue at nodejs repo: https://github.com/nodejs/node/issues/31776
50 }); 53 });
51} 54}
55
56/**
57 * Downloads artifact from given `downloadUrl`.
58 * Creates `installationDir` if it is not yet created and puts the artifact under
59 * `artifactFileName`.
60 * Displays info about the download progress in an info message printing the name
61 * of the artifact as `displayName`.
62 */
63export async function downloadArtifactWithProgressUi(
64 { downloadUrl, releaseName }: ArtifactReleaseInfo,
65 artifactFileName: string,
66 installationDir: string,
67 displayName: string,
68) {
69 await fs.promises.mkdir(installationDir).catch(err => assert(
70 err?.code === "EEXIST",
71 `Couldn't create directory "${installationDir}" to download ` +
72 `${artifactFileName} artifact: ${err?.message}`
73 ));
74
75 const installationPath = path.join(installationDir, artifactFileName);
76
77 await vscode.window.withProgress(
78 {
79 location: vscode.ProgressLocation.Notification,
80 cancellable: false, // FIXME: add support for canceling download?
81 title: `Downloading rust-analyzer ${displayName} (${releaseName})`
82 },
83 async (progress, _cancellationToken) => {
84 let lastPrecentage = 0;
85 const filePermissions = 0o755; // (rwx, r_x, r_x)
86 await downloadFile(downloadUrl, installationPath, filePermissions, (readBytes, totalBytes) => {
87 const newPercentage = (readBytes / totalBytes) * 100;
88 progress.report({
89 message: newPercentage.toFixed(0) + "%",
90 increment: newPercentage - lastPrecentage
91 });
92
93 lastPrecentage = newPercentage;
94 });
95 }
96 );
97}
diff --git a/editors/code/src/installation/extension.ts b/editors/code/src/installation/extension.ts
new file mode 100644
index 000000000..eea6fded2
--- /dev/null
+++ b/editors/code/src/installation/extension.ts
@@ -0,0 +1,144 @@
1import * as vscode from "vscode";
2import * as path from "path";
3import { promises as fs } from 'fs';
4
5import { vscodeReinstallExtension, vscodeReloadWindow, log, vscodeInstallExtensionFromVsix, assert, notReentrant } from "../util";
6import { Config, UpdatesChannel } from "../config";
7import { ArtifactReleaseInfo, ArtifactSource } from "./interfaces";
8import { downloadArtifactWithProgressUi } from "./downloads";
9import { fetchArtifactReleaseInfo } from "./fetch_artifact_release_info";
10
11const HEURISTIC_NIGHTLY_RELEASE_PERIOD_IN_HOURS = 25;
12
13/**
14 * Installs `stable` or latest `nightly` version or does nothing if the current
15 * extension version is what's needed according to `desiredUpdateChannel`.
16 */
17export async function ensureProperExtensionVersion(config: Config): Promise<never | void> {
18 // User has built lsp server from sources, she should manage updates manually
19 if (config.serverSource?.type === ArtifactSource.Type.ExplicitPath) return;
20
21 const currentUpdChannel = config.installedExtensionUpdateChannel;
22 const desiredUpdChannel = config.updatesChannel;
23
24 if (currentUpdChannel === UpdatesChannel.Stable) {
25 // Release date is present only when we are on nightly
26 await config.installedNightlyExtensionReleaseDate.set(null);
27 }
28
29 if (desiredUpdChannel === UpdatesChannel.Stable) {
30 // VSCode should handle updates for stable channel
31 if (currentUpdChannel === UpdatesChannel.Stable) return;
32
33 if (!await askToDownloadProperExtensionVersion(config)) return;
34
35 await vscodeReinstallExtension(config.extensionId);
36 await vscodeReloadWindow(); // never returns
37 }
38
39 if (currentUpdChannel === UpdatesChannel.Stable) {
40 if (!await askToDownloadProperExtensionVersion(config)) return;
41
42 return await tryDownloadNightlyExtension(config);
43 }
44
45 const currentExtReleaseDate = config.installedNightlyExtensionReleaseDate.get();
46
47 if (currentExtReleaseDate === null) {
48 void vscode.window.showErrorMessage(
49 "Nightly release date must've been set during the installation. " +
50 "Did you download and install the nightly .vsix package manually?"
51 );
52 throw new Error("Nightly release date was not set in globalStorage");
53 }
54
55 const dateNow = new Date;
56 const hoursSinceLastUpdate = diffInHours(currentExtReleaseDate, dateNow);
57 log.debug(
58 "Current rust-analyzer nightly was downloaded", hoursSinceLastUpdate,
59 "hours ago, namely:", currentExtReleaseDate, "and now is", dateNow
60 );
61
62 if (hoursSinceLastUpdate < HEURISTIC_NIGHTLY_RELEASE_PERIOD_IN_HOURS) {
63 return;
64 }
65 if (!await askToDownloadProperExtensionVersion(config, "The installed nightly version is most likely outdated. ")) {
66 return;
67 }
68
69 await tryDownloadNightlyExtension(config, releaseInfo => {
70 assert(
71 currentExtReleaseDate.getTime() === config.installedNightlyExtensionReleaseDate.get()?.getTime(),
72 "Other active VSCode instance has reinstalled the extension"
73 );
74
75 if (releaseInfo.releaseDate.getTime() === currentExtReleaseDate.getTime()) {
76 vscode.window.showInformationMessage(
77 "Whoops, it appears that your nightly version is up-to-date. " +
78 "There might be some problems with the upcomming nightly release " +
79 "or you traveled too far into the future. Sorry for that 😅! "
80 );
81 return false;
82 }
83 return true;
84 });
85}
86
87async function askToDownloadProperExtensionVersion(config: Config, reason = "") {
88 if (!config.askBeforeDownload) return true;
89
90 const stableOrNightly = config.updatesChannel === UpdatesChannel.Stable ? "stable" : "latest nightly";
91
92 // In case of reentering this function and showing the same info message
93 // (e.g. after we had shown this message, the user changed the config)
94 // vscode will dismiss the already shown one (i.e. return undefined).
95 // This behaviour is what we want, but likely it is not documented
96
97 const userResponse = await vscode.window.showInformationMessage(
98 reason + `Do you want to download the ${stableOrNightly} rust-analyzer extension ` +
99 `version and reload the window now?`,
100 "Download now", "Cancel"
101 );
102 return userResponse === "Download now";
103}
104
105/**
106 * Shutdowns the process in case of success (i.e. reloads the window) or throws an error.
107 *
108 * ACHTUNG!: this function has a crazy amount of state transitions, handling errors during
109 * each of them would result in a ton of code (especially accounting for cross-process
110 * shared mutable `globalState` access). Enforcing no reentrancy for this is best-effort.
111 */
112const tryDownloadNightlyExtension = notReentrant(async (
113 config: Config,
114 shouldDownload: (releaseInfo: ArtifactReleaseInfo) => boolean = () => true
115): Promise<never | void> => {
116 const vsixSource = config.nightlyVsixSource;
117 try {
118 const releaseInfo = await fetchArtifactReleaseInfo(vsixSource.repo, vsixSource.file, vsixSource.tag);
119
120 if (!shouldDownload(releaseInfo)) return;
121
122 await downloadArtifactWithProgressUi(releaseInfo, vsixSource.file, vsixSource.dir, "nightly extension");
123
124 const vsixPath = path.join(vsixSource.dir, vsixSource.file);
125
126 await vscodeInstallExtensionFromVsix(vsixPath);
127 await config.installedNightlyExtensionReleaseDate.set(releaseInfo.releaseDate);
128 await fs.unlink(vsixPath);
129
130 await vscodeReloadWindow(); // never returns
131 } catch (err) {
132 log.downloadError(err, "nightly extension", vsixSource.repo.name);
133 }
134});
135
136function diffInHours(a: Date, b: Date): number {
137 // Discard the time and time-zone information (to abstract from daylight saving time bugs)
138 // https://stackoverflow.com/a/15289883/9259330
139
140 const utcA = Date.UTC(a.getFullYear(), a.getMonth(), a.getDate());
141 const utcB = Date.UTC(b.getFullYear(), b.getMonth(), b.getDate());
142
143 return (utcA - utcB) / (1000 * 60 * 60);
144}
diff --git a/editors/code/src/installation/fetch_artifact_release_info.ts b/editors/code/src/installation/fetch_artifact_release_info.ts
index b1b5a3485..1ad3b8338 100644
--- a/editors/code/src/installation/fetch_artifact_release_info.ts
+++ b/editors/code/src/installation/fetch_artifact_release_info.ts
@@ -59,12 +59,15 @@ export async function fetchArtifactReleaseInfo(
59 59
60 return { 60 return {
61 releaseName: release.name, 61 releaseName: release.name,
62 releaseDate: new Date(release.published_at),
62 downloadUrl: artifact.browser_download_url 63 downloadUrl: artifact.browser_download_url
63 }; 64 };
64 65
65 // We omit declaration of tremendous amount of fields that we are not using here 66 // We omit declaration of tremendous amount of fields that we are not using here
66 interface GithubRelease { 67 interface GithubRelease {
67 name: string; 68 name: string;
69 // eslint-disable-next-line camelcase
70 published_at: string;
68 assets: Array<{ 71 assets: Array<{
69 name: string; 72 name: string;
70 // eslint-disable-next-line camelcase 73 // eslint-disable-next-line camelcase
diff --git a/editors/code/src/installation/interfaces.ts b/editors/code/src/installation/interfaces.ts
index 50b635921..1a8ea0884 100644
--- a/editors/code/src/installation/interfaces.ts
+++ b/editors/code/src/installation/interfaces.ts
@@ -1,5 +1,3 @@
1import * as vscode from "vscode";
2
3export interface GithubRepo { 1export interface GithubRepo {
4 name: string; 2 name: string;
5 owner: string; 3 owner: string;
@@ -9,6 +7,7 @@ export interface GithubRepo {
9 * Metadata about particular artifact retrieved from GitHub releases. 7 * Metadata about particular artifact retrieved from GitHub releases.
10 */ 8 */
11export interface ArtifactReleaseInfo { 9export interface ArtifactReleaseInfo {
10 releaseDate: Date;
12 releaseName: string; 11 releaseName: string;
13 downloadUrl: string; 12 downloadUrl: string;
14} 13}
@@ -42,6 +41,9 @@ export namespace ArtifactSource {
42 */ 41 */
43 repo: GithubRepo; 42 repo: GithubRepo;
44 43
44
45 // FIXME: add installationPath: string;
46
45 /** 47 /**
46 * Directory on the filesystem where the bundled binary is stored. 48 * Directory on the filesystem where the bundled binary is stored.
47 */ 49 */
@@ -57,17 +59,5 @@ export namespace ArtifactSource {
57 * Tag of github release that denotes a version required by this extension. 59 * Tag of github release that denotes a version required by this extension.
58 */ 60 */
59 tag: string; 61 tag: 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;
66
67 /**
68 * Ask for the user permission before downloading the artifact.
69 */
70 askBeforeDownload: boolean;
71 } 62 }
72
73} 63}
diff --git a/editors/code/src/installation/server.ts b/editors/code/src/installation/server.ts
index ef1c45ff6..05730a778 100644
--- a/editors/code/src/installation/server.ts
+++ b/editors/code/src/installation/server.ts
@@ -1,14 +1,16 @@
1import * as vscode from "vscode"; 1import * as vscode from "vscode";
2import * as path from "path"; 2import * as path from "path";
3import { promises as dns } from "dns";
4import { spawnSync } from "child_process"; 3import { spawnSync } from "child_process";
5 4
6import { ArtifactSource } from "./interfaces"; 5import { ArtifactSource } from "./interfaces";
7import { fetchArtifactReleaseInfo } from "./fetch_artifact_release_info"; 6import { fetchArtifactReleaseInfo } from "./fetch_artifact_release_info";
8import { downloadArtifact } from "./download_artifact"; 7import { downloadArtifactWithProgressUi } from "./downloads";
9import { log, assert } from "../util"; 8import { log, assert, notReentrant } from "../util";
9import { Config, NIGHTLY_TAG } from "../config";
10
11export async function ensureServerBinary(config: Config): Promise<null | string> {
12 const source = config.serverSource;
10 13
11export async function ensureServerBinary(source: null | ArtifactSource): Promise<null | string> {
12 if (!source) { 14 if (!source) {
13 vscode.window.showErrorMessage( 15 vscode.window.showErrorMessage(
14 "Unfortunately we don't ship binaries for your platform yet. " + 16 "Unfortunately we don't ship binaries for your platform yet. " +
@@ -35,18 +37,11 @@ export async function ensureServerBinary(source: null | ArtifactSource): Promise
35 return null; 37 return null;
36 } 38 }
37 case ArtifactSource.Type.GithubRelease: { 39 case ArtifactSource.Type.GithubRelease: {
38 const prebuiltBinaryPath = path.join(source.dir, source.file); 40 if (!shouldDownloadServer(source, config)) {
39 41 return path.join(source.dir, source.file);
40 const installedVersion: null | string = getServerVersion(source.storage);
41 const requiredVersion: string = source.tag;
42
43 log.debug("Installed version:", installedVersion, "required:", requiredVersion);
44
45 if (isBinaryAvailable(prebuiltBinaryPath) && installedVersion === requiredVersion) {
46 return prebuiltBinaryPath;
47 } 42 }
48 43
49 if (source.askBeforeDownload) { 44 if (config.askBeforeDownload) {
50 const userResponse = await vscode.window.showInformationMessage( 45 const userResponse = await vscode.window.showInformationMessage(
51 `Language server version ${source.tag} for rust-analyzer is not installed. ` + 46 `Language server version ${source.tag} for rust-analyzer is not installed. ` +
52 "Do you want to download it now?", 47 "Do you want to download it now?",
@@ -55,38 +50,56 @@ export async function ensureServerBinary(source: null | ArtifactSource): Promise
55 if (userResponse !== "Download now") return null; 50 if (userResponse !== "Download now") return null;
56 } 51 }
57 52
58 if (!await downloadServer(source)) return null; 53 return await downloadServer(source, config);
59
60 return prebuiltBinaryPath;
61 } 54 }
62 } 55 }
63} 56}
64 57
65async function downloadServer(source: ArtifactSource.GithubRelease): Promise<boolean> { 58function shouldDownloadServer(
59 source: ArtifactSource.GithubRelease,
60 config: Config
61): boolean {
62 if (!isBinaryAvailable(path.join(source.dir, source.file))) return true;
63
64 const installed = {
65 tag: config.serverReleaseTag.get(),
66 date: config.serverReleaseDate.get()
67 };
68 const required = {
69 tag: source.tag,
70 date: config.installedNightlyExtensionReleaseDate.get()
71 };
72
73 log.debug("Installed server:", installed, "required:", required);
74
75 if (required.tag !== NIGHTLY_TAG || installed.tag !== NIGHTLY_TAG) {
76 return required.tag !== installed.tag;
77 }
78
79 assert(required.date !== null, "Extension release date should have been saved during its installation");
80 assert(installed.date !== null, "Server release date should have been saved during its installation");
81
82 return installed.date.getTime() !== required.date.getTime();
83}
84
85/**
86 * Enforcing no reentrancy for this is best-effort.
87 */
88const downloadServer = notReentrant(async (
89 source: ArtifactSource.GithubRelease,
90 config: Config,
91): Promise<null | string> => {
66 try { 92 try {
67 const releaseInfo = await fetchArtifactReleaseInfo(source.repo, source.file, source.tag); 93 const releaseInfo = await fetchArtifactReleaseInfo(source.repo, source.file, source.tag);
68 94
69 await downloadArtifact(releaseInfo, source.file, source.dir, "language server"); 95 await downloadArtifactWithProgressUi(releaseInfo, source.file, source.dir, "language server");
70 await setServerVersion(source.storage, releaseInfo.releaseName); 96 await Promise.all([
97 config.serverReleaseTag.set(releaseInfo.releaseName),
98 config.serverReleaseDate.set(releaseInfo.releaseDate)
99 ]);
71 } catch (err) { 100 } catch (err) {
72 vscode.window.showErrorMessage( 101 log.downloadError(err, "language server", source.repo.name);
73 `Failed to download language server from ${source.repo.name} ` + 102 return null;
74 `GitHub repository: ${err.message}`
75 );
76
77 log.error(err);
78
79 dns.resolve('example.com').then(
80 addrs => log.debug("DNS resolution for example.com was successful", addrs),
81 err => {
82 log.error(
83 "DNS resolution for example.com failed, " +
84 "there might be an issue with Internet availability"
85 );
86 log.error(err);
87 }
88 );
89 return false;
90 } 103 }
91 104
92 const binaryPath = path.join(source.dir, source.file); 105 const binaryPath = path.join(source.dir, source.file);
@@ -101,8 +114,8 @@ async function downloadServer(source: ArtifactSource.GithubRelease): Promise<boo
101 "Rust analyzer language server was successfully installed 🦀" 114 "Rust analyzer language server was successfully installed 🦀"
102 ); 115 );
103 116
104 return true; 117 return binaryPath;
105} 118});
106 119
107function isBinaryAvailable(binaryPath: string): boolean { 120function isBinaryAvailable(binaryPath: string): boolean {
108 const res = spawnSync(binaryPath, ["--version"]); 121 const res = spawnSync(binaryPath, ["--version"]);
@@ -115,14 +128,3 @@ function isBinaryAvailable(binaryPath: string): boolean {
115 128
116 return res.status === 0; 129 return res.status === 0;
117} 130}
118
119function getServerVersion(storage: vscode.Memento): null | string {
120 const version = storage.get<null | string>("server-version", null);
121 log.debug("Get server-version:", version);
122 return version;
123}
124
125async function setServerVersion(storage: vscode.Memento, version: string): Promise<void> {
126 log.debug("Set server-version:", version);
127 await storage.update("server-version", version.toString());
128}
diff --git a/editors/code/src/main.ts b/editors/code/src/main.ts
index e01c89cc7..bd4661a36 100644
--- a/editors/code/src/main.ts
+++ b/editors/code/src/main.ts
@@ -8,6 +8,7 @@ import { activateHighlighting } from './highlighting';
8import { ensureServerBinary } from './installation/server'; 8import { ensureServerBinary } from './installation/server';
9import { Config } from './config'; 9import { Config } from './config';
10import { log } from './util'; 10import { log } from './util';
11import { ensureProperExtensionVersion } from './installation/extension';
11 12
12let ctx: Ctx | undefined; 13let ctx: Ctx | undefined;
13 14
@@ -34,7 +35,13 @@ export async function activate(context: vscode.ExtensionContext) {
34 35
35 const config = new Config(context); 36 const config = new Config(context);
36 37
37 const serverPath = await ensureServerBinary(config.serverSource); 38 vscode.workspace.onDidChangeConfiguration(() => ensureProperExtensionVersion(config).catch(log.error));
39
40 // Don't await the user response here, otherwise we will block the lsp server bootstrap
41 void ensureProperExtensionVersion(config).catch(log.error);
42
43 const serverPath = await ensureServerBinary(config);
44
38 if (serverPath == null) { 45 if (serverPath == null) {
39 throw new Error( 46 throw new Error(
40 "Rust Analyzer Language Server is not available. " + 47 "Rust Analyzer Language Server is not available. " +
diff --git a/editors/code/src/util.ts b/editors/code/src/util.ts
index 95a5f1227..2bfc145e6 100644
--- a/editors/code/src/util.ts
+++ b/editors/code/src/util.ts
@@ -1,5 +1,6 @@
1import * as lc from "vscode-languageclient"; 1import * as lc from "vscode-languageclient";
2import * as vscode from "vscode"; 2import * as vscode from "vscode";
3import { promises as dns } from "dns";
3import { strict as nativeAssert } from "assert"; 4import { strict as nativeAssert } from "assert";
4 5
5export function assert(condition: boolean, explanation: string): asserts condition { 6export function assert(condition: boolean, explanation: string): asserts condition {
@@ -11,21 +12,40 @@ export function assert(condition: boolean, explanation: string): asserts conditi
11 } 12 }
12} 13}
13 14
14export const log = { 15export const log = new class {
15 enabled: true, 16 private enabled = true;
17
18 setEnabled(yes: boolean): void {
19 log.enabled = yes;
20 }
21
16 debug(message?: any, ...optionalParams: any[]): void { 22 debug(message?: any, ...optionalParams: any[]): void {
17 if (!log.enabled) return; 23 if (!log.enabled) return;
18 // eslint-disable-next-line no-console 24 // eslint-disable-next-line no-console
19 console.log(message, ...optionalParams); 25 console.log(message, ...optionalParams);
20 }, 26 }
27
21 error(message?: any, ...optionalParams: any[]): void { 28 error(message?: any, ...optionalParams: any[]): void {
22 if (!log.enabled) return; 29 if (!log.enabled) return;
23 debugger; 30 debugger;
24 // eslint-disable-next-line no-console 31 // eslint-disable-next-line no-console
25 console.error(message, ...optionalParams); 32 console.error(message, ...optionalParams);
26 }, 33 }
27 setEnabled(yes: boolean): void { 34
28 log.enabled = yes; 35 downloadError(err: Error, artifactName: string, repoName: string) {
36 vscode.window.showErrorMessage(
37 `Failed to download the rust-analyzer ${artifactName} from ${repoName} ` +
38 `GitHub repository: ${err.message}`
39 );
40 log.error(err);
41 dns.resolve('example.com').then(
42 addrs => log.debug("DNS resolution for example.com was successful", addrs),
43 err => log.error(
44 "DNS resolution for example.com failed, " +
45 "there might be an issue with Internet availability",
46 err
47 )
48 );
29 } 49 }
30}; 50};
31 51
@@ -66,6 +86,17 @@ function sleep(ms: number) {
66 return new Promise(resolve => setTimeout(resolve, ms)); 86 return new Promise(resolve => setTimeout(resolve, ms));
67} 87}
68 88
89export function notReentrant<TThis, TParams extends any[], TRet>(
90 fn: (this: TThis, ...params: TParams) => Promise<TRet>
91): typeof fn {
92 let entered = false;
93 return function(...params) {
94 assert(!entered, `Reentrancy invariant for ${fn.name} is violated`);
95 entered = true;
96 return fn.apply(this, params).finally(() => entered = false);
97 };
98}
99
69export type RustDocument = vscode.TextDocument & { languageId: "rust" }; 100export type RustDocument = vscode.TextDocument & { languageId: "rust" };
70export type RustEditor = vscode.TextEditor & { document: RustDocument; id: string }; 101export type RustEditor = vscode.TextEditor & { document: RustDocument; id: string };
71 102
@@ -79,3 +110,29 @@ export function isRustDocument(document: vscode.TextDocument): document is RustD
79export function isRustEditor(editor: vscode.TextEditor): editor is RustEditor { 110export function isRustEditor(editor: vscode.TextEditor): editor is RustEditor {
80 return isRustDocument(editor.document); 111 return isRustDocument(editor.document);
81} 112}
113
114/**
115 * @param extensionId The canonical extension identifier in the form of: `publisher.name`
116 */
117export async function vscodeReinstallExtension(extensionId: string) {
118 // Unfortunately there is no straightforward way as of now, these commands
119 // were found in vscode source code.
120
121 log.debug("Uninstalling extension", extensionId);
122 await vscode.commands.executeCommand("workbench.extensions.uninstallExtension", extensionId);
123 log.debug("Installing extension", extensionId);
124 await vscode.commands.executeCommand("workbench.extensions.installExtension", extensionId);
125}
126
127export async function vscodeReloadWindow(): Promise<never> {
128 await vscode.commands.executeCommand("workbench.action.reloadWindow");
129
130 assert(false, "unreachable");
131}
132
133export async function vscodeInstallExtensionFromVsix(vsixPath: string) {
134 await vscode.commands.executeCommand(
135 "workbench.extensions.installExtension",
136 vscode.Uri.file(vsixPath)
137 );
138}