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