diff options
author | bors[bot] <26634292+bors[bot]@users.noreply.github.com> | 2020-03-16 10:26:31 +0000 |
---|---|---|
committer | GitHub <[email protected]> | 2020-03-16 10:26:31 +0000 |
commit | 200c275c2e9955371e61f6ad7684084655df46fc (patch) | |
tree | c4b61de644cec37cffca9010d56afc4136d23ca8 /editors/code | |
parent | a99cac671c3e6105a0192acbb1a91cb83e453018 (diff) | |
parent | 5a0041c5aaeee49be84ce771fb0360ae55cbd8b2 (diff) |
Merge #3534
3534: Feature: vscode impl nightlies download and installation r=Veetaha a=Veetaha
I need to test things more, but the core shape of the code is quite well-formed.
The main problem is that we save the release date only for nightlies and there are no means to get the release date of the stable extension (i.e. for this we would need to consult the github releases via a network request, or we would need to somehow save this info into package.json or any other file accessible from the extension code during the deployment step, but this will be very hard I guess).
So there is an invariant that the users can install nightly only from our extension and they can't do it manually, because when installing the nightly `.vsix` we actually save its release date to `globalState`
Closes: #3402
TODO:
- [x] More manual tests and documentation
cc @matklad @lnicola
Co-authored-by: Veetaha <[email protected]>
Co-authored-by: Veetaha <[email protected]>
Diffstat (limited to 'editors/code')
-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 |
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 | ||
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 | } | ||