]> git.lizzy.rs Git - rust.git/blob - editors/code/src/main.ts
Merge #3708
[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 } 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 { activateHighlighting } from './highlighting';
11 import { Config, NIGHTLY_TAG } from './config';
12 import { log, assert } from './util';
13 import { PersistentState } from './persistent_state';
14 import { fetchRelease, download } from './net';
15 import { spawnSync } 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     // Note: we try to start the server before we activate type hints so that it
45     // registers its `onDidChangeDocument` handler before us.
46     //
47     // This a horribly, horribly wrong way to deal with this problem.
48     ctx = await Ctx.create(config, context, serverPath);
49
50     // Commands which invokes manually via command palette, shortcut, etc.
51     ctx.registerCommand('reload', (ctx) => {
52         return async () => {
53             vscode.window.showInformationMessage('Reloading rust-analyzer...');
54             // @DanTup maneuver
55             // https://github.com/microsoft/vscode/issues/45774#issuecomment-373423895
56             await deactivate();
57             for (const sub of ctx.subscriptions) {
58                 try {
59                     sub.dispose();
60                 } catch (e) {
61                     log.error(e);
62                 }
63             }
64             await activate(context);
65         };
66     });
67
68     ctx.registerCommand('analyzerStatus', commands.analyzerStatus);
69     ctx.registerCommand('collectGarbage', commands.collectGarbage);
70     ctx.registerCommand('matchingBrace', commands.matchingBrace);
71     ctx.registerCommand('joinLines', commands.joinLines);
72     ctx.registerCommand('parentModule', commands.parentModule);
73     ctx.registerCommand('syntaxTree', commands.syntaxTree);
74     ctx.registerCommand('expandMacro', commands.expandMacro);
75     ctx.registerCommand('run', commands.run);
76
77     defaultOnEnter.dispose();
78     ctx.registerCommand('onEnter', commands.onEnter);
79
80     ctx.registerCommand('ssr', commands.ssr);
81     ctx.registerCommand('serverVersion', commands.serverVersion);
82
83     // Internal commands which are invoked by the server.
84     ctx.registerCommand('runSingle', commands.runSingle);
85     ctx.registerCommand('debugSingle', commands.debugSingle);
86     ctx.registerCommand('showReferences', commands.showReferences);
87     ctx.registerCommand('applySourceChange', commands.applySourceChange);
88     ctx.registerCommand('selectAndApplySourceChange', commands.selectAndApplySourceChange);
89
90     activateStatusDisplay(ctx);
91
92     if (!ctx.config.highlightingSemanticTokens) {
93         activateHighlighting(ctx);
94     }
95     activateInlayHints(ctx);
96 }
97
98 export async function deactivate() {
99     await ctx?.client?.stop();
100     ctx = undefined;
101 }
102
103 async function bootstrap(config: Config, state: PersistentState): Promise<string> {
104     await fs.mkdir(config.globalStoragePath, { recursive: true });
105
106     await bootstrapExtension(config, state);
107     const path = await bootstrapServer(config, state);
108
109     return path;
110 }
111
112 async function bootstrapExtension(config: Config, state: PersistentState): Promise<void> {
113     if (config.package.releaseTag === undefined) return;
114     if (config.channel === "stable") {
115         if (config.package.releaseTag === NIGHTLY_TAG) {
116             vscode.window.showWarningMessage(`You are running a nightly version of rust-analyzer extension.
117 To switch to stable, uninstall the extension and re-install it from the marketplace`);
118         }
119         return;
120     };
121
122     const lastCheck = state.lastCheck;
123     const now = Date.now();
124
125     const anHour = 60 * 60 * 1000;
126     const shouldDownloadNightly = state.releaseId === undefined || (now - (lastCheck ?? 0)) > anHour;
127
128     if (!shouldDownloadNightly) return;
129
130     const release = await fetchRelease("nightly").catch((e) => {
131         log.error(e);
132         if (state.releaseId === undefined) { // Show error only for the initial download
133             vscode.window.showErrorMessage(`Failed to download rust-analyzer nightly ${e}`);
134         }
135         return undefined;
136     });
137     if (release === undefined || release.id === state.releaseId) return;
138
139     const userResponse = await vscode.window.showInformationMessage(
140         "New version of rust-analyzer (nightly) is available (requires reload).",
141         "Update"
142     );
143     if (userResponse !== "Update") return;
144
145     const artifact = release.assets.find(artifact => artifact.name === "rust-analyzer.vsix");
146     assert(!!artifact, `Bad release: ${JSON.stringify(release)}`);
147
148     const dest = path.join(config.globalStoragePath, "rust-analyzer.vsix");
149     await download(artifact.browser_download_url, dest, "Downloading rust-analyzer extension");
150
151     await vscode.commands.executeCommand("workbench.extensions.installExtension", vscode.Uri.file(dest));
152     await fs.unlink(dest);
153
154     await state.updateReleaseId(release.id);
155     await state.updateLastCheck(now);
156     await vscode.commands.executeCommand("workbench.action.reloadWindow");
157 }
158
159 async function bootstrapServer(config: Config, state: PersistentState): Promise<string> {
160     const path = await getServer(config, state);
161     if (!path) {
162         throw new Error(
163             "Rust Analyzer Language Server is not available. " +
164             "Please, ensure its [proper installation](https://rust-analyzer.github.io/manual.html#installation)."
165         );
166     }
167
168     const res = spawnSync(path, ["--version"], { encoding: 'utf8' });
169     log.debug("Checked binary availability via --version", res);
170     log.debug(res, "--version output:", res.output);
171     if (res.status !== 0) {
172         throw new Error(
173             `Failed to execute ${path} --version`
174         );
175     }
176
177     return path;
178 }
179
180 async function getServer(config: Config, state: PersistentState): Promise<string | undefined> {
181     const explicitPath = process.env.__RA_LSP_SERVER_DEBUG ?? config.serverPath;
182     if (explicitPath) {
183         if (explicitPath.startsWith("~/")) {
184             return os.homedir() + explicitPath.slice("~".length);
185         }
186         return explicitPath;
187     };
188     if (config.package.releaseTag === undefined) return "rust-analyzer";
189
190     let binaryName: string | undefined = undefined;
191     if (process.arch === "x64" || process.arch === "x32") {
192         if (process.platform === "linux") binaryName = "rust-analyzer-linux";
193         if (process.platform === "darwin") binaryName = "rust-analyzer-mac";
194         if (process.platform === "win32") binaryName = "rust-analyzer-windows.exe";
195     }
196     if (binaryName === undefined) {
197         vscode.window.showErrorMessage(
198             "Unfortunately we don't ship binaries for your platform yet. " +
199             "You need to manually clone rust-analyzer repository and " +
200             "run `cargo xtask install --server` to build the language server from sources. " +
201             "If you feel that your platform should be supported, please create an issue " +
202             "about that [here](https://github.com/rust-analyzer/rust-analyzer/issues) and we " +
203             "will consider it."
204         );
205         return undefined;
206     }
207
208     const dest = path.join(config.globalStoragePath, binaryName);
209     const exists = await fs.stat(dest).then(() => true, () => false);
210     if (!exists) {
211         await state.updateServerVersion(undefined);
212     }
213
214     if (state.serverVersion === config.package.version) return dest;
215
216     if (config.askBeforeDownload) {
217         const userResponse = await vscode.window.showInformationMessage(
218             `Language server version ${config.package.version} for rust-analyzer is not installed.`,
219             "Download now"
220         );
221         if (userResponse !== "Download now") return dest;
222     }
223
224     const release = await fetchRelease(config.package.releaseTag);
225     const artifact = release.assets.find(artifact => artifact.name === binaryName);
226     assert(!!artifact, `Bad release: ${JSON.stringify(release)}`);
227
228     await download(artifact.browser_download_url, dest, "Downloading rust-analyzer server", { mode: 0o755 });
229     await state.updateServerVersion(config.package.version);
230     return dest;
231 }