diff options
author | bors[bot] <26634292+bors[bot]@users.noreply.github.com> | 2020-03-19 08:06:48 +0000 |
---|---|---|
committer | GitHub <[email protected]> | 2020-03-19 08:06:48 +0000 |
commit | aca3c3086ee99f5770a60970e20af640c895d42a (patch) | |
tree | 2f0b3233cc4728436ba5e47a6e91e7df33585d43 /editors/code/src/main.ts | |
parent | 55336722b3662cbdcc9e1b92a3e27ed0442d2452 (diff) | |
parent | fb6e655de8a44c65275ad45a27bf5bd684670ba0 (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.ts | 158 |
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 @@ | |||
1 | import * as vscode from 'vscode'; | 1 | import * as vscode from 'vscode'; |
2 | import * as path from "path"; | ||
3 | import * as os from "os"; | ||
4 | import { promises as fs } from "fs"; | ||
2 | 5 | ||
3 | import * as commands from './commands'; | 6 | import * as commands from './commands'; |
4 | import { activateInlayHints } from './inlay_hints'; | 7 | import { activateInlayHints } from './inlay_hints'; |
5 | import { activateStatusDisplay } from './status_display'; | 8 | import { activateStatusDisplay } from './status_display'; |
6 | import { Ctx } from './ctx'; | 9 | import { Ctx } from './ctx'; |
7 | import { activateHighlighting } from './highlighting'; | 10 | import { activateHighlighting } from './highlighting'; |
8 | import { ensureServerBinary } from './installation/server'; | 11 | import { Config, NIGHTLY_TAG } from './config'; |
9 | import { Config } from './config'; | 12 | import { log, assert } from './util'; |
10 | import { log } from './util'; | ||
11 | import { ensureProperExtensionVersion } from './installation/extension'; | ||
12 | import { PersistentState } from './persistent_state'; | 13 | import { PersistentState } from './persistent_state'; |
14 | import { fetchRelease, download } from './net'; | ||
15 | import { spawnSync } from 'child_process'; | ||
13 | 16 | ||
14 | let ctx: Ctx | undefined; | 17 | let 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 | |||
103 | async 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 | |||
112 | async 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. | ||
116 | To 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 | |||
158 | async 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 | |||
179 | async 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 | } | ||