]> git.lizzy.rs Git - rust.git/blob - editors/code/src/main.ts
Merge #6055
[rust.git] / editors / code / src / main.ts
1 import * as vscode from 'vscode';
2 import * as path from "path";
3 import * as os from "os";
4 import { promises as fs, PathLike } from "fs";
5
6 import * as commands from './commands';
7 import { activateInlayHints } from './inlay_hints';
8 import { Ctx } from './ctx';
9 import { Config, NIGHTLY_TAG } from './config';
10 import { log, assert, isValidExecutable } from './util';
11 import { PersistentState } from './persistent_state';
12 import { fetchRelease, download } from './net';
13 import { activateTaskProvider } from './tasks';
14 import { setContextValue } from './util';
15 import { exec } from 'child_process';
16
17 let ctx: Ctx | undefined;
18
19 const RUST_PROJECT_CONTEXT_NAME = "inRustProject";
20
21 export async function activate(context: vscode.ExtensionContext) {
22     // For some reason vscode not always shows pop-up error notifications
23     // when an extension fails to activate, so we do it explicitly by ourselves.
24     // FIXME: remove this bit of code once vscode fixes this issue: https://github.com/microsoft/vscode/issues/101242
25     await tryActivate(context).catch(err => {
26         void vscode.window.showErrorMessage(`Cannot activate rust-analyzer: ${err.message}`);
27         throw err;
28     });
29 }
30
31 async function tryActivate(context: vscode.ExtensionContext) {
32     // Register a "dumb" onEnter command for the case where server fails to
33     // start.
34     //
35     // FIXME: refactor command registration code such that commands are
36     // **always** registered, even if the server does not start. Use API like
37     // this perhaps?
38     //
39     // ```TypeScript
40     // registerCommand(
41     //    factory: (Ctx) => ((Ctx) => any),
42     //    fallback: () => any = () => vscode.window.showErrorMessage(
43     //        "rust-analyzer is not available"
44     //    ),
45     // )
46     const defaultOnEnter = vscode.commands.registerCommand(
47         'rust-analyzer.onEnter',
48         () => vscode.commands.executeCommand('default:type', { text: '\n' }),
49     );
50     context.subscriptions.push(defaultOnEnter);
51
52     const config = new Config(context);
53     const state = new PersistentState(context.globalState);
54     const serverPath = await bootstrap(config, state).catch(err => {
55         let message = "bootstrap error. ";
56
57         if (err.code === "EBUSY" || err.code === "ETXTBSY" || err.code === "EPERM") {
58             message += "Other vscode windows might be using rust-analyzer, ";
59             message += "you should close them and reload this window to retry. ";
60         }
61
62         message += 'See the logs in "OUTPUT > Rust Analyzer Client" (should open automatically). ';
63         message += 'To enable verbose logs use { "rust-analyzer.trace.extension": true }';
64
65         log.error("Bootstrap error", err);
66         throw new Error(message);
67     });
68
69     const workspaceFolder = vscode.workspace.workspaceFolders?.[0];
70     if (workspaceFolder === undefined) {
71         throw new Error("no folder is opened");
72     }
73
74     // Note: we try to start the server before we activate type hints so that it
75     // registers its `onDidChangeDocument` handler before us.
76     //
77     // This a horribly, horribly wrong way to deal with this problem.
78     ctx = await Ctx.create(config, context, serverPath, workspaceFolder.uri.fsPath);
79
80     setContextValue(RUST_PROJECT_CONTEXT_NAME, true);
81
82     // Commands which invokes manually via command palette, shortcut, etc.
83
84     // Reloading is inspired by @DanTup maneuver: https://github.com/microsoft/vscode/issues/45774#issuecomment-373423895
85     ctx.registerCommand('reload', _ => async () => {
86         void vscode.window.showInformationMessage('Reloading rust-analyzer...');
87         await deactivate();
88         while (context.subscriptions.length > 0) {
89             try {
90                 context.subscriptions.pop()!.dispose();
91             } catch (err) {
92                 log.error("Dispose error:", err);
93             }
94         }
95         await activate(context).catch(log.error);
96     });
97
98     ctx.registerCommand('updateGithubToken', ctx => async () => {
99         await queryForGithubToken(new PersistentState(ctx.globalState));
100     });
101
102     ctx.registerCommand('analyzerStatus', commands.analyzerStatus);
103     ctx.registerCommand('memoryUsage', commands.memoryUsage);
104     ctx.registerCommand('reloadWorkspace', commands.reloadWorkspace);
105     ctx.registerCommand('matchingBrace', commands.matchingBrace);
106     ctx.registerCommand('joinLines', commands.joinLines);
107     ctx.registerCommand('parentModule', commands.parentModule);
108     ctx.registerCommand('syntaxTree', commands.syntaxTree);
109     ctx.registerCommand('expandMacro', commands.expandMacro);
110     ctx.registerCommand('run', commands.run);
111     ctx.registerCommand('debug', commands.debug);
112     ctx.registerCommand('newDebugConfig', commands.newDebugConfig);
113
114     defaultOnEnter.dispose();
115     ctx.registerCommand('onEnter', commands.onEnter);
116
117     ctx.registerCommand('ssr', commands.ssr);
118     ctx.registerCommand('serverVersion', commands.serverVersion);
119     ctx.registerCommand('toggleInlayHints', commands.toggleInlayHints);
120
121     // Internal commands which are invoked by the server.
122     ctx.registerCommand('runSingle', commands.runSingle);
123     ctx.registerCommand('debugSingle', commands.debugSingle);
124     ctx.registerCommand('showReferences', commands.showReferences);
125     ctx.registerCommand('applySnippetWorkspaceEdit', commands.applySnippetWorkspaceEditCommand);
126     ctx.registerCommand('resolveCodeAction', commands.resolveCodeAction);
127     ctx.registerCommand('applyActionGroup', commands.applyActionGroup);
128     ctx.registerCommand('gotoLocation', commands.gotoLocation);
129
130     ctx.pushCleanup(activateTaskProvider(workspaceFolder, ctx.config));
131
132     activateInlayHints(ctx);
133
134     vscode.workspace.onDidChangeConfiguration(
135         _ => ctx?.client?.sendNotification('workspace/didChangeConfiguration', { settings: "" }),
136         null,
137         ctx.subscriptions,
138     );
139 }
140
141 export async function deactivate() {
142     setContextValue(RUST_PROJECT_CONTEXT_NAME, undefined);
143     await ctx?.client.stop();
144     ctx = undefined;
145 }
146
147 async function bootstrap(config: Config, state: PersistentState): Promise<string> {
148     await fs.mkdir(config.globalStoragePath, { recursive: true });
149
150     await bootstrapExtension(config, state);
151     const path = await bootstrapServer(config, state);
152
153     return path;
154 }
155
156 async function bootstrapExtension(config: Config, state: PersistentState): Promise<void> {
157     if (config.package.releaseTag === null) return;
158     if (config.channel === "stable") {
159         if (config.package.releaseTag === NIGHTLY_TAG) {
160             void vscode.window.showWarningMessage(
161                 `You are running a nightly version of rust-analyzer extension. ` +
162                 `To switch to stable, uninstall the extension and re-install it from the marketplace`
163             );
164         }
165         return;
166     };
167
168     const now = Date.now();
169     if (config.package.releaseTag === NIGHTLY_TAG) {
170         // Check if we should poll github api for the new nightly version
171         // if we haven't done it during the past hour
172         const lastCheck = state.lastCheck;
173
174         const anHour = 60 * 60 * 1000;
175         const shouldCheckForNewNightly = state.releaseId === undefined || (now - (lastCheck ?? 0)) > anHour;
176
177         if (!shouldCheckForNewNightly) return;
178     }
179
180     const release = await downloadWithRetryDialog(state, async () => {
181         return await fetchRelease("nightly", state.githubToken);
182     }).catch((e) => {
183         log.error(e);
184         if (state.releaseId === undefined) { // Show error only for the initial download
185             vscode.window.showErrorMessage(`Failed to download rust-analyzer nightly ${e}`);
186         }
187         return undefined;
188     });
189     if (release === undefined || release.id === state.releaseId) return;
190
191     const userResponse = await vscode.window.showInformationMessage(
192         "New version of rust-analyzer (nightly) is available (requires reload).",
193         "Update"
194     );
195     if (userResponse !== "Update") return;
196
197     const artifact = release.assets.find(artifact => artifact.name === "rust-analyzer.vsix");
198     assert(!!artifact, `Bad release: ${JSON.stringify(release)}`);
199
200     const dest = path.join(config.globalStoragePath, "rust-analyzer.vsix");
201
202     await downloadWithRetryDialog(state, async () => {
203         await download({
204             url: artifact.browser_download_url,
205             dest,
206             progressTitle: "Downloading rust-analyzer extension",
207             overwrite: true,
208         });
209     });
210
211     await vscode.commands.executeCommand("workbench.extensions.installExtension", vscode.Uri.file(dest));
212     await fs.unlink(dest);
213
214     await state.updateReleaseId(release.id);
215     await state.updateLastCheck(now);
216     await vscode.commands.executeCommand("workbench.action.reloadWindow");
217 }
218
219 async function bootstrapServer(config: Config, state: PersistentState): Promise<string> {
220     const path = await getServer(config, state);
221     if (!path) {
222         throw new Error(
223             "Rust Analyzer Language Server is not available. " +
224             "Please, ensure its [proper installation](https://rust-analyzer.github.io/manual.html#installation)."
225         );
226     }
227
228     log.info("Using server binary at", path);
229
230     if (!isValidExecutable(path)) {
231         throw new Error(`Failed to execute ${path} --version`);
232     }
233
234     return path;
235 }
236
237 async function patchelf(dest: PathLike): Promise<void> {
238     await vscode.window.withProgress(
239         {
240             location: vscode.ProgressLocation.Notification,
241             title: "Patching rust-analyzer for NixOS"
242         },
243         async (progress, _) => {
244             const expression = `
245             {src, pkgs ? import <nixpkgs> {}}:
246                 pkgs.stdenv.mkDerivation {
247                     name = "rust-analyzer";
248                     inherit src;
249                     phases = [ "installPhase" "fixupPhase" ];
250                     installPhase = "cp $src $out";
251                     fixupPhase = ''
252                     chmod 755 $out
253                     patchelf --set-interpreter "$(cat $NIX_CC/nix-support/dynamic-linker)" $out
254                     '';
255                 }
256             `;
257             const origFile = dest + "-orig";
258             await fs.rename(dest, origFile);
259             progress.report({ message: "Patching executable", increment: 20 });
260             await new Promise((resolve, reject) => {
261                 const handle = exec(`nix-build -E - --arg src '${origFile}' -o ${dest}`,
262                     (err, stdout, stderr) => {
263                         if (err != null) {
264                             reject(Error(stderr));
265                         } else {
266                             resolve(stdout);
267                         }
268                     });
269                 handle.stdin?.write(expression);
270                 handle.stdin?.end();
271             });
272             await fs.unlink(origFile);
273         }
274     );
275 }
276
277 async function getServer(config: Config, state: PersistentState): Promise<string | undefined> {
278     const explicitPath = process.env.__RA_LSP_SERVER_DEBUG ?? config.serverPath;
279     if (explicitPath) {
280         if (explicitPath.startsWith("~/")) {
281             return os.homedir() + explicitPath.slice("~".length);
282         }
283         return explicitPath;
284     };
285     if (config.package.releaseTag === null) return "rust-analyzer";
286
287     let platform: string | undefined;
288     if (process.arch === "x64" || process.arch === "ia32") {
289         if (process.platform === "linux") platform = "linux";
290         if (process.platform === "darwin") platform = "mac";
291         if (process.platform === "win32") platform = "windows";
292     }
293     if (platform === undefined) {
294         vscode.window.showErrorMessage(
295             "Unfortunately we don't ship binaries for your platform yet. " +
296             "You need to manually clone rust-analyzer repository and " +
297             "run `cargo xtask install --server` to build the language server from sources. " +
298             "If you feel that your platform should be supported, please create an issue " +
299             "about that [here](https://github.com/rust-analyzer/rust-analyzer/issues) and we " +
300             "will consider it."
301         );
302         return undefined;
303     }
304     const ext = platform === "windows" ? ".exe" : "";
305     const dest = path.join(config.globalStoragePath, `rust-analyzer-${platform}${ext}`);
306     const exists = await fs.stat(dest).then(() => true, () => false);
307     if (!exists) {
308         await state.updateServerVersion(undefined);
309     }
310
311     if (state.serverVersion === config.package.version) return dest;
312
313     if (config.askBeforeDownload) {
314         const userResponse = await vscode.window.showInformationMessage(
315             `Language server version ${config.package.version} for rust-analyzer is not installed.`,
316             "Download now"
317         );
318         if (userResponse !== "Download now") return dest;
319     }
320
321     const releaseTag = config.package.releaseTag;
322     const release = await downloadWithRetryDialog(state, async () => {
323         return await fetchRelease(releaseTag, state.githubToken);
324     });
325     const artifact = release.assets.find(artifact => artifact.name === `rust-analyzer-${platform}.gz`);
326     assert(!!artifact, `Bad release: ${JSON.stringify(release)}`);
327
328     await downloadWithRetryDialog(state, async () => {
329         await download({
330             url: artifact.browser_download_url,
331             dest,
332             progressTitle: "Downloading rust-analyzer server",
333             gunzip: true,
334             mode: 0o755,
335             overwrite: true,
336         });
337     });
338
339     // Patching executable if that's NixOS.
340     if (await fs.stat("/etc/nixos").then(_ => true).catch(_ => false)) {
341         await patchelf(dest);
342     }
343
344     await state.updateServerVersion(config.package.version);
345     return dest;
346 }
347
348 async function downloadWithRetryDialog<T>(state: PersistentState, downloadFunc: () => Promise<T>): Promise<T> {
349     while (true) {
350         try {
351             return await downloadFunc();
352         } catch (e) {
353             const selected = await vscode.window.showErrorMessage("Failed to download: " + e.message, {}, {
354                 title: "Update Github Auth Token",
355                 updateToken: true,
356             }, {
357                 title: "Retry download",
358                 retry: true,
359             }, {
360                 title: "Dismiss",
361             });
362
363             if (selected?.updateToken) {
364                 await queryForGithubToken(state);
365                 continue;
366             } else if (selected?.retry) {
367                 continue;
368             }
369             throw e;
370         };
371     }
372 }
373
374 async function queryForGithubToken(state: PersistentState): Promise<void> {
375     const githubTokenOptions: vscode.InputBoxOptions = {
376         value: state.githubToken,
377         password: true,
378         prompt: `
379             This dialog allows to store a Github authorization token.
380             The usage of an authorization token will increase the rate
381             limit on the use of Github APIs and can thereby prevent getting
382             throttled.
383             Auth tokens can be created at https://github.com/settings/tokens`,
384     };
385
386     const newToken = await vscode.window.showInputBox(githubTokenOptions);
387     if (newToken === undefined) {
388         // The user aborted the dialog => Do not update the stored token
389         return;
390     }
391
392     if (newToken === "") {
393         log.info("Clearing github token");
394         await state.updateGithubToken(undefined);
395     } else {
396         log.info("Storing new github token");
397         await state.updateGithubToken(newToken);
398     }
399 }