]> git.lizzy.rs Git - rust.git/blob - editors/code/src/main.ts
Fix clearing the token
[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
114     defaultOnEnter.dispose();
115     ctx.registerCommand('onEnter', commands.onEnter);
116
117     ctx.registerCommand('ssr', commands.ssr);
118     ctx.registerCommand('serverVersion', commands.serverVersion);
119     ctx.registerCommand('toggleInlayHints', commands.toggleInlayHints);
120
121     // Internal commands which are invoked by the server.
122     ctx.registerCommand('runSingle', commands.runSingle);
123     ctx.registerCommand('debugSingle', commands.debugSingle);
124     ctx.registerCommand('showReferences', commands.showReferences);
125     ctx.registerCommand('applySnippetWorkspaceEdit', commands.applySnippetWorkspaceEditCommand);
126     ctx.registerCommand('resolveCodeAction', commands.resolveCodeAction);
127     ctx.registerCommand('applyActionGroup', commands.applyActionGroup);
128     ctx.registerCommand('gotoLocation', commands.gotoLocation);
129
130     ctx.pushCleanup(activateTaskProvider(workspaceFolder, ctx.config));
131
132     activateInlayHints(ctx);
133
134     vscode.workspace.onDidChangeConfiguration(
135         _ => ctx?.client?.sendNotification('workspace/didChangeConfiguration', { settings: "" }),
136         null,
137         ctx.subscriptions,
138     );
139 }
140
141 export async function deactivate() {
142     setContextValue(RUST_PROJECT_CONTEXT_NAME, undefined);
143     await ctx?.client.stop();
144     ctx = undefined;
145 }
146
147 async function bootstrap(config: Config, state: PersistentState): Promise<string> {
148     await fs.mkdir(config.globalStoragePath, { recursive: true });
149
150     await bootstrapExtension(config, state);
151     const path = await bootstrapServer(config, state);
152
153     return path;
154 }
155
156 async function bootstrapExtension(config: Config, state: PersistentState): Promise<void> {
157     if (config.package.releaseTag === null) return;
158     if (config.channel === "stable") {
159         if (config.package.releaseTag === NIGHTLY_TAG) {
160             void vscode.window.showWarningMessage(
161                 `You are running a nightly version of rust-analyzer extension. ` +
162                 `To switch to stable, uninstall the extension and re-install it from the marketplace`
163             );
164         }
165         return;
166     };
167
168     const now = Date.now();
169     if (config.package.releaseTag === NIGHTLY_TAG) {
170         // Check if we should poll github api for the new nightly version
171         // if we haven't done it during the past hour
172         const lastCheck = state.lastCheck;
173
174         const anHour = 60 * 60 * 1000;
175         const shouldCheckForNewNightly = state.releaseId === undefined || (now - (lastCheck ?? 0)) > anHour;
176
177         if (!shouldCheckForNewNightly) return;
178     }
179
180     const release = await performDownloadWithRetryDialog(async () => {
181         return await fetchRelease("nightly", state.githubToken);
182     }, state).catch((e) => {
183         log.error(e);
184         if (state.releaseId === undefined) { // Show error only for the initial download
185             vscode.window.showErrorMessage(`Failed to download rust-analyzer nightly ${e}`);
186         }
187         return undefined;
188     });
189     if (release === undefined || release.id === state.releaseId) return;
190
191     const userResponse = await vscode.window.showInformationMessage(
192         "New version of rust-analyzer (nightly) is available (requires reload).",
193         "Update"
194     );
195     if (userResponse !== "Update") return;
196
197     const artifact = release.assets.find(artifact => artifact.name === "rust-analyzer.vsix");
198     assert(!!artifact, `Bad release: ${JSON.stringify(release)}`);
199
200     const dest = path.join(config.globalStoragePath, "rust-analyzer.vsix");
201
202     await performDownloadWithRetryDialog(async () => {
203         // Unlinking the exe file before moving new one on its place should prevent ETXTBSY error.
204         await fs.unlink(dest).catch(err => {
205             if (err.code !== "ENOENT") throw err;
206         });
207
208         await download({
209             url: artifact.browser_download_url,
210             dest,
211             progressTitle: "Downloading rust-analyzer extension",
212         });
213     }, state);
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 = process.env.__RA_LSP_SERVER_DEBUG ?? config.serverPath;
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     let platform: string | undefined;
292     if (process.arch === "x64" || process.arch === "ia32") {
293         if (process.platform === "linux") platform = "linux";
294         if (process.platform === "darwin") platform = "mac";
295         if (process.platform === "win32") platform = "windows";
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 performDownloadWithRetryDialog(async () => {
327         return await fetchRelease(releaseTag, state.githubToken);
328     }, state);
329     const artifact = release.assets.find(artifact => artifact.name === `rust-analyzer-${platform}.gz`);
330     assert(!!artifact, `Bad release: ${JSON.stringify(release)}`);
331
332     await performDownloadWithRetryDialog(async () => {
333         // Unlinking the exe file before moving new one on its place should prevent ETXTBSY error.
334         await fs.unlink(dest).catch(err => {
335             if (err.code !== "ENOENT") throw err;
336         });
337
338         await download({
339             url: artifact.browser_download_url,
340             dest,
341             progressTitle: "Downloading rust-analyzer server",
342             gunzip: true,
343             mode: 0o755
344         });
345     }, state);
346
347     // Patching executable if that's NixOS.
348     if (await fs.stat("/etc/nixos").then(_ => true).catch(_ => false)) {
349         await patchelf(dest);
350     }
351
352     await state.updateServerVersion(config.package.version);
353     return dest;
354 }
355
356 async function performDownloadWithRetryDialog<T>(downloadFunc: () => Promise<T>, state: PersistentState): Promise<T> {
357     while (true) {
358         try {
359             return await downloadFunc();
360         } catch (e) {
361             const selected = await vscode.window.showErrorMessage("Failed perform download: " + e.message, {}, {
362                 title: "Update Github Auth Token",
363                 updateToken: true,
364             }, {
365                 title: "Retry download",
366                 retry: true,
367             }, {
368                 title: "Dismiss",
369             });
370
371             if (selected?.updateToken) {
372                 await queryForGithubToken(state);
373                 continue;
374             } else if (selected?.retry) {
375                 continue;
376             }
377             throw e;
378         };
379     }
380
381 }
382
383 async function queryForGithubToken(state: PersistentState): Promise<void> {
384     const githubTokenOptions: vscode.InputBoxOptions = {
385         value: state.githubToken,
386         password: true,
387         prompt: `
388             This dialog allows to store a Github authorization token.
389             The usage of an authorization token will increase the rate
390             limit on the use of Github APIs and can thereby prevent getting
391             throttled.
392             Auth tokens can be created at https://github.com/settings/tokens`,
393     };
394
395     const newToken = await vscode.window.showInputBox(githubTokenOptions);
396     if (newToken !== undefined) {
397         if (newToken === "") {
398             log.info("Clearing github token");
399             await state.updateGithubToken(undefined);
400         } else {
401             log.info("Storing new github token");
402             await state.updateGithubToken(newToken);
403         }
404     }
405 }