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";
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';
17 let ctx: Ctx | undefined;
19 const RUST_PROJECT_CONTEXT_NAME = "inRustProject";
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}`);
31 async function tryActivate(context: vscode.ExtensionContext) {
32 // Register a "dumb" onEnter command for the case where server fails to
35 // FIXME: refactor command registration code such that commands are
36 // **always** registered, even if the server does not start. Use API like
41 // factory: (Ctx) => ((Ctx) => any),
42 // fallback: () => any = () => vscode.window.showErrorMessage(
43 // "rust-analyzer is not available"
46 const defaultOnEnter = vscode.commands.registerCommand(
47 'rust-analyzer.onEnter',
48 () => vscode.commands.executeCommand('default:type', { text: '\n' }),
50 context.subscriptions.push(defaultOnEnter);
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. ";
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. ";
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 }';
65 log.error("Bootstrap error", err);
66 throw new Error(message);
69 const workspaceFolder = vscode.workspace.workspaceFolders?.[0];
70 if (workspaceFolder === undefined) {
71 throw new Error("no folder is opened");
74 // Note: we try to start the server before we activate type hints so that it
75 // registers its `onDidChangeDocument` handler before us.
77 // This a horribly, horribly wrong way to deal with this problem.
78 ctx = await Ctx.create(config, context, serverPath, workspaceFolder.uri.fsPath);
80 setContextValue(RUST_PROJECT_CONTEXT_NAME, true);
82 // Commands which invokes manually via command palette, shortcut, etc.
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...');
88 while (context.subscriptions.length > 0) {
90 context.subscriptions.pop()!.dispose();
92 log.error("Dispose error:", err);
95 await activate(context).catch(log.error);
98 ctx.registerCommand('updateGithubToken', ctx => async () => {
99 await queryForGithubToken(new PersistentState(ctx.globalState));
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 ctx.registerCommand('openDocs', commands.openDocs);
114 ctx.registerCommand('openCargoToml', commands.openCargoToml);
116 defaultOnEnter.dispose();
117 ctx.registerCommand('onEnter', commands.onEnter);
119 ctx.registerCommand('ssr', commands.ssr);
120 ctx.registerCommand('serverVersion', commands.serverVersion);
121 ctx.registerCommand('toggleInlayHints', commands.toggleInlayHints);
123 // Internal commands which are invoked by the server.
124 ctx.registerCommand('runSingle', commands.runSingle);
125 ctx.registerCommand('debugSingle', commands.debugSingle);
126 ctx.registerCommand('showReferences', commands.showReferences);
127 ctx.registerCommand('applySnippetWorkspaceEdit', commands.applySnippetWorkspaceEditCommand);
128 ctx.registerCommand('resolveCodeAction', commands.resolveCodeAction);
129 ctx.registerCommand('applyActionGroup', commands.applyActionGroup);
130 ctx.registerCommand('gotoLocation', commands.gotoLocation);
132 ctx.pushCleanup(activateTaskProvider(workspaceFolder, ctx.config));
134 activateInlayHints(ctx);
136 vscode.workspace.onDidChangeConfiguration(
137 _ => ctx?.client?.sendNotification('workspace/didChangeConfiguration', { settings: "" }),
143 export async function deactivate() {
144 setContextValue(RUST_PROJECT_CONTEXT_NAME, undefined);
145 await ctx?.client.stop();
149 async function bootstrap(config: Config, state: PersistentState): Promise<string> {
150 await fs.mkdir(config.globalStoragePath, { recursive: true });
152 await bootstrapExtension(config, state);
153 const path = await bootstrapServer(config, state);
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`
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;
176 const anHour = 60 * 60 * 1000;
177 const shouldCheckForNewNightly = state.releaseId === undefined || (now - (lastCheck ?? 0)) > anHour;
179 if (!shouldCheckForNewNightly) return;
182 const release = await downloadWithRetryDialog(state, async () => {
183 return await fetchRelease("nightly", state.githubToken);
186 if (state.releaseId === undefined) { // Show error only for the initial download
187 vscode.window.showErrorMessage(`Failed to download rust-analyzer nightly ${e}`);
191 if (release === undefined || release.id === state.releaseId) return;
193 const userResponse = await vscode.window.showInformationMessage(
194 "New version of rust-analyzer (nightly) is available (requires reload).",
197 if (userResponse !== "Update") return;
199 const artifact = release.assets.find(artifact => artifact.name === "rust-analyzer.vsix");
200 assert(!!artifact, `Bad release: ${JSON.stringify(release)}`);
202 const dest = path.join(config.globalStoragePath, "rust-analyzer.vsix");
204 await downloadWithRetryDialog(state, async () => {
206 url: artifact.browser_download_url,
208 progressTitle: "Downloading rust-analyzer extension",
213 await vscode.commands.executeCommand("workbench.extensions.installExtension", vscode.Uri.file(dest));
214 await fs.unlink(dest);
216 await state.updateReleaseId(release.id);
217 await state.updateLastCheck(now);
218 await vscode.commands.executeCommand("workbench.action.reloadWindow");
221 async function bootstrapServer(config: Config, state: PersistentState): Promise<string> {
222 const path = await getServer(config, state);
225 "Rust Analyzer Language Server is not available. " +
226 "Please, ensure its [proper installation](https://rust-analyzer.github.io/manual.html#installation)."
230 log.info("Using server binary at", path);
232 if (!isValidExecutable(path)) {
233 throw new Error(`Failed to execute ${path} --version`);
239 async function patchelf(dest: PathLike): Promise<void> {
240 await vscode.window.withProgress(
242 location: vscode.ProgressLocation.Notification,
243 title: "Patching rust-analyzer for NixOS"
245 async (progress, _) => {
247 {src, pkgs ? import <nixpkgs> {}}:
248 pkgs.stdenv.mkDerivation {
249 name = "rust-analyzer";
251 phases = [ "installPhase" "fixupPhase" ];
252 installPhase = "cp $src $out";
255 patchelf --set-interpreter "$(cat $NIX_CC/nix-support/dynamic-linker)" $out
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) => {
266 reject(Error(stderr));
271 handle.stdin?.write(expression);
274 await fs.unlink(origFile);
279 async function getServer(config: Config, state: PersistentState): Promise<string | undefined> {
280 const explicitPath = process.env.__RA_LSP_SERVER_DEBUG ?? config.serverPath;
282 if (explicitPath.startsWith("~/")) {
283 return os.homedir() + explicitPath.slice("~".length);
287 if (config.package.releaseTag === null) return "rust-analyzer";
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";
295 if (platform === undefined) {
296 vscode.window.showErrorMessage(
297 "Unfortunately we don't ship binaries for your platform yet. " +
298 "You need to manually clone rust-analyzer repository and " +
299 "run `cargo xtask install --server` to build the language server from sources. " +
300 "If you feel that your platform should be supported, please create an issue " +
301 "about that [here](https://github.com/rust-analyzer/rust-analyzer/issues) and we " +
306 const ext = platform === "windows" ? ".exe" : "";
307 const dest = path.join(config.globalStoragePath, `rust-analyzer-${platform}${ext}`);
308 const exists = await fs.stat(dest).then(() => true, () => false);
310 await state.updateServerVersion(undefined);
313 if (state.serverVersion === config.package.version) return dest;
315 if (config.askBeforeDownload) {
316 const userResponse = await vscode.window.showInformationMessage(
317 `Language server version ${config.package.version} for rust-analyzer is not installed.`,
320 if (userResponse !== "Download now") return dest;
323 const releaseTag = config.package.releaseTag;
324 const release = await downloadWithRetryDialog(state, async () => {
325 return await fetchRelease(releaseTag, state.githubToken);
327 const artifact = release.assets.find(artifact => artifact.name === `rust-analyzer-${platform}.gz`);
328 assert(!!artifact, `Bad release: ${JSON.stringify(release)}`);
330 await downloadWithRetryDialog(state, async () => {
332 url: artifact.browser_download_url,
334 progressTitle: "Downloading rust-analyzer server",
341 // Patching executable if that's NixOS.
342 if (await fs.stat("/etc/nixos").then(_ => true).catch(_ => false)) {
343 await patchelf(dest);
346 await state.updateServerVersion(config.package.version);
350 async function downloadWithRetryDialog<T>(state: PersistentState, downloadFunc: () => Promise<T>): Promise<T> {
353 return await downloadFunc();
355 const selected = await vscode.window.showErrorMessage("Failed to download: " + e.message, {}, {
356 title: "Update Github Auth Token",
359 title: "Retry download",
365 if (selected?.updateToken) {
366 await queryForGithubToken(state);
368 } else if (selected?.retry) {
376 async function queryForGithubToken(state: PersistentState): Promise<void> {
377 const githubTokenOptions: vscode.InputBoxOptions = {
378 value: state.githubToken,
381 This dialog allows to store a Github authorization token.
382 The usage of an authorization token will increase the rate
383 limit on the use of Github APIs and can thereby prevent getting
385 Auth tokens can be created at https://github.com/settings/tokens`,
388 const newToken = await vscode.window.showInputBox(githubTokenOptions);
389 if (newToken === undefined) {
390 // The user aborted the dialog => Do not update the stored token
394 if (newToken === "") {
395 log.info("Clearing github token");
396 await state.updateGithubToken(undefined);
398 log.info("Storing new github token");
399 await state.updateGithubToken(newToken);