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