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