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