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