diff options
-rw-r--r-- | docs/user/readme.adoc | 19 | ||||
-rw-r--r-- | editors/code/package-lock.json | 2 | ||||
-rw-r--r-- | editors/code/package.json | 17 | ||||
-rw-r--r-- | editors/code/src/commands/server_version.ts | 3 | ||||
-rw-r--r-- | editors/code/src/config.ts | 117 | ||||
-rw-r--r-- | editors/code/src/installation/download_artifact.ts | 50 | ||||
-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.ts | 144 | ||||
-rw-r--r-- | editors/code/src/installation/fetch_artifact_release_info.ts | 3 | ||||
-rw-r--r-- | editors/code/src/installation/interfaces.ts | 18 | ||||
-rw-r--r-- | editors/code/src/installation/server.ts | 104 | ||||
-rw-r--r-- | editors/code/src/main.ts | 9 | ||||
-rw-r--r-- | editors/code/src/util.ts | 69 |
13 files changed, 454 insertions, 147 deletions
diff --git a/docs/user/readme.adoc b/docs/user/readme.adoc index 4e99dd0a6..2e6c6112f 100644 --- a/docs/user/readme.adoc +++ b/docs/user/readme.adoc | |||
@@ -65,6 +65,25 @@ Note that we only support the latest version of VS Code. | |||
65 | 65 | ||
66 | The extension will be updated automatically as new versions become available. It will ask your permission to download the matching language server version binary if needed. | 66 | The extension will be updated automatically as new versions become available. It will ask your permission to download the matching language server version binary if needed. |
67 | 67 | ||
68 | ===== Nightly | ||
69 | |||
70 | We ship nightly releases for VS Code. To help us out with testing the newest code and follow the bleeding edge of our `master`, please use the following config: | ||
71 | |||
72 | [source,json] | ||
73 | ---- | ||
74 | { "rust-analyzer.updates.channel": "nightly" } | ||
75 | ---- | ||
76 | |||
77 | You will be prompted to install the `nightly` extension version. Just click `Download now` and from that moment you will get automatic updates each 24 hours. | ||
78 | |||
79 | If you don't want to be asked for `Download now` every day when the new nightly version is released add the following to your `settings.json`: | ||
80 | [source,json] | ||
81 | ---- | ||
82 | { "rust-analyzer.updates.askBeforeDownload": false } | ||
83 | ---- | ||
84 | |||
85 | NOTE: Nightly extension should **only** be installed via the `Download now` action from VS Code. | ||
86 | |||
68 | ==== Building From Source | 87 | ==== Building From Source |
69 | 88 | ||
70 | Alternatively, both the server and the plugin can be installed from source: | 89 | Alternatively, both the server and the plugin can be installed from source: |
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 | ||
6 | export function serverVersion(ctx: Ctx): Cmd { | 6 | export 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 @@ | |||
1 | import * as os from "os"; | 1 | import * as os from "os"; |
2 | import * as vscode from 'vscode'; | 2 | import * as vscode from 'vscode'; |
3 | import { ArtifactSource } from "./installation/interfaces"; | 3 | import { ArtifactSource } from "./installation/interfaces"; |
4 | import { log } from "./util"; | 4 | import { log, vscodeReloadWindow } from "./util"; |
5 | 5 | ||
6 | const RA_LSP_DEBUG = process.env.__RA_LSP_SERVER_DEBUG; | 6 | const 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 | |||
27 | export const enum UpdatesChannel { | ||
28 | Stable = "stable", | ||
29 | Nightly = "nightly" | ||
30 | } | ||
31 | |||
32 | export const NIGHTLY_TAG = "nightly"; | ||
26 | export class Config { | 33 | export 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 | |||
236 | export 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 | } | ||
253 | export 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 @@ | |||
1 | import * as vscode from "vscode"; | ||
2 | import * as path from "path"; | ||
3 | import { promises as fs } from "fs"; | ||
4 | |||
5 | import { ArtifactReleaseInfo } from "./interfaces"; | ||
6 | import { downloadFile } from "./download_file"; | ||
7 | import { 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 | */ | ||
16 | export 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 @@ | |||
1 | import fetch from "node-fetch"; | 1 | import fetch from "node-fetch"; |
2 | import * as vscode from "vscode"; | ||
3 | import * as path from "path"; | ||
2 | import * as fs from "fs"; | 4 | import * as fs from "fs"; |
3 | import * as stream from "stream"; | 5 | import * as stream from "stream"; |
4 | import * as util from "util"; | 6 | import * as util from "util"; |
5 | import { log, assert } from "../util"; | 7 | import { log, assert } from "../util"; |
8 | import { ArtifactReleaseInfo } from "./interfaces"; | ||
6 | 9 | ||
7 | const pipeline = util.promisify(stream.pipeline); | 10 | const 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 | */ | ||
63 | export 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 @@ | |||
1 | import * as vscode from "vscode"; | ||
2 | import * as path from "path"; | ||
3 | import { promises as fs } from 'fs'; | ||
4 | |||
5 | import { vscodeReinstallExtension, vscodeReloadWindow, log, vscodeInstallExtensionFromVsix, assert, notReentrant } from "../util"; | ||
6 | import { Config, UpdatesChannel } from "../config"; | ||
7 | import { ArtifactReleaseInfo, ArtifactSource } from "./interfaces"; | ||
8 | import { downloadArtifactWithProgressUi } from "./downloads"; | ||
9 | import { fetchArtifactReleaseInfo } from "./fetch_artifact_release_info"; | ||
10 | |||
11 | const 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 | */ | ||
17 | export 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 | |||
87 | async 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 | */ | ||
112 | const 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 | |||
136 | function 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 @@ | |||
1 | import * as vscode from "vscode"; | ||
2 | |||
3 | export interface GithubRepo { | 1 | export 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 | */ |
11 | export interface ArtifactReleaseInfo { | 9 | export 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 @@ | |||
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 { promises as dns } from "dns"; | ||
4 | import { spawnSync } from "child_process"; | 3 | import { spawnSync } from "child_process"; |
5 | 4 | ||
6 | import { ArtifactSource } from "./interfaces"; | 5 | import { ArtifactSource } from "./interfaces"; |
7 | import { fetchArtifactReleaseInfo } from "./fetch_artifact_release_info"; | 6 | import { fetchArtifactReleaseInfo } from "./fetch_artifact_release_info"; |
8 | import { downloadArtifact } from "./download_artifact"; | 7 | import { downloadArtifactWithProgressUi } from "./downloads"; |
9 | import { log, assert } from "../util"; | 8 | import { log, assert, notReentrant } from "../util"; |
9 | import { Config, NIGHTLY_TAG } from "../config"; | ||
10 | |||
11 | export async function ensureServerBinary(config: Config): Promise<null | string> { | ||
12 | const source = config.serverSource; | ||
10 | 13 | ||
11 | export 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 | ||
65 | async function downloadServer(source: ArtifactSource.GithubRelease): Promise<boolean> { | 58 | function 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 | */ | ||
88 | const 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 | ||
107 | function isBinaryAvailable(binaryPath: string): boolean { | 120 | function 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 | |||
119 | function 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 | |||
125 | async 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'; | |||
8 | import { ensureServerBinary } from './installation/server'; | 8 | import { ensureServerBinary } from './installation/server'; |
9 | import { Config } from './config'; | 9 | import { Config } from './config'; |
10 | import { log } from './util'; | 10 | import { log } from './util'; |
11 | import { ensureProperExtensionVersion } from './installation/extension'; | ||
11 | 12 | ||
12 | let ctx: Ctx | undefined; | 13 | let 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 @@ | |||
1 | import * as lc from "vscode-languageclient"; | 1 | import * as lc from "vscode-languageclient"; |
2 | import * as vscode from "vscode"; | 2 | import * as vscode from "vscode"; |
3 | import { promises as dns } from "dns"; | ||
3 | import { strict as nativeAssert } from "assert"; | 4 | import { strict as nativeAssert } from "assert"; |
4 | 5 | ||
5 | export function assert(condition: boolean, explanation: string): asserts condition { | 6 | export 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 | ||
14 | export const log = { | 15 | export 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 | ||
89 | export 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 | |||
69 | export type RustDocument = vscode.TextDocument & { languageId: "rust" }; | 100 | export type RustDocument = vscode.TextDocument & { languageId: "rust" }; |
70 | export type RustEditor = vscode.TextEditor & { document: RustDocument; id: string }; | 101 | export type RustEditor = vscode.TextEditor & { document: RustDocument; id: string }; |
71 | 102 | ||
@@ -79,3 +110,29 @@ export function isRustDocument(document: vscode.TextDocument): document is RustD | |||
79 | export function isRustEditor(editor: vscode.TextEditor): editor is RustEditor { | 110 | export 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 | */ | ||
117 | export 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 | |||
127 | export async function vscodeReloadWindow(): Promise<never> { | ||
128 | await vscode.commands.executeCommand("workbench.action.reloadWindow"); | ||
129 | |||
130 | assert(false, "unreachable"); | ||
131 | } | ||
132 | |||
133 | export async function vscodeInstallExtensionFromVsix(vsixPath: string) { | ||
134 | await vscode.commands.executeCommand( | ||
135 | "workbench.extensions.installExtension", | ||
136 | vscode.Uri.file(vsixPath) | ||
137 | ); | ||
138 | } | ||