aboutsummaryrefslogtreecommitdiff
path: root/editors/code/src/main.ts
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 /editors/code/src/main.ts
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]>
Diffstat (limited to 'editors/code/src/main.ts')
-rw-r--r--editors/code/src/main.ts158
1 files changed, 138 insertions, 20 deletions
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}