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);
115 defaultOnEnter.dispose();
116 ctx.registerCommand('onEnter', commands.onEnter);
118 ctx.registerCommand('ssr', commands.ssr);
119 ctx.registerCommand('serverVersion', commands.serverVersion);
120 ctx.registerCommand('toggleInlayHints', commands.toggleInlayHints);
122 // Internal commands which are invoked by the server.
123 ctx.registerCommand('runSingle', commands.runSingle);
124 ctx.registerCommand('debugSingle', commands.debugSingle);
125 ctx.registerCommand('showReferences', commands.showReferences);
126 ctx.registerCommand('applySnippetWorkspaceEdit', commands.applySnippetWorkspaceEditCommand);
127 ctx.registerCommand('resolveCodeAction', commands.resolveCodeAction);
128 ctx.registerCommand('applyActionGroup', commands.applyActionGroup);
129 ctx.registerCommand('gotoLocation', commands.gotoLocation);
131 ctx.pushCleanup(activateTaskProvider(workspaceFolder, ctx.config));
133 activateInlayHints(ctx);
135 vscode.workspace.onDidChangeConfiguration(
136 _ => ctx?.client?.sendNotification('workspace/didChangeConfiguration', { settings: "" }),
142 export async function deactivate() {
143 setContextValue(RUST_PROJECT_CONTEXT_NAME, undefined);
144 await ctx?.client.stop();
148 async function bootstrap(config: Config, state: PersistentState): Promise<string> {
149 await fs.mkdir(config.globalStoragePath, { recursive: true });
151 await bootstrapExtension(config, state);
152 const path = await bootstrapServer(config, state);
157 async function bootstrapExtension(config: Config, state: PersistentState): Promise<void> {
158 if (config.package.releaseTag === null) return;
159 if (config.channel === "stable") {
160 if (config.package.releaseTag === NIGHTLY_TAG) {
161 void vscode.window.showWarningMessage(
162 `You are running a nightly version of rust-analyzer extension. ` +
163 `To switch to stable, uninstall the extension and re-install it from the marketplace`
169 const now = Date.now();
170 if (config.package.releaseTag === NIGHTLY_TAG) {
171 // Check if we should poll github api for the new nightly version
172 // if we haven't done it during the past hour
173 const lastCheck = state.lastCheck;
175 const anHour = 60 * 60 * 1000;
176 const shouldCheckForNewNightly = state.releaseId === undefined || (now - (lastCheck ?? 0)) > anHour;
178 if (!shouldCheckForNewNightly) return;
181 const release = await downloadWithRetryDialog(state, async () => {
182 return await fetchRelease("nightly", state.githubToken);
185 if (state.releaseId === undefined) { // Show error only for the initial download
186 vscode.window.showErrorMessage(`Failed to download rust-analyzer nightly ${e}`);
190 if (release === undefined || release.id === state.releaseId) return;
192 const userResponse = await vscode.window.showInformationMessage(
193 "New version of rust-analyzer (nightly) is available (requires reload).",
196 if (userResponse !== "Update") return;
198 const artifact = release.assets.find(artifact => artifact.name === "rust-analyzer.vsix");
199 assert(!!artifact, `Bad release: ${JSON.stringify(release)}`);
201 const dest = path.join(config.globalStoragePath, "rust-analyzer.vsix");
203 await downloadWithRetryDialog(state, async () => {
205 url: artifact.browser_download_url,
207 progressTitle: "Downloading rust-analyzer extension",
212 await vscode.commands.executeCommand("workbench.extensions.installExtension", vscode.Uri.file(dest));
213 await fs.unlink(dest);
215 await state.updateReleaseId(release.id);
216 await state.updateLastCheck(now);
217 await vscode.commands.executeCommand("workbench.action.reloadWindow");
220 async function bootstrapServer(config: Config, state: PersistentState): Promise<string> {
221 const path = await getServer(config, state);
224 "Rust Analyzer Language Server is not available. " +
225 "Please, ensure its [proper installation](https://rust-analyzer.github.io/manual.html#installation)."
229 log.info("Using server binary at", path);
231 if (!isValidExecutable(path)) {
232 throw new Error(`Failed to execute ${path} --version`);
238 async function patchelf(dest: PathLike): Promise<void> {
239 await vscode.window.withProgress(
241 location: vscode.ProgressLocation.Notification,
242 title: "Patching rust-analyzer for NixOS"
244 async (progress, _) => {
246 {src, pkgs ? import <nixpkgs> {}}:
247 pkgs.stdenv.mkDerivation {
248 name = "rust-analyzer";
250 phases = [ "installPhase" "fixupPhase" ];
251 installPhase = "cp $src $out";
254 patchelf --set-interpreter "$(cat $NIX_CC/nix-support/dynamic-linker)" $out
258 const origFile = dest + "-orig";
259 await fs.rename(dest, origFile);
260 progress.report({ message: "Patching executable", increment: 20 });
261 await new Promise((resolve, reject) => {
262 const handle = exec(`nix-build -E - --arg src '${origFile}' -o ${dest}`,
263 (err, stdout, stderr) => {
265 reject(Error(stderr));
270 handle.stdin?.write(expression);
273 await fs.unlink(origFile);
278 async function getServer(config: Config, state: PersistentState): Promise<string | undefined> {
279 const explicitPath = process.env.__RA_LSP_SERVER_DEBUG ?? config.serverPath;
281 if (explicitPath.startsWith("~/")) {
282 return os.homedir() + explicitPath.slice("~".length);
286 if (config.package.releaseTag === null) return "rust-analyzer";
288 let platform: string | undefined;
289 if (process.arch === "x64" || process.arch === "ia32") {
290 if (process.platform === "linux") platform = "linux";
291 if (process.platform === "darwin") platform = "mac";
292 if (process.platform === "win32") platform = "windows";
294 if (platform === undefined) {
295 vscode.window.showErrorMessage(
296 "Unfortunately we don't ship binaries for your platform yet. " +
297 "You need to manually clone rust-analyzer repository and " +
298 "run `cargo xtask install --server` to build the language server from sources. " +
299 "If you feel that your platform should be supported, please create an issue " +
300 "about that [here](https://github.com/rust-analyzer/rust-analyzer/issues) and we " +
305 const ext = platform === "windows" ? ".exe" : "";
306 const dest = path.join(config.globalStoragePath, `rust-analyzer-${platform}${ext}`);
307 const exists = await fs.stat(dest).then(() => true, () => false);
309 await state.updateServerVersion(undefined);
312 if (state.serverVersion === config.package.version) return dest;
314 if (config.askBeforeDownload) {
315 const userResponse = await vscode.window.showInformationMessage(
316 `Language server version ${config.package.version} for rust-analyzer is not installed.`,
319 if (userResponse !== "Download now") return dest;
322 const releaseTag = config.package.releaseTag;
323 const release = await downloadWithRetryDialog(state, async () => {
324 return await fetchRelease(releaseTag, state.githubToken);
326 const artifact = release.assets.find(artifact => artifact.name === `rust-analyzer-${platform}.gz`);
327 assert(!!artifact, `Bad release: ${JSON.stringify(release)}`);
329 await downloadWithRetryDialog(state, async () => {
331 url: artifact.browser_download_url,
333 progressTitle: "Downloading rust-analyzer server",
340 // Patching executable if that's NixOS.
341 if (await fs.stat("/etc/nixos").then(_ => true).catch(_ => false)) {
342 await patchelf(dest);
345 await state.updateServerVersion(config.package.version);
349 async function downloadWithRetryDialog<T>(state: PersistentState, downloadFunc: () => Promise<T>): Promise<T> {
352 return await downloadFunc();
354 const selected = await vscode.window.showErrorMessage("Failed to download: " + e.message, {}, {
355 title: "Update Github Auth Token",
358 title: "Retry download",
364 if (selected?.updateToken) {
365 await queryForGithubToken(state);
367 } else if (selected?.retry) {
375 async function queryForGithubToken(state: PersistentState): Promise<void> {
376 const githubTokenOptions: vscode.InputBoxOptions = {
377 value: state.githubToken,
380 This dialog allows to store a Github authorization token.
381 The usage of an authorization token will increase the rate
382 limit on the use of Github APIs and can thereby prevent getting
384 Auth tokens can be created at https://github.com/settings/tokens`,
387 const newToken = await vscode.window.showInputBox(githubTokenOptions);
388 if (newToken === undefined) {
389 // The user aborted the dialog => Do not update the stored token
393 if (newToken === "") {
394 log.info("Clearing github token");
395 await state.updateGithubToken(undefined);
397 log.info("Storing new github token");
398 await state.updateGithubToken(newToken);