]> git.lizzy.rs Git - rust.git/blob - editors/code/src/main.ts
Merge pull request #4495 from vsrs/fixture_meta
[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 { activateStatusDisplay } from './status_display';
9 import { Ctx } from './ctx';
10 import { Config, NIGHTLY_TAG } from './config';
11 import { log, assert, isValidExecutable } from './util';
12 import { PersistentState } from './persistent_state';
13 import { fetchRelease, download } from './net';
14 import { activateTaskProvider } from './tasks';
15 import { exec } from 'child_process';
16
17 let ctx: Ctx | undefined;
18
19 export async function activate(context: vscode.ExtensionContext) {
20     // Register a "dumb" onEnter command for the case where server fails to
21     // start.
22     //
23     // FIXME: refactor command registration code such that commands are
24     // **always** registered, even if the server does not start. Use API like
25     // this perhaps?
26     //
27     // ```TypeScript
28     // registerCommand(
29     //    factory: (Ctx) => ((Ctx) => any),
30     //    fallback: () => any = () => vscode.window.showErrorMessage(
31     //        "rust-analyzer is not available"
32     //    ),
33     // )
34     const defaultOnEnter = vscode.commands.registerCommand(
35         'rust-analyzer.onEnter',
36         () => vscode.commands.executeCommand('default:type', { text: '\n' }),
37     );
38     context.subscriptions.push(defaultOnEnter);
39
40     const config = new Config(context);
41     const state = new PersistentState(context.globalState);
42     const serverPath = await bootstrap(config, state);
43
44     const workspaceFolder = vscode.workspace.workspaceFolders?.[0];
45     if (workspaceFolder === undefined) {
46         const err = "Cannot activate rust-analyzer when no folder is opened";
47         void vscode.window.showErrorMessage(err);
48         throw new Error(err);
49     }
50
51     // Note: we try to start the server before we activate type hints so that it
52     // registers its `onDidChangeDocument` handler before us.
53     //
54     // This a horribly, horribly wrong way to deal with this problem.
55     ctx = await Ctx.create(config, context, serverPath, workspaceFolder.uri.fsPath);
56
57     // Commands which invokes manually via command palette, shortcut, etc.
58
59     // Reloading is inspired by @DanTup maneuver: https://github.com/microsoft/vscode/issues/45774#issuecomment-373423895
60     ctx.registerCommand('reload', _ => async () => {
61         void vscode.window.showInformationMessage('Reloading rust-analyzer...');
62         await deactivate();
63         while (context.subscriptions.length > 0) {
64             try {
65                 context.subscriptions.pop()!.dispose();
66             } catch (err) {
67                 log.error("Dispose error:", err);
68             }
69         }
70         await activate(context).catch(log.error);
71     });
72
73     ctx.registerCommand('analyzerStatus', commands.analyzerStatus);
74     ctx.registerCommand('collectGarbage', commands.collectGarbage);
75     ctx.registerCommand('matchingBrace', commands.matchingBrace);
76     ctx.registerCommand('joinLines', commands.joinLines);
77     ctx.registerCommand('parentModule', commands.parentModule);
78     ctx.registerCommand('syntaxTree', commands.syntaxTree);
79     ctx.registerCommand('expandMacro', commands.expandMacro);
80     ctx.registerCommand('run', commands.run);
81     ctx.registerCommand('debug', commands.debug);
82     ctx.registerCommand('newDebugConfig', commands.newDebugConfig);
83
84     defaultOnEnter.dispose();
85     ctx.registerCommand('onEnter', commands.onEnter);
86
87     ctx.registerCommand('ssr', commands.ssr);
88     ctx.registerCommand('serverVersion', commands.serverVersion);
89
90     // Internal commands which are invoked by the server.
91     ctx.registerCommand('runSingle', commands.runSingle);
92     ctx.registerCommand('debugSingle', commands.debugSingle);
93     ctx.registerCommand('showReferences', commands.showReferences);
94     ctx.registerCommand('applySourceChange', commands.applySourceChange);
95     ctx.registerCommand('applySnippetWorkspaceEdit', commands.applySnippetWorkspaceEditCommand);
96     ctx.registerCommand('applyActionGroup', commands.applyActionGroup);
97
98     ctx.pushCleanup(activateTaskProvider(workspaceFolder));
99
100     activateStatusDisplay(ctx);
101
102     activateInlayHints(ctx);
103
104     vscode.workspace.onDidChangeConfiguration(
105         _ => ctx?.client?.sendNotification('workspace/didChangeConfiguration', { settings: "" }),
106         null,
107         ctx.subscriptions,
108     );
109 }
110
111 export async function deactivate() {
112     await ctx?.client.stop();
113     ctx = undefined;
114 }
115
116 async function bootstrap(config: Config, state: PersistentState): Promise<string> {
117     await fs.mkdir(config.globalStoragePath, { recursive: true });
118
119     await bootstrapExtension(config, state);
120     const path = await bootstrapServer(config, state);
121
122     return path;
123 }
124
125 async function bootstrapExtension(config: Config, state: PersistentState): Promise<void> {
126     if (config.package.releaseTag === null) return;
127     if (config.channel === "stable") {
128         if (config.package.releaseTag === NIGHTLY_TAG) {
129             void vscode.window.showWarningMessage(
130                 `You are running a nightly version of rust-analyzer extension. ` +
131                 `To switch to stable, uninstall the extension and re-install it from the marketplace`
132             );
133         }
134         return;
135     };
136
137     const lastCheck = state.lastCheck;
138     const now = Date.now();
139
140     const anHour = 60 * 60 * 1000;
141     const shouldDownloadNightly = state.releaseId === undefined || (now - (lastCheck ?? 0)) > anHour;
142
143     if (!shouldDownloadNightly) return;
144
145     const release = await fetchRelease("nightly").catch((e) => {
146         log.error(e);
147         if (state.releaseId === undefined) { // Show error only for the initial download
148             vscode.window.showErrorMessage(`Failed to download rust-analyzer nightly ${e}`);
149         }
150         return undefined;
151     });
152     if (release === undefined || release.id === state.releaseId) return;
153
154     const userResponse = await vscode.window.showInformationMessage(
155         "New version of rust-analyzer (nightly) is available (requires reload).",
156         "Update"
157     );
158     if (userResponse !== "Update") return;
159
160     const artifact = release.assets.find(artifact => artifact.name === "rust-analyzer.vsix");
161     assert(!!artifact, `Bad release: ${JSON.stringify(release)}`);
162
163     const dest = path.join(config.globalStoragePath, "rust-analyzer.vsix");
164     await download(artifact.browser_download_url, dest, "Downloading rust-analyzer extension");
165
166     await vscode.commands.executeCommand("workbench.extensions.installExtension", vscode.Uri.file(dest));
167     await fs.unlink(dest);
168
169     await state.updateReleaseId(release.id);
170     await state.updateLastCheck(now);
171     await vscode.commands.executeCommand("workbench.action.reloadWindow");
172 }
173
174 async function bootstrapServer(config: Config, state: PersistentState): Promise<string> {
175     const path = await getServer(config, state);
176     if (!path) {
177         throw new Error(
178             "Rust Analyzer Language Server is not available. " +
179             "Please, ensure its [proper installation](https://rust-analyzer.github.io/manual.html#installation)."
180         );
181     }
182
183     log.debug("Using server binary at", path);
184
185     if (!isValidExecutable(path)) {
186         throw new Error(`Failed to execute ${path} --version`);
187     }
188
189     return path;
190 }
191
192 async function patchelf(dest: PathLike): Promise<void> {
193     await vscode.window.withProgress(
194         {
195             location: vscode.ProgressLocation.Notification,
196             title: "Patching rust-analyzer for NixOS"
197         },
198         async (progress, _) => {
199             const expression = `
200             {src, pkgs ? import <nixpkgs> {}}:
201                 pkgs.stdenv.mkDerivation {
202                     name = "rust-analyzer";
203                     inherit src;
204                     phases = [ "installPhase" "fixupPhase" ];
205                     installPhase = "cp $src $out";
206                     fixupPhase = ''
207                     chmod 755 $out
208                     patchelf --set-interpreter "$(cat $NIX_CC/nix-support/dynamic-linker)" $out
209                     '';
210                 }
211             `;
212             const origFile = dest + "-orig";
213             await fs.rename(dest, origFile);
214             progress.report({ message: "Patching executable", increment: 20 });
215             await new Promise((resolve, reject) => {
216                 const handle = exec(`nix-build -E - --arg src '${origFile}' -o ${dest}`,
217                     (err, stdout, stderr) => {
218                         if (err != null) {
219                             reject(Error(stderr));
220                         } else {
221                             resolve(stdout);
222                         }
223                     });
224                 handle.stdin?.write(expression);
225                 handle.stdin?.end();
226             });
227             await fs.unlink(origFile);
228         }
229     );
230 }
231
232 async function getServer(config: Config, state: PersistentState): Promise<string | undefined> {
233     const explicitPath = process.env.__RA_LSP_SERVER_DEBUG ?? config.serverPath;
234     if (explicitPath) {
235         if (explicitPath.startsWith("~/")) {
236             return os.homedir() + explicitPath.slice("~".length);
237         }
238         return explicitPath;
239     };
240     if (config.package.releaseTag === null) return "rust-analyzer";
241
242     let binaryName: string | undefined = undefined;
243     if (process.arch === "x64" || process.arch === "ia32") {
244         if (process.platform === "linux") binaryName = "rust-analyzer-linux";
245         if (process.platform === "darwin") binaryName = "rust-analyzer-mac";
246         if (process.platform === "win32") binaryName = "rust-analyzer-windows.exe";
247     }
248     if (binaryName === undefined) {
249         vscode.window.showErrorMessage(
250             "Unfortunately we don't ship binaries for your platform yet. " +
251             "You need to manually clone rust-analyzer repository and " +
252             "run `cargo xtask install --server` to build the language server from sources. " +
253             "If you feel that your platform should be supported, please create an issue " +
254             "about that [here](https://github.com/rust-analyzer/rust-analyzer/issues) and we " +
255             "will consider it."
256         );
257         return undefined;
258     }
259
260     const dest = path.join(config.globalStoragePath, binaryName);
261     const exists = await fs.stat(dest).then(() => true, () => false);
262     if (!exists) {
263         await state.updateServerVersion(undefined);
264     }
265
266     if (state.serverVersion === config.package.version) return dest;
267
268     if (config.askBeforeDownload) {
269         const userResponse = await vscode.window.showInformationMessage(
270             `Language server version ${config.package.version} for rust-analyzer is not installed.`,
271             "Download now"
272         );
273         if (userResponse !== "Download now") return dest;
274     }
275
276     const release = await fetchRelease(config.package.releaseTag);
277     const artifact = release.assets.find(artifact => artifact.name === binaryName);
278     assert(!!artifact, `Bad release: ${JSON.stringify(release)}`);
279
280     await download(artifact.browser_download_url, dest, "Downloading rust-analyzer server", { mode: 0o755 });
281
282     // Patching executable if that's NixOS.
283     if (await fs.stat("/etc/nixos").then(_ => true).catch(_ => false)) {
284         await patchelf(dest);
285     }
286
287     await state.updateServerVersion(config.package.version);
288     return dest;
289 }