aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorbors[bot] <26634292+bors[bot]@users.noreply.github.com>2020-03-19 08:06:48 +0000
committerGitHub <[email protected]>2020-03-19 08:06:48 +0000
commitaca3c3086ee99f5770a60970e20af640c895d42a (patch)
tree2f0b3233cc4728436ba5e47a6e91e7df33585d43
parent55336722b3662cbdcc9e1b92a3e27ed0442d2452 (diff)
parentfb6e655de8a44c65275ad45a27bf5bd684670ba0 (diff)
Merge #3629
3629: Alternative aproach to plugin auto update r=matklad a=matklad This is very much WIP (as in, I haven't run this once), but I like the result so far. cc @Veetaha The primary focus here on simplification: * local simplification of data structures and control-flow: using union of strings instead of an enum, using unwrapped GitHub API responses * global simplification of control flow: all logic is now in `main.ts`, implemented as linear functions without abstractions. This is stateful side-effective code, so arguments from [Carmack](http://number-none.com/blow/john_carmack_on_inlined_code.html) very much apply. We need all user interractions, all mutations, and all network requests to happen in a single file. * as a side-effect of condensing everything to functions, we can get rid of various enums. The enums were basically a reified control flow: ``` enum E { A, B } fn foo() -> E { if cond { E::A } else { E::B } } fn bar(e: E) { match e { E::A => do_a(), E::B => do_b(), } } ==>> fn all() { if cond { do_a() } else { do_b() } } ``` * simplification of model: we don't need to reinstall on settings update, we can just ask the user to reload, we don't need to handle nightly=>stable fallback, we can ask the user to reinstall extension, (todo) we don't need to parse out the date from the version, we can use build id for nightly and for stable we can write the info directly into package.json. Co-authored-by: Aleksey Kladov <[email protected]>
-rw-r--r--.vscode/launch.json1
-rw-r--r--editors/code/package.json2
-rw-r--r--editors/code/src/commands/server_version.ts16
-rw-r--r--editors/code/src/config.ts103
-rw-r--r--editors/code/src/ctx.ts9
-rw-r--r--editors/code/src/installation/extension.ts146
-rw-r--r--editors/code/src/installation/fetch_artifact_release_info.ts77
-rw-r--r--editors/code/src/installation/interfaces.ts63
-rw-r--r--editors/code/src/installation/server.ts131
-rw-r--r--editors/code/src/main.ts158
-rw-r--r--editors/code/src/net.ts (renamed from editors/code/src/installation/downloads.ts)132
-rw-r--r--editors/code/src/persistent_state.ts64
-rw-r--r--editors/code/src/util.ts54
13 files changed, 265 insertions, 691 deletions
diff --git a/.vscode/launch.json b/.vscode/launch.json
index 2e5c61735..ca70fb209 100644
--- a/.vscode/launch.json
+++ b/.vscode/launch.json
@@ -16,6 +16,7 @@
16 "request": "launch", 16 "request": "launch",
17 "runtimeExecutable": "${execPath}", 17 "runtimeExecutable": "${execPath}",
18 "args": [ 18 "args": [
19 // "--user-data-dir=${workspaceFolder}/target/code",
19 "--disable-extensions", 20 "--disable-extensions",
20 "--extensionDevelopmentPath=${workspaceFolder}/editors/code" 21 "--extensionDevelopmentPath=${workspaceFolder}/editors/code"
21 ], 22 ],
diff --git a/editors/code/package.json b/editors/code/package.json
index 84642d11c..150c36845 100644
--- a/editors/code/package.json
+++ b/editors/code/package.json
@@ -228,7 +228,7 @@
228 "default": "stable", 228 "default": "stable",
229 "markdownEnumDescriptions": [ 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", 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**" 231 "`\"nightly\"` updates are shipped daily (extension updates automatically by downloading artifacts directly from GitHub), 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 ], 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" 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 }, 234 },
diff --git a/editors/code/src/commands/server_version.ts b/editors/code/src/commands/server_version.ts
index 83b1acf67..03528b825 100644
--- a/editors/code/src/commands/server_version.ts
+++ b/editors/code/src/commands/server_version.ts
@@ -1,20 +1,10 @@
1import * as vscode from 'vscode'; 1import * as vscode from "vscode";
2import { ensureServerBinary } from '../installation/server'; 2import { spawnSync } from "child_process";
3import { Ctx, Cmd } from '../ctx'; 3import { Ctx, Cmd } from '../ctx';
4import { spawnSync } from 'child_process';
5 4
6export function serverVersion(ctx: Ctx): Cmd { 5export function serverVersion(ctx: Ctx): Cmd {
7 return async () => { 6 return async () => {
8 const binaryPath = await ensureServerBinary(ctx.config, ctx.state); 7 const version = spawnSync(ctx.serverPath, ["--version"], { encoding: "utf8" }).stdout;
9
10 if (binaryPath == null) {
11 throw new Error(
12 "Rust Analyzer Language Server is not available. " +
13 "Please, ensure its [proper installation](https://rust-analyzer.github.io/manual.html#installation)."
14 );
15 }
16
17 const version = spawnSync(binaryPath, ["--version"], { encoding: "utf8" }).stdout;
18 vscode.window.showInformationMessage('rust-analyzer version : ' + version); 8 vscode.window.showInformationMessage('rust-analyzer version : ' + version);
19 }; 9 };
20} 10}
diff --git a/editors/code/src/config.ts b/editors/code/src/config.ts
index b45b14bef..28698ab8e 100644
--- a/editors/code/src/config.ts
+++ b/editors/code/src/config.ts
@@ -1,9 +1,5 @@
1import * as os from "os";
2import * as vscode from 'vscode'; 1import * as vscode from 'vscode';
3import { ArtifactSource } from "./installation/interfaces"; 2import { log } from "./util";
4import { log, vscodeReloadWindow } from "./util";
5
6const RA_LSP_DEBUG = process.env.__RA_LSP_SERVER_DEBUG;
7 3
8export interface InlayHintOptions { 4export interface InlayHintOptions {
9 typeHints: boolean; 5 typeHints: boolean;
@@ -25,10 +21,7 @@ export interface CargoFeatures {
25 loadOutDirsFromCheck: boolean; 21 loadOutDirsFromCheck: boolean;
26} 22}
27 23
28export const enum UpdatesChannel { 24export type UpdatesChannel = "stable" | "nightly";
29 Stable = "stable",
30 Nightly = "nightly"
31}
32 25
33export const NIGHTLY_TAG = "nightly"; 26export const NIGHTLY_TAG = "nightly";
34export class Config { 27export class Config {
@@ -41,6 +34,7 @@ export class Config {
41 "cargo-watch", 34 "cargo-watch",
42 "highlighting.semanticTokens", 35 "highlighting.semanticTokens",
43 "inlayHints", 36 "inlayHints",
37 "updates.channel",
44 ] 38 ]
45 .map(opt => `${this.rootSection}.${opt}`); 39 .map(opt => `${this.rootSection}.${opt}`);
46 40
@@ -94,100 +88,17 @@ export class Config {
94 ); 88 );
95 89
96 if (userResponse === "Reload now") { 90 if (userResponse === "Reload now") {
97 await vscodeReloadWindow(); 91 await vscode.commands.executeCommand("workbench.action.reloadWindow");
98 } 92 }
99 } 93 }
100 94
101 private static replaceTildeWithHomeDir(path: string) { 95 get globalStoragePath(): string { return this.ctx.globalStoragePath; }
102 if (path.startsWith("~/")) {
103 return os.homedir() + path.slice("~".length);
104 }
105 return path;
106 }
107
108 /**
109 * Name of the binary artifact for `rust-analyzer` that is published for
110 * `platform` on GitHub releases. (It is also stored under the same name when
111 * downloaded by the extension).
112 */
113 get prebuiltServerFileName(): null | string {
114 // See possible `arch` values here:
115 // https://nodejs.org/api/process.html#process_process_arch
116
117 switch (process.platform) {
118
119 case "linux": {
120 switch (process.arch) {
121 case "arm":
122 case "arm64": return null;
123
124 default: return "rust-analyzer-linux";
125 }
126 }
127
128 case "darwin": return "rust-analyzer-mac";
129 case "win32": return "rust-analyzer-windows.exe";
130
131 // Users on these platforms yet need to manually build from sources
132 case "aix":
133 case "android":
134 case "freebsd":
135 case "openbsd":
136 case "sunos":
137 case "cygwin":
138 case "netbsd": return null;
139 // The list of platforms is exhaustive (see `NodeJS.Platform` type definition)
140 }
141 }
142
143 get installedExtensionUpdateChannel(): UpdatesChannel {
144 return this.extensionReleaseTag === NIGHTLY_TAG
145 ? UpdatesChannel.Nightly
146 : UpdatesChannel.Stable;
147 }
148
149 get serverSource(): null | ArtifactSource {
150 const serverPath = RA_LSP_DEBUG ?? this.serverPath;
151
152 if (serverPath) {
153 return {
154 type: ArtifactSource.Type.ExplicitPath,
155 path: Config.replaceTildeWithHomeDir(serverPath)
156 };
157 }
158
159 const prebuiltBinaryName = this.prebuiltServerFileName;
160
161 if (!prebuiltBinaryName) return null;
162
163 return this.createGithubReleaseSource(
164 prebuiltBinaryName,
165 this.extensionReleaseTag
166 );
167 }
168
169 private createGithubReleaseSource(file: string, tag: string): ArtifactSource.GithubRelease {
170 return {
171 type: ArtifactSource.Type.GithubRelease,
172 file,
173 tag,
174 dir: this.ctx.globalStoragePath,
175 repo: {
176 name: "rust-analyzer",
177 owner: "rust-analyzer",
178 }
179 };
180 }
181
182 get nightlyVsixSource(): ArtifactSource.GithubRelease {
183 return this.createGithubReleaseSource("rust-analyzer.vsix", NIGHTLY_TAG);
184 }
185 96
186 // We don't do runtime config validation here for simplicity. More on stackoverflow: 97 // We don't do runtime config validation here for simplicity. More on stackoverflow:
187 // https://stackoverflow.com/questions/60135780/what-is-the-best-way-to-type-check-the-configuration-for-vscode-extension 98 // https://stackoverflow.com/questions/60135780/what-is-the-best-way-to-type-check-the-configuration-for-vscode-extension
188 99
189 private get serverPath() { return this.cfg.get("serverPath") as null | string; } 100 get serverPath() { return this.cfg.get("serverPath") as null | string; }
190 get updatesChannel() { return this.cfg.get("updates.channel") as UpdatesChannel; } 101 get channel() { return this.cfg.get<"stable" | "nightly">("updates.channel")!; }
191 get askBeforeDownload() { return this.cfg.get("updates.askBeforeDownload") as boolean; } 102 get askBeforeDownload() { return this.cfg.get("updates.askBeforeDownload") as boolean; }
192 get highlightingSemanticTokens() { return this.cfg.get("highlighting.semanticTokens") as boolean; } 103 get highlightingSemanticTokens() { return this.cfg.get("highlighting.semanticTokens") as boolean; }
193 get highlightingOn() { return this.cfg.get("highlightingOn") as boolean; } 104 get highlightingOn() { return this.cfg.get("highlightingOn") as boolean; }
diff --git a/editors/code/src/ctx.ts b/editors/code/src/ctx.ts
index c929ab063..84c170ea8 100644
--- a/editors/code/src/ctx.ts
+++ b/editors/code/src/ctx.ts
@@ -4,21 +4,20 @@ import * as lc from 'vscode-languageclient';
4import { Config } from './config'; 4import { Config } from './config';
5import { createClient } from './client'; 5import { createClient } from './client';
6import { isRustEditor, RustEditor } from './util'; 6import { isRustEditor, RustEditor } from './util';
7import { PersistentState } from './persistent_state';
8 7
9export class Ctx { 8export class Ctx {
10 private constructor( 9 private constructor(
11 readonly config: Config, 10 readonly config: Config,
12 readonly state: PersistentState,
13 private readonly extCtx: vscode.ExtensionContext, 11 private readonly extCtx: vscode.ExtensionContext,
14 readonly client: lc.LanguageClient 12 readonly client: lc.LanguageClient,
13 readonly serverPath: string,
15 ) { 14 ) {
16 15
17 } 16 }
18 17
19 static async create(config: Config, state: PersistentState, extCtx: vscode.ExtensionContext, serverPath: string): Promise<Ctx> { 18 static async create(config: Config, extCtx: vscode.ExtensionContext, serverPath: string): Promise<Ctx> {
20 const client = await createClient(config, serverPath); 19 const client = await createClient(config, serverPath);
21 const res = new Ctx(config, state, extCtx, client); 20 const res = new Ctx(config, extCtx, client, serverPath);
22 res.pushCleanup(client.start()); 21 res.pushCleanup(client.start());
23 await client.onReady(); 22 await client.onReady();
24 return res; 23 return res;
diff --git a/editors/code/src/installation/extension.ts b/editors/code/src/installation/extension.ts
deleted file mode 100644
index a1db96f05..000000000
--- a/editors/code/src/installation/extension.ts
+++ /dev/null
@@ -1,146 +0,0 @@
1import * as vscode from "vscode";
2import * as path from "path";
3import { promises as fs } from 'fs';
4
5import { vscodeReinstallExtension, vscodeReloadWindow, log, vscodeInstallExtensionFromVsix, assert, notReentrant } from "../util";
6import { Config, UpdatesChannel } from "../config";
7import { ArtifactReleaseInfo, ArtifactSource } from "./interfaces";
8import { downloadArtifactWithProgressUi } from "./downloads";
9import { fetchArtifactReleaseInfo } from "./fetch_artifact_release_info";
10import { PersistentState } from "../persistent_state";
11
12const HEURISTIC_NIGHTLY_RELEASE_PERIOD_IN_HOURS = 25;
13
14/**
15 * Installs `stable` or latest `nightly` version or does nothing if the current
16 * extension version is what's needed according to `desiredUpdateChannel`.
17 */
18export async function ensureProperExtensionVersion(config: Config, state: PersistentState): Promise<never | void> {
19 // User has built lsp server from sources, she should manage updates manually
20 if (config.serverSource?.type === ArtifactSource.Type.ExplicitPath) return;
21
22 const currentUpdChannel = config.installedExtensionUpdateChannel;
23 const desiredUpdChannel = config.updatesChannel;
24
25 if (currentUpdChannel === UpdatesChannel.Stable) {
26 // Release date is present only when we are on nightly
27 await state.installedNightlyExtensionReleaseDate.set(null);
28 }
29
30 if (desiredUpdChannel === UpdatesChannel.Stable) {
31 // VSCode should handle updates for stable channel
32 if (currentUpdChannel === UpdatesChannel.Stable) return;
33
34 if (!await askToDownloadProperExtensionVersion(config)) return;
35
36 await vscodeReinstallExtension(config.extensionId);
37 await vscodeReloadWindow(); // never returns
38 }
39
40 if (currentUpdChannel === UpdatesChannel.Stable) {
41 if (!await askToDownloadProperExtensionVersion(config)) return;
42
43 return await tryDownloadNightlyExtension(config, state);
44 }
45
46 const currentExtReleaseDate = state.installedNightlyExtensionReleaseDate.get();
47
48 if (currentExtReleaseDate === null) {
49 void vscode.window.showErrorMessage(
50 "Nightly release date must've been set during the installation. " +
51 "Did you download and install the nightly .vsix package manually?"
52 );
53 throw new Error("Nightly release date was not set in globalStorage");
54 }
55
56 const dateNow = new Date;
57 const hoursSinceLastUpdate = diffInHours(currentExtReleaseDate, dateNow);
58 log.debug(
59 "Current rust-analyzer nightly was downloaded", hoursSinceLastUpdate,
60 "hours ago, namely:", currentExtReleaseDate, "and now is", dateNow
61 );
62
63 if (hoursSinceLastUpdate < HEURISTIC_NIGHTLY_RELEASE_PERIOD_IN_HOURS) {
64 return;
65 }
66 if (!await askToDownloadProperExtensionVersion(config, "The installed nightly version is most likely outdated. ")) {
67 return;
68 }
69
70 await tryDownloadNightlyExtension(config, state, releaseInfo => {
71 assert(
72 currentExtReleaseDate.getTime() === state.installedNightlyExtensionReleaseDate.get()?.getTime(),
73 "Other active VSCode instance has reinstalled the extension"
74 );
75
76 if (releaseInfo.releaseDate.getTime() === currentExtReleaseDate.getTime()) {
77 vscode.window.showInformationMessage(
78 "Whoops, it appears that your nightly version is up-to-date. " +
79 "There might be some problems with the upcomming nightly release " +
80 "or you traveled too far into the future. Sorry for that 😅! "
81 );
82 return false;
83 }
84 return true;
85 });
86}
87
88async function askToDownloadProperExtensionVersion(config: Config, reason = "") {
89 if (!config.askBeforeDownload) return true;
90
91 const stableOrNightly = config.updatesChannel === UpdatesChannel.Stable ? "stable" : "latest nightly";
92
93 // In case of reentering this function and showing the same info message
94 // (e.g. after we had shown this message, the user changed the config)
95 // vscode will dismiss the already shown one (i.e. return undefined).
96 // This behaviour is what we want, but likely it is not documented
97
98 const userResponse = await vscode.window.showInformationMessage(
99 reason + `Do you want to download the ${stableOrNightly} rust-analyzer extension ` +
100 `version and reload the window now?`,
101 "Download now", "Cancel"
102 );
103 return userResponse === "Download now";
104}
105
106/**
107 * Shutdowns the process in case of success (i.e. reloads the window) or throws an error.
108 *
109 * ACHTUNG!: this function has a crazy amount of state transitions, handling errors during
110 * each of them would result in a ton of code (especially accounting for cross-process
111 * shared mutable `globalState` access). Enforcing no reentrancy for this is best-effort.
112 */
113const tryDownloadNightlyExtension = notReentrant(async (
114 config: Config,
115 state: PersistentState,
116 shouldDownload: (releaseInfo: ArtifactReleaseInfo) => boolean = () => true
117): Promise<never | void> => {
118 const vsixSource = config.nightlyVsixSource;
119 try {
120 const releaseInfo = await fetchArtifactReleaseInfo(vsixSource.repo, vsixSource.file, vsixSource.tag);
121
122 if (!shouldDownload(releaseInfo)) return;
123
124 await downloadArtifactWithProgressUi(releaseInfo, vsixSource.file, vsixSource.dir, "nightly extension");
125
126 const vsixPath = path.join(vsixSource.dir, vsixSource.file);
127
128 await vscodeInstallExtensionFromVsix(vsixPath);
129 await state.installedNightlyExtensionReleaseDate.set(releaseInfo.releaseDate);
130 await fs.unlink(vsixPath);
131
132 await vscodeReloadWindow(); // never returns
133 } catch (err) {
134 log.downloadError(err, "nightly extension", vsixSource.repo.name);
135 }
136});
137
138function diffInHours(a: Date, b: Date): number {
139 // Discard the time and time-zone information (to abstract from daylight saving time bugs)
140 // https://stackoverflow.com/a/15289883/9259330
141
142 const utcA = Date.UTC(a.getFullYear(), a.getMonth(), a.getDate());
143 const utcB = Date.UTC(b.getFullYear(), b.getMonth(), b.getDate());
144
145 return (utcA - utcB) / (1000 * 60 * 60);
146}
diff --git a/editors/code/src/installation/fetch_artifact_release_info.ts b/editors/code/src/installation/fetch_artifact_release_info.ts
deleted file mode 100644
index 1ad3b8338..000000000
--- a/editors/code/src/installation/fetch_artifact_release_info.ts
+++ /dev/null
@@ -1,77 +0,0 @@
1import fetch from "node-fetch";
2import { GithubRepo, ArtifactReleaseInfo } from "./interfaces";
3import { log } from "../util";
4
5const GITHUB_API_ENDPOINT_URL = "https://api.github.com";
6
7/**
8 * Fetches the release with `releaseTag` from GitHub `repo` and
9 * returns metadata about `artifactFileName` shipped with
10 * this release.
11 *
12 * @throws Error upon network failure or if no such repository, release, or artifact exists.
13 */
14export async function fetchArtifactReleaseInfo(
15 repo: GithubRepo,
16 artifactFileName: string,
17 releaseTag: string
18): Promise<ArtifactReleaseInfo> {
19
20 const repoOwner = encodeURIComponent(repo.owner);
21 const repoName = encodeURIComponent(repo.name);
22
23 const apiEndpointPath = `/repos/${repoOwner}/${repoName}/releases/tags/${releaseTag}`;
24
25 const requestUrl = GITHUB_API_ENDPOINT_URL + apiEndpointPath;
26
27 log.debug("Issuing request for released artifacts metadata to", requestUrl);
28
29 const response = await fetch(requestUrl, { headers: { Accept: "application/vnd.github.v3+json" } });
30
31 if (!response.ok) {
32 log.error("Error fetching artifact release info", {
33 requestUrl,
34 releaseTag,
35 artifactFileName,
36 response: {
37 headers: response.headers,
38 status: response.status,
39 body: await response.text(),
40 }
41 });
42
43 throw new Error(
44 `Got response ${response.status} when trying to fetch ` +
45 `"${artifactFileName}" artifact release info for ${releaseTag} release`
46 );
47 }
48
49 // We skip runtime type checks for simplicity (here we cast from `any` to `GithubRelease`)
50 const release: GithubRelease = await response.json();
51
52 const artifact = release.assets.find(artifact => artifact.name === artifactFileName);
53
54 if (!artifact) {
55 throw new Error(
56 `Artifact ${artifactFileName} was not found in ${release.name} release!`
57 );
58 }
59
60 return {
61 releaseName: release.name,
62 releaseDate: new Date(release.published_at),
63 downloadUrl: artifact.browser_download_url
64 };
65
66 // We omit declaration of tremendous amount of fields that we are not using here
67 interface GithubRelease {
68 name: string;
69 // eslint-disable-next-line camelcase
70 published_at: string;
71 assets: Array<{
72 name: string;
73 // eslint-disable-next-line camelcase
74 browser_download_url: string;
75 }>;
76 }
77}
diff --git a/editors/code/src/installation/interfaces.ts b/editors/code/src/installation/interfaces.ts
deleted file mode 100644
index 1a8ea0884..000000000
--- a/editors/code/src/installation/interfaces.ts
+++ /dev/null
@@ -1,63 +0,0 @@
1export interface GithubRepo {
2 name: string;
3 owner: string;
4}
5
6/**
7 * Metadata about particular artifact retrieved from GitHub releases.
8 */
9export interface ArtifactReleaseInfo {
10 releaseDate: Date;
11 releaseName: string;
12 downloadUrl: string;
13}
14
15/**
16 * Represents the source of a an artifact which is either specified by the user
17 * explicitly, or bundled by this extension from GitHub releases.
18 */
19export type ArtifactSource = ArtifactSource.ExplicitPath | ArtifactSource.GithubRelease;
20
21export namespace ArtifactSource {
22 /**
23 * Type tag for `ArtifactSource` discriminated union.
24 */
25 export const enum Type { ExplicitPath, GithubRelease }
26
27 export interface ExplicitPath {
28 type: Type.ExplicitPath;
29
30 /**
31 * Filesystem path to the binary specified by the user explicitly.
32 */
33 path: string;
34 }
35
36 export interface GithubRelease {
37 type: Type.GithubRelease;
38
39 /**
40 * Repository where the binary is stored.
41 */
42 repo: GithubRepo;
43
44
45 // FIXME: add installationPath: string;
46
47 /**
48 * Directory on the filesystem where the bundled binary is stored.
49 */
50 dir: string;
51
52 /**
53 * Name of the binary file. It is stored under the same name on GitHub releases
54 * and in local `.dir`.
55 */
56 file: string;
57
58 /**
59 * Tag of github release that denotes a version required by this extension.
60 */
61 tag: string;
62 }
63}
diff --git a/editors/code/src/installation/server.ts b/editors/code/src/installation/server.ts
deleted file mode 100644
index 05d326131..000000000
--- a/editors/code/src/installation/server.ts
+++ /dev/null
@@ -1,131 +0,0 @@
1import * as vscode from "vscode";
2import * as path from "path";
3import { spawnSync } from "child_process";
4
5import { ArtifactSource } from "./interfaces";
6import { fetchArtifactReleaseInfo } from "./fetch_artifact_release_info";
7import { downloadArtifactWithProgressUi } from "./downloads";
8import { log, assert, notReentrant } from "../util";
9import { Config, NIGHTLY_TAG } from "../config";
10import { PersistentState } from "../persistent_state";
11
12export async function ensureServerBinary(config: Config, state: PersistentState): Promise<null | string> {
13 const source = config.serverSource;
14
15 if (!source) {
16 vscode.window.showErrorMessage(
17 "Unfortunately we don't ship binaries for your platform yet. " +
18 "You need to manually clone rust-analyzer repository and " +
19 "run `cargo xtask install --server` to build the language server from sources. " +
20 "If you feel that your platform should be supported, please create an issue " +
21 "about that [here](https://github.com/rust-analyzer/rust-analyzer/issues) and we " +
22 "will consider it."
23 );
24 return null;
25 }
26
27 switch (source.type) {
28 case ArtifactSource.Type.ExplicitPath: {
29 if (isBinaryAvailable(source.path)) {
30 return source.path;
31 }
32
33 vscode.window.showErrorMessage(
34 `Unable to run ${source.path} binary. ` +
35 `To use the pre-built language server, set "rust-analyzer.serverPath" ` +
36 "value to `null` or remove it from the settings to use it by default."
37 );
38 return null;
39 }
40 case ArtifactSource.Type.GithubRelease: {
41 if (!shouldDownloadServer(state, source)) {
42 return path.join(source.dir, source.file);
43 }
44
45 if (config.askBeforeDownload) {
46 const userResponse = await vscode.window.showInformationMessage(
47 `Language server version ${source.tag} for rust-analyzer is not installed. ` +
48 "Do you want to download it now?",
49 "Download now", "Cancel"
50 );
51 if (userResponse !== "Download now") return null;
52 }
53
54 return await downloadServer(state, source);
55 }
56 }
57}
58
59function shouldDownloadServer(
60 state: PersistentState,
61 source: ArtifactSource.GithubRelease,
62): boolean {
63 if (!isBinaryAvailable(path.join(source.dir, source.file))) return true;
64
65 const installed = {
66 tag: state.serverReleaseTag.get(),
67 date: state.serverReleaseDate.get()
68 };
69 const required = {
70 tag: source.tag,
71 date: state.installedNightlyExtensionReleaseDate.get()
72 };
73
74 log.debug("Installed server:", installed, "required:", required);
75
76 if (required.tag !== NIGHTLY_TAG || installed.tag !== NIGHTLY_TAG) {
77 return required.tag !== installed.tag;
78 }
79
80 assert(required.date !== null, "Extension release date should have been saved during its installation");
81 assert(installed.date !== null, "Server release date should have been saved during its installation");
82
83 return installed.date.getTime() !== required.date.getTime();
84}
85
86/**
87 * Enforcing no reentrancy for this is best-effort.
88 */
89const downloadServer = notReentrant(async (
90 state: PersistentState,
91 source: ArtifactSource.GithubRelease,
92): Promise<null | string> => {
93 try {
94 const releaseInfo = await fetchArtifactReleaseInfo(source.repo, source.file, source.tag);
95
96 await downloadArtifactWithProgressUi(releaseInfo, source.file, source.dir, "language server");
97 await Promise.all([
98 state.serverReleaseTag.set(releaseInfo.releaseName),
99 state.serverReleaseDate.set(releaseInfo.releaseDate)
100 ]);
101 } catch (err) {
102 log.downloadError(err, "language server", source.repo.name);
103 return null;
104 }
105
106 const binaryPath = path.join(source.dir, source.file);
107
108 assert(isBinaryAvailable(binaryPath),
109 `Downloaded language server binary is not functional.` +
110 `Downloaded from GitHub repo ${source.repo.owner}/${source.repo.name} ` +
111 `to ${binaryPath}`
112 );
113
114 vscode.window.showInformationMessage(
115 "Rust analyzer language server was successfully installed 🦀"
116 );
117
118 return binaryPath;
119});
120
121function isBinaryAvailable(binaryPath: string): boolean {
122 const res = spawnSync(binaryPath, ["--version"]);
123
124 // ACHTUNG! `res` type declaration is inherently wrong, see
125 // https://github.com/DefinitelyTyped/DefinitelyTyped/issues/42221
126
127 log.debug("Checked binary availablity via --version", res);
128 log.debug(binaryPath, "--version output:", res.output?.map(String));
129
130 return res.status === 0;
131}
diff --git a/editors/code/src/main.ts b/editors/code/src/main.ts
index 94ecd4dab..d907f3e6f 100644
--- a/editors/code/src/main.ts
+++ b/editors/code/src/main.ts
@@ -1,15 +1,18 @@
1import * as vscode from 'vscode'; 1import * as vscode from 'vscode';
2import * as path from "path";
3import * as os from "os";
4import { promises as fs } from "fs";
2 5
3import * as commands from './commands'; 6import * as commands from './commands';
4import { activateInlayHints } from './inlay_hints'; 7import { activateInlayHints } from './inlay_hints';
5import { activateStatusDisplay } from './status_display'; 8import { activateStatusDisplay } from './status_display';
6import { Ctx } from './ctx'; 9import { Ctx } from './ctx';
7import { activateHighlighting } from './highlighting'; 10import { activateHighlighting } from './highlighting';
8import { ensureServerBinary } from './installation/server'; 11import { Config, NIGHTLY_TAG } from './config';
9import { Config } from './config'; 12import { log, assert } from './util';
10import { log } from './util';
11import { ensureProperExtensionVersion } from './installation/extension';
12import { PersistentState } from './persistent_state'; 13import { PersistentState } from './persistent_state';
14import { fetchRelease, download } from './net';
15import { spawnSync } from 'child_process';
13 16
14let ctx: Ctx | undefined; 17let ctx: Ctx | undefined;
15 18
@@ -35,27 +38,14 @@ export async function activate(context: vscode.ExtensionContext) {
35 context.subscriptions.push(defaultOnEnter); 38 context.subscriptions.push(defaultOnEnter);
36 39
37 const config = new Config(context); 40 const config = new Config(context);
38 const state = new PersistentState(context); 41 const state = new PersistentState(context.globalState);
39 42 const serverPath = await bootstrap(config, state);
40 vscode.workspace.onDidChangeConfiguration(() => ensureProperExtensionVersion(config, state).catch(log.error));
41
42 // Don't await the user response here, otherwise we will block the lsp server bootstrap
43 void ensureProperExtensionVersion(config, state).catch(log.error);
44
45 const serverPath = await ensureServerBinary(config, state);
46
47 if (serverPath == null) {
48 throw new Error(
49 "Rust Analyzer Language Server is not available. " +
50 "Please, ensure its [proper installation](https://rust-analyzer.github.io/manual.html#installation)."
51 );
52 }
53 43
54 // Note: we try to start the server before we activate type hints so that it 44 // Note: we try to start the server before we activate type hints so that it
55 // registers its `onDidChangeDocument` handler before us. 45 // registers its `onDidChangeDocument` handler before us.
56 // 46 //
57 // This a horribly, horribly wrong way to deal with this problem. 47 // This a horribly, horribly wrong way to deal with this problem.
58 ctx = await Ctx.create(config, state, context, serverPath); 48 ctx = await Ctx.create(config, context, serverPath);
59 49
60 // Commands which invokes manually via command palette, shortcut, etc. 50 // Commands which invokes manually via command palette, shortcut, etc.
61 ctx.registerCommand('reload', (ctx) => { 51 ctx.registerCommand('reload', (ctx) => {
@@ -109,3 +99,131 @@ export async function deactivate() {
109 await ctx?.client?.stop(); 99 await ctx?.client?.stop();
110 ctx = undefined; 100 ctx = undefined;
111} 101}
102
103async function bootstrap(config: Config, state: PersistentState): Promise<string> {
104 await fs.mkdir(config.globalStoragePath, { recursive: true });
105
106 await bootstrapExtension(config, state);
107 const path = await bootstrapServer(config, state);
108
109 return path;
110}
111
112async function bootstrapExtension(config: Config, state: PersistentState): Promise<void> {
113 if (config.channel === "stable") {
114 if (config.extensionReleaseTag === NIGHTLY_TAG) {
115 vscode.window.showWarningMessage(`You are running a nightly version of rust-analyzer extension.
116To switch to stable, uninstall the extension and re-install it from the marketplace`);
117 }
118 return;
119 };
120
121 const lastCheck = state.lastCheck;
122 const now = Date.now();
123
124 const anHour = 60 * 60 * 1000;
125 const shouldDownloadNightly = state.releaseId === undefined || (now - (lastCheck ?? 0)) > anHour;
126
127 if (!shouldDownloadNightly) return;
128
129 const release = await fetchRelease("nightly").catch((e) => {
130 log.error(e);
131 if (state.releaseId === undefined) { // Show error only for the initial download
132 vscode.window.showErrorMessage(`Failed to download rust-analyzer nightly ${e}`);
133 }
134 return undefined;
135 });
136 if (release === undefined || release.id === state.releaseId) return;
137
138 const userResponse = await vscode.window.showInformationMessage(
139 "New version of rust-analyzer (nightly) is available (requires reload).",
140 "Update"
141 );
142 if (userResponse !== "Update") return;
143
144 const artifact = release.assets.find(artifact => artifact.name === "rust-analyzer.vsix");
145 assert(!!artifact, `Bad release: ${JSON.stringify(release)}`);
146
147 const dest = path.join(config.globalStoragePath, "rust-analyzer.vsix");
148 await download(artifact.browser_download_url, dest, "Downloading rust-analyzer extension");
149
150 await vscode.commands.executeCommand("workbench.extensions.installExtension", vscode.Uri.file(dest));
151 await fs.unlink(dest);
152
153 await state.updateReleaseId(release.id);
154 await state.updateLastCheck(now);
155 await vscode.commands.executeCommand("workbench.action.reloadWindow");
156}
157
158async function bootstrapServer(config: Config, state: PersistentState): Promise<string> {
159 const path = await getServer(config, state);
160 if (!path) {
161 throw new Error(
162 "Rust Analyzer Language Server is not available. " +
163 "Please, ensure its [proper installation](https://rust-analyzer.github.io/manual.html#installation)."
164 );
165 }
166
167 const res = spawnSync(path, ["--version"], { encoding: 'utf8' });
168 log.debug("Checked binary availability via --version", res);
169 log.debug(res, "--version output:", res.output);
170 if (res.status !== 0) {
171 throw new Error(
172 `Failed to execute ${path} --version`
173 );
174 }
175
176 return path;
177}
178
179async function getServer(config: Config, state: PersistentState): Promise<string | undefined> {
180 const explicitPath = process.env.__RA_LSP_SERVER_DEBUG ?? config.serverPath;
181 if (explicitPath) {
182 if (explicitPath.startsWith("~/")) {
183 return os.homedir() + explicitPath.slice("~".length);
184 }
185 return explicitPath;
186 };
187
188 let binaryName: string | undefined = undefined;
189 if (process.arch === "x64" || process.arch === "x32") {
190 if (process.platform === "linux") binaryName = "rust-analyzer-linux";
191 if (process.platform === "darwin") binaryName = "rust-analyzer-mac";
192 if (process.platform === "win32") binaryName = "rust-analyzer-windows.exe";
193 }
194 if (binaryName === undefined) {
195 vscode.window.showErrorMessage(
196 "Unfortunately we don't ship binaries for your platform yet. " +
197 "You need to manually clone rust-analyzer repository and " +
198 "run `cargo xtask install --server` to build the language server from sources. " +
199 "If you feel that your platform should be supported, please create an issue " +
200 "about that [here](https://github.com/rust-analyzer/rust-analyzer/issues) and we " +
201 "will consider it."
202 );
203 return undefined;
204 }
205
206 const dest = path.join(config.globalStoragePath, binaryName);
207 const exists = await fs.stat(dest).then(() => true, () => false);
208 if (!exists) {
209 await state.updateServerVersion(undefined);
210 }
211
212 if (state.serverVersion === config.packageJsonVersion) return dest;
213
214 if (config.askBeforeDownload) {
215 const userResponse = await vscode.window.showInformationMessage(
216 `Language server version ${config.packageJsonVersion} for rust-analyzer is not installed.`,
217 "Download now"
218 );
219 if (userResponse !== "Download now") return dest;
220 }
221
222 const release = await fetchRelease(config.extensionReleaseTag);
223 const artifact = release.assets.find(artifact => artifact.name === binaryName);
224 assert(!!artifact, `Bad release: ${JSON.stringify(release)}`);
225
226 await download(artifact.browser_download_url, dest, "Downloading rust-analyzer server", { mode: 0o755 });
227 await state.updateServerVersion(config.packageJsonVersion);
228 return dest;
229}
diff --git a/editors/code/src/installation/downloads.ts b/editors/code/src/net.ts
index 7ce2e2960..492213937 100644
--- a/editors/code/src/installation/downloads.ts
+++ b/editors/code/src/net.ts
@@ -1,24 +1,101 @@
1import fetch from "node-fetch"; 1import fetch from "node-fetch";
2import * as vscode from "vscode"; 2import * as vscode from "vscode";
3import * as path from "path";
4import * as fs from "fs"; 3import * as fs from "fs";
5import * as stream from "stream"; 4import * as stream from "stream";
6import * as util from "util"; 5import * as util from "util";
7import { log, assert } from "../util"; 6import { log, assert } from "./util";
8import { ArtifactReleaseInfo } from "./interfaces";
9 7
10const pipeline = util.promisify(stream.pipeline); 8const pipeline = util.promisify(stream.pipeline);
11 9
10const GITHUB_API_ENDPOINT_URL = "https://api.github.com";
11const OWNER = "rust-analyzer";
12const REPO = "rust-analyzer";
13
14export async function fetchRelease(
15 releaseTag: string
16): Promise<GithubRelease> {
17
18 const apiEndpointPath = `/repos/${OWNER}/${REPO}/releases/tags/${releaseTag}`;
19
20 const requestUrl = GITHUB_API_ENDPOINT_URL + apiEndpointPath;
21
22 log.debug("Issuing request for released artifacts metadata to", requestUrl);
23
24 const response = await fetch(requestUrl, { headers: { Accept: "application/vnd.github.v3+json" } });
25
26 if (!response.ok) {
27 log.error("Error fetching artifact release info", {
28 requestUrl,
29 releaseTag,
30 response: {
31 headers: response.headers,
32 status: response.status,
33 body: await response.text(),
34 }
35 });
36
37 throw new Error(
38 `Got response ${response.status} when trying to fetch ` +
39 `release info for ${releaseTag} release`
40 );
41 }
42
43 // We skip runtime type checks for simplicity (here we cast from `any` to `GithubRelease`)
44 const release: GithubRelease = await response.json();
45 return release;
46}
47
48// We omit declaration of tremendous amount of fields that we are not using here
49export interface GithubRelease {
50 name: string;
51 id: number;
52 // eslint-disable-next-line camelcase
53 published_at: string;
54 assets: Array<{
55 name: string;
56 // eslint-disable-next-line camelcase
57 browser_download_url: string;
58 }>;
59}
60
61
62export async function download(
63 downloadUrl: string,
64 destinationPath: string,
65 progressTitle: string,
66 { mode }: { mode?: number } = {},
67) {
68 await vscode.window.withProgress(
69 {
70 location: vscode.ProgressLocation.Notification,
71 cancellable: false,
72 title: progressTitle
73 },
74 async (progress, _cancellationToken) => {
75 let lastPercentage = 0;
76 await downloadFile(downloadUrl, destinationPath, mode, (readBytes, totalBytes) => {
77 const newPercentage = (readBytes / totalBytes) * 100;
78 progress.report({
79 message: newPercentage.toFixed(0) + "%",
80 increment: newPercentage - lastPercentage
81 });
82
83 lastPercentage = newPercentage;
84 });
85 }
86 );
87}
88
12/** 89/**
13 * Downloads file from `url` and stores it at `destFilePath` with `destFilePermissions`. 90 * Downloads file from `url` and stores it at `destFilePath` with `destFilePermissions`.
14 * `onProgress` callback is called on recieveing each chunk of bytes 91 * `onProgress` callback is called on recieveing each chunk of bytes
15 * to track the progress of downloading, it gets the already read and total 92 * to track the progress of downloading, it gets the already read and total
16 * amount of bytes to read as its parameters. 93 * amount of bytes to read as its parameters.
17 */ 94 */
18export async function downloadFile( 95async function downloadFile(
19 url: string, 96 url: string,
20 destFilePath: fs.PathLike, 97 destFilePath: fs.PathLike,
21 destFilePermissions: number, 98 mode: number | undefined,
22 onProgress: (readBytes: number, totalBytes: number) => void 99 onProgress: (readBytes: number, totalBytes: number) => void
23): Promise<void> { 100): Promise<void> {
24 const res = await fetch(url); 101 const res = await fetch(url);
@@ -41,7 +118,7 @@ export async function downloadFile(
41 onProgress(readBytes, totalBytes); 118 onProgress(readBytes, totalBytes);
42 }); 119 });
43 120
44 const destFileStream = fs.createWriteStream(destFilePath, { mode: destFilePermissions }); 121 const destFileStream = fs.createWriteStream(destFilePath, { mode });
45 122
46 await pipeline(res.body, destFileStream); 123 await pipeline(res.body, destFileStream);
47 return new Promise<void>(resolve => { 124 return new Promise<void>(resolve => {
@@ -52,46 +129,3 @@ export async function downloadFile(
52 // Issue at nodejs repo: https://github.com/nodejs/node/issues/31776 129 // Issue at nodejs repo: https://github.com/nodejs/node/issues/31776
53 }); 130 });
54} 131}
55
56/**
57 * Downloads artifact from given `downloadUrl`.
58 * Creates `installationDir` if it is not yet created and puts the artifact under
59 * `artifactFileName`.
60 * Displays info about the download progress in an info message printing the name
61 * of the artifact as `displayName`.
62 */
63export async function downloadArtifactWithProgressUi(
64 { downloadUrl, releaseName }: ArtifactReleaseInfo,
65 artifactFileName: string,
66 installationDir: string,
67 displayName: string,
68) {
69 await fs.promises.mkdir(installationDir).catch(err => assert(
70 err?.code === "EEXIST",
71 `Couldn't create directory "${installationDir}" to download ` +
72 `${artifactFileName} artifact: ${err?.message}`
73 ));
74
75 const installationPath = path.join(installationDir, artifactFileName);
76
77 await vscode.window.withProgress(
78 {
79 location: vscode.ProgressLocation.Notification,
80 cancellable: false, // FIXME: add support for canceling download?
81 title: `Downloading rust-analyzer ${displayName} (${releaseName})`
82 },
83 async (progress, _cancellationToken) => {
84 let lastPrecentage = 0;
85 const filePermissions = 0o755; // (rwx, r_x, r_x)
86 await downloadFile(downloadUrl, installationPath, filePermissions, (readBytes, totalBytes) => {
87 const newPercentage = (readBytes / totalBytes) * 100;
88 progress.report({
89 message: newPercentage.toFixed(0) + "%",
90 increment: newPercentage - lastPrecentage
91 });
92
93 lastPrecentage = newPercentage;
94 });
95 }
96 );
97}
diff --git a/editors/code/src/persistent_state.ts b/editors/code/src/persistent_state.ts
index 13095b806..138d11b89 100644
--- a/editors/code/src/persistent_state.ts
+++ b/editors/code/src/persistent_state.ts
@@ -1,49 +1,41 @@
1import * as vscode from 'vscode'; 1import * as vscode from 'vscode';
2import { log } from "./util"; 2import { log } from './util';
3 3
4export class PersistentState { 4export class PersistentState {
5 constructor(private readonly ctx: vscode.ExtensionContext) { 5 constructor(private readonly globalState: vscode.Memento) {
6 const { lastCheck, releaseId, serverVersion } = this;
7 log.debug("PersistentState: ", { lastCheck, releaseId, serverVersion });
6 } 8 }
7 9
8 readonly installedNightlyExtensionReleaseDate = new DateStorage( 10 /**
9 "installed-nightly-extension-release-date", 11 * Used to check for *nightly* updates once an hour.
10 this.ctx.globalState 12 */
11 ); 13 get lastCheck(): number | undefined {
12 readonly serverReleaseDate = new DateStorage("server-release-date", this.ctx.globalState); 14 return this.globalState.get("lastCheck");
13 readonly serverReleaseTag = new Storage<null | string>("server-release-tag", this.ctx.globalState, null);
14}
15
16
17export class Storage<T> {
18 constructor(
19 private readonly key: string,
20 private readonly storage: vscode.Memento,
21 private readonly defaultVal: T
22 ) { }
23
24 get(): T {
25 const val = this.storage.get(this.key, this.defaultVal);
26 log.debug(this.key, "==", val);
27 return val;
28 } 15 }
29 async set(val: T) { 16 async updateLastCheck(value: number) {
30 log.debug(this.key, "=", val); 17 await this.globalState.update("lastCheck", value);
31 await this.storage.update(this.key, val);
32 } 18 }
33}
34export class DateStorage {
35 inner: Storage<null | string>;
36 19
37 constructor(key: string, storage: vscode.Memento) { 20 /**
38 this.inner = new Storage(key, storage, null); 21 * Release id of the *nightly* extension.
22 * Used to check if we should update.
23 */
24 get releaseId(): number | undefined {
25 return this.globalState.get("releaseId");
39 } 26 }
40 27 async updateReleaseId(value: number) {
41 get(): null | Date { 28 await this.globalState.update("releaseId", value);
42 const dateStr = this.inner.get();
43 return dateStr ? new Date(dateStr) : null;
44 } 29 }
45 30
46 async set(date: null | Date) { 31 /**
47 await this.inner.set(date ? date.toString() : null); 32 * Version of the extension that installed the server.
33 * Used to check if we need to update the server.
34 */
35 get serverVersion(): string | undefined {
36 return this.globalState.get("serverVersion");
37 }
38 async updateServerVersion(value: string | undefined) {
39 await this.globalState.update("serverVersion", value);
48 } 40 }
49} 41}
diff --git a/editors/code/src/util.ts b/editors/code/src/util.ts
index 2bfc145e6..978a31751 100644
--- a/editors/code/src/util.ts
+++ b/editors/code/src/util.ts
@@ -1,6 +1,5 @@
1import * as lc from "vscode-languageclient"; 1import * as lc from "vscode-languageclient";
2import * as vscode from "vscode"; 2import * as vscode from "vscode";
3import { promises as dns } from "dns";
4import { strict as nativeAssert } from "assert"; 3import { strict as nativeAssert } from "assert";
5 4
6export function assert(condition: boolean, explanation: string): asserts condition { 5export function assert(condition: boolean, explanation: string): asserts condition {
@@ -31,22 +30,6 @@ export const log = new class {
31 // eslint-disable-next-line no-console 30 // eslint-disable-next-line no-console
32 console.error(message, ...optionalParams); 31 console.error(message, ...optionalParams);
33 } 32 }
34
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 );
49 }
50}; 33};
51 34
52export async function sendRequestWithRetry<TParam, TRet>( 35export async function sendRequestWithRetry<TParam, TRet>(
@@ -86,17 +69,6 @@ function sleep(ms: number) {
86 return new Promise(resolve => setTimeout(resolve, ms)); 69 return new Promise(resolve => setTimeout(resolve, ms));
87} 70}
88 71
89export function notReentrant<TThis, TParams extends any[], TRet>(
90 fn: (this: TThis, ...params: TParams) => Promise<TRet>
91): typeof fn {
92 let entered = false;
93 return function(...params) {
94 assert(!entered, `Reentrancy invariant for ${fn.name} is violated`);
95 entered = true;
96 return fn.apply(this, params).finally(() => entered = false);
97 };
98}
99
100export type RustDocument = vscode.TextDocument & { languageId: "rust" }; 72export type RustDocument = vscode.TextDocument & { languageId: "rust" };
101export type RustEditor = vscode.TextEditor & { document: RustDocument; id: string }; 73export type RustEditor = vscode.TextEditor & { document: RustDocument; id: string };
102 74
@@ -110,29 +82,3 @@ export function isRustDocument(document: vscode.TextDocument): document is RustD
110export function isRustEditor(editor: vscode.TextEditor): editor is RustEditor { 82export function isRustEditor(editor: vscode.TextEditor): editor is RustEditor {
111 return isRustDocument(editor.document); 83 return isRustDocument(editor.document);
112} 84}
113
114/**
115 * @param extensionId The canonical extension identifier in the form of: `publisher.name`
116 */
117export async function vscodeReinstallExtension(extensionId: string) {
118 // Unfortunately there is no straightforward way as of now, these commands
119 // were found in vscode source code.
120
121 log.debug("Uninstalling extension", extensionId);
122 await vscode.commands.executeCommand("workbench.extensions.uninstallExtension", extensionId);
123 log.debug("Installing extension", extensionId);
124 await vscode.commands.executeCommand("workbench.extensions.installExtension", extensionId);
125}
126
127export async function vscodeReloadWindow(): Promise<never> {
128 await vscode.commands.executeCommand("workbench.action.reloadWindow");
129
130 assert(false, "unreachable");
131}
132
133export async function vscodeInstallExtensionFromVsix(vsixPath: string) {
134 await vscode.commands.executeCommand(
135 "workbench.extensions.installExtension",
136 vscode.Uri.file(vsixPath)
137 );
138}