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