]> git.lizzy.rs Git - rust.git/blob - editors/code/src/main.ts
Merge #6130 #6135
[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     ctx.registerCommand('openDocs', commands.openDocs);
114
115     defaultOnEnter.dispose();
116     ctx.registerCommand('onEnter', commands.onEnter);
117
118     ctx.registerCommand('ssr', commands.ssr);
119     ctx.registerCommand('serverVersion', commands.serverVersion);
120     ctx.registerCommand('toggleInlayHints', commands.toggleInlayHints);
121
122     // Internal commands which are invoked by the server.
123     ctx.registerCommand('runSingle', commands.runSingle);
124     ctx.registerCommand('debugSingle', commands.debugSingle);
125     ctx.registerCommand('showReferences', commands.showReferences);
126     ctx.registerCommand('applySnippetWorkspaceEdit', commands.applySnippetWorkspaceEditCommand);
127     ctx.registerCommand('resolveCodeAction', commands.resolveCodeAction);
128     ctx.registerCommand('applyActionGroup', commands.applyActionGroup);
129     ctx.registerCommand('gotoLocation', commands.gotoLocation);
130
131     ctx.pushCleanup(activateTaskProvider(workspaceFolder, ctx.config));
132
133     activateInlayHints(ctx);
134
135     vscode.workspace.onDidChangeConfiguration(
136         _ => ctx?.client?.sendNotification('workspace/didChangeConfiguration', { settings: "" }),
137         null,
138         ctx.subscriptions,
139     );
140 }
141
142 export async function deactivate() {
143     setContextValue(RUST_PROJECT_CONTEXT_NAME, undefined);
144     await ctx?.client.stop();
145     ctx = undefined;
146 }
147
148 async function bootstrap(config: Config, state: PersistentState): Promise<string> {
149     await fs.mkdir(config.globalStoragePath, { recursive: true });
150
151     await bootstrapExtension(config, state);
152     const path = await bootstrapServer(config, state);
153
154     return path;
155 }
156
157 async function bootstrapExtension(config: Config, state: PersistentState): Promise<void> {
158     if (config.package.releaseTag === null) return;
159     if (config.channel === "stable") {
160         if (config.package.releaseTag === NIGHTLY_TAG) {
161             void vscode.window.showWarningMessage(
162                 `You are running a nightly version of rust-analyzer extension. ` +
163                 `To switch to stable, uninstall the extension and re-install it from the marketplace`
164             );
165         }
166         return;
167     };
168
169     const now = Date.now();
170     if (config.package.releaseTag === NIGHTLY_TAG) {
171         // Check if we should poll github api for the new nightly version
172         // if we haven't done it during the past hour
173         const lastCheck = state.lastCheck;
174
175         const anHour = 60 * 60 * 1000;
176         const shouldCheckForNewNightly = state.releaseId === undefined || (now - (lastCheck ?? 0)) > anHour;
177
178         if (!shouldCheckForNewNightly) return;
179     }
180
181     const release = await downloadWithRetryDialog(state, async () => {
182         return await fetchRelease("nightly", state.githubToken);
183     }).catch((e) => {
184         log.error(e);
185         if (state.releaseId === undefined) { // Show error only for the initial download
186             vscode.window.showErrorMessage(`Failed to download rust-analyzer nightly ${e}`);
187         }
188         return undefined;
189     });
190     if (release === undefined || release.id === state.releaseId) return;
191
192     const userResponse = await vscode.window.showInformationMessage(
193         "New version of rust-analyzer (nightly) is available (requires reload).",
194         "Update"
195     );
196     if (userResponse !== "Update") return;
197
198     const artifact = release.assets.find(artifact => artifact.name === "rust-analyzer.vsix");
199     assert(!!artifact, `Bad release: ${JSON.stringify(release)}`);
200
201     const dest = path.join(config.globalStoragePath, "rust-analyzer.vsix");
202
203     await downloadWithRetryDialog(state, async () => {
204         await download({
205             url: artifact.browser_download_url,
206             dest,
207             progressTitle: "Downloading rust-analyzer extension",
208             overwrite: true,
209         });
210     });
211
212     await vscode.commands.executeCommand("workbench.extensions.installExtension", vscode.Uri.file(dest));
213     await fs.unlink(dest);
214
215     await state.updateReleaseId(release.id);
216     await state.updateLastCheck(now);
217     await vscode.commands.executeCommand("workbench.action.reloadWindow");
218 }
219
220 async function bootstrapServer(config: Config, state: PersistentState): Promise<string> {
221     const path = await getServer(config, state);
222     if (!path) {
223         throw new Error(
224             "Rust Analyzer Language Server is not available. " +
225             "Please, ensure its [proper installation](https://rust-analyzer.github.io/manual.html#installation)."
226         );
227     }
228
229     log.info("Using server binary at", path);
230
231     if (!isValidExecutable(path)) {
232         throw new Error(`Failed to execute ${path} --version`);
233     }
234
235     return path;
236 }
237
238 async function patchelf(dest: PathLike): Promise<void> {
239     await vscode.window.withProgress(
240         {
241             location: vscode.ProgressLocation.Notification,
242             title: "Patching rust-analyzer for NixOS"
243         },
244         async (progress, _) => {
245             const expression = `
246             {src, pkgs ? import <nixpkgs> {}}:
247                 pkgs.stdenv.mkDerivation {
248                     name = "rust-analyzer";
249                     inherit src;
250                     phases = [ "installPhase" "fixupPhase" ];
251                     installPhase = "cp $src $out";
252                     fixupPhase = ''
253                     chmod 755 $out
254                     patchelf --set-interpreter "$(cat $NIX_CC/nix-support/dynamic-linker)" $out
255                     '';
256                 }
257             `;
258             const origFile = dest + "-orig";
259             await fs.rename(dest, origFile);
260             progress.report({ message: "Patching executable", increment: 20 });
261             await new Promise((resolve, reject) => {
262                 const handle = exec(`nix-build -E - --arg src '${origFile}' -o ${dest}`,
263                     (err, stdout, stderr) => {
264                         if (err != null) {
265                             reject(Error(stderr));
266                         } else {
267                             resolve(stdout);
268                         }
269                     });
270                 handle.stdin?.write(expression);
271                 handle.stdin?.end();
272             });
273             await fs.unlink(origFile);
274         }
275     );
276 }
277
278 async function getServer(config: Config, state: PersistentState): Promise<string | undefined> {
279     const explicitPath = process.env.__RA_LSP_SERVER_DEBUG ?? config.serverPath;
280     if (explicitPath) {
281         if (explicitPath.startsWith("~/")) {
282             return os.homedir() + explicitPath.slice("~".length);
283         }
284         return explicitPath;
285     };
286     if (config.package.releaseTag === null) return "rust-analyzer";
287
288     let platform: string | undefined;
289     if (process.arch === "x64" || process.arch === "ia32") {
290         if (process.platform === "linux") platform = "linux";
291         if (process.platform === "darwin") platform = "mac";
292         if (process.platform === "win32") platform = "windows";
293     }
294     if (platform === undefined) {
295         vscode.window.showErrorMessage(
296             "Unfortunately we don't ship binaries for your platform yet. " +
297             "You need to manually clone rust-analyzer repository and " +
298             "run `cargo xtask install --server` to build the language server from sources. " +
299             "If you feel that your platform should be supported, please create an issue " +
300             "about that [here](https://github.com/rust-analyzer/rust-analyzer/issues) and we " +
301             "will consider it."
302         );
303         return undefined;
304     }
305     const ext = platform === "windows" ? ".exe" : "";
306     const dest = path.join(config.globalStoragePath, `rust-analyzer-${platform}${ext}`);
307     const exists = await fs.stat(dest).then(() => true, () => false);
308     if (!exists) {
309         await state.updateServerVersion(undefined);
310     }
311
312     if (state.serverVersion === config.package.version) return dest;
313
314     if (config.askBeforeDownload) {
315         const userResponse = await vscode.window.showInformationMessage(
316             `Language server version ${config.package.version} for rust-analyzer is not installed.`,
317             "Download now"
318         );
319         if (userResponse !== "Download now") return dest;
320     }
321
322     const releaseTag = config.package.releaseTag;
323     const release = await downloadWithRetryDialog(state, async () => {
324         return await fetchRelease(releaseTag, state.githubToken);
325     });
326     const artifact = release.assets.find(artifact => artifact.name === `rust-analyzer-${platform}.gz`);
327     assert(!!artifact, `Bad release: ${JSON.stringify(release)}`);
328
329     await downloadWithRetryDialog(state, async () => {
330         await download({
331             url: artifact.browser_download_url,
332             dest,
333             progressTitle: "Downloading rust-analyzer server",
334             gunzip: true,
335             mode: 0o755,
336             overwrite: true,
337         });
338     });
339
340     // Patching executable if that's NixOS.
341     if (await fs.stat("/etc/nixos").then(_ => true).catch(_ => false)) {
342         await patchelf(dest);
343     }
344
345     await state.updateServerVersion(config.package.version);
346     return dest;
347 }
348
349 async function downloadWithRetryDialog<T>(state: PersistentState, downloadFunc: () => Promise<T>): Promise<T> {
350     while (true) {
351         try {
352             return await downloadFunc();
353         } catch (e) {
354             const selected = await vscode.window.showErrorMessage("Failed to download: " + e.message, {}, {
355                 title: "Update Github Auth Token",
356                 updateToken: true,
357             }, {
358                 title: "Retry download",
359                 retry: true,
360             }, {
361                 title: "Dismiss",
362             });
363
364             if (selected?.updateToken) {
365                 await queryForGithubToken(state);
366                 continue;
367             } else if (selected?.retry) {
368                 continue;
369             }
370             throw e;
371         };
372     }
373 }
374
375 async function queryForGithubToken(state: PersistentState): Promise<void> {
376     const githubTokenOptions: vscode.InputBoxOptions = {
377         value: state.githubToken,
378         password: true,
379         prompt: `
380             This dialog allows to store a Github authorization token.
381             The usage of an authorization token will increase the rate
382             limit on the use of Github APIs and can thereby prevent getting
383             throttled.
384             Auth tokens can be created at https://github.com/settings/tokens`,
385     };
386
387     const newToken = await vscode.window.showInputBox(githubTokenOptions);
388     if (newToken === undefined) {
389         // The user aborted the dialog => Do not update the stored token
390         return;
391     }
392
393     if (newToken === "") {
394         log.info("Clearing github token");
395         await state.updateGithubToken(undefined);
396     } else {
397         log.info("Storing new github token");
398         await state.updateGithubToken(newToken);
399     }
400 }