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('analyzerStatus', commands.analyzerStatus);
99 ctx.registerCommand('memoryUsage', commands.memoryUsage);
100 ctx.registerCommand('reloadWorkspace', commands.reloadWorkspace);
101 ctx.registerCommand('matchingBrace', commands.matchingBrace);
102 ctx.registerCommand('joinLines', commands.joinLines);
103 ctx.registerCommand('parentModule', commands.parentModule);
104 ctx.registerCommand('syntaxTree', commands.syntaxTree);
105 ctx.registerCommand('expandMacro', commands.expandMacro);
106 ctx.registerCommand('run', commands.run);
107 ctx.registerCommand('debug', commands.debug);
108 ctx.registerCommand('newDebugConfig', commands.newDebugConfig);
110 defaultOnEnter.dispose();
111 ctx.registerCommand('onEnter', commands.onEnter);
113 ctx.registerCommand('ssr', commands.ssr);
114 ctx.registerCommand('serverVersion', commands.serverVersion);
115 ctx.registerCommand('toggleInlayHints', commands.toggleInlayHints);
117 // Internal commands which are invoked by the server.
118 ctx.registerCommand('runSingle', commands.runSingle);
119 ctx.registerCommand('debugSingle', commands.debugSingle);
120 ctx.registerCommand('showReferences', commands.showReferences);
121 ctx.registerCommand('applySnippetWorkspaceEdit', commands.applySnippetWorkspaceEditCommand);
122 ctx.registerCommand('resolveCodeAction', commands.resolveCodeAction);
123 ctx.registerCommand('applyActionGroup', commands.applyActionGroup);
124 ctx.registerCommand('gotoLocation', commands.gotoLocation);
126 ctx.pushCleanup(activateTaskProvider(workspaceFolder, ctx.config));
128 activateInlayHints(ctx);
130 vscode.workspace.onDidChangeConfiguration(
131 _ => ctx?.client?.sendNotification('workspace/didChangeConfiguration', { settings: "" }),
137 export async function deactivate() {
138 setContextValue(RUST_PROJECT_CONTEXT_NAME, undefined);
139 await ctx?.client.stop();
143 async function bootstrap(config: Config, state: PersistentState): Promise<string> {
144 await fs.mkdir(config.globalStoragePath, { recursive: true });
146 await bootstrapExtension(config, state);
147 const path = await bootstrapServer(config, state);
152 async function bootstrapExtension(config: Config, state: PersistentState): Promise<void> {
153 if (config.package.releaseTag === null) return;
154 if (config.channel === "stable") {
155 if (config.package.releaseTag === NIGHTLY_TAG) {
156 void vscode.window.showWarningMessage(
157 `You are running a nightly version of rust-analyzer extension. ` +
158 `To switch to stable, uninstall the extension and re-install it from the marketplace`
164 const now = Date.now();
165 if (config.package.releaseTag === NIGHTLY_TAG) {
166 // Check if we should poll github api for the new nightly version
167 // if we haven't done it during the past hour
168 const lastCheck = state.lastCheck;
170 const anHour = 60 * 60 * 1000;
171 const shouldCheckForNewNightly = state.releaseId === undefined || (now - (lastCheck ?? 0)) > anHour;
173 if (!shouldCheckForNewNightly) return;
176 const release = await performDownloadWithRetryDialog(async () => {
177 return await fetchRelease("nightly", state.githubToken);
178 }, state).catch((e) => {
180 if (state.releaseId === undefined) { // Show error only for the initial download
181 vscode.window.showErrorMessage(`Failed to download rust-analyzer nightly ${e}`);
185 if (release === undefined || release.id === state.releaseId) return;
187 const userResponse = await vscode.window.showInformationMessage(
188 "New version of rust-analyzer (nightly) is available (requires reload).",
191 if (userResponse !== "Update") return;
193 const artifact = release.assets.find(artifact => artifact.name === "rust-analyzer.vsix");
194 assert(!!artifact, `Bad release: ${JSON.stringify(release)}`);
196 const dest = path.join(config.globalStoragePath, "rust-analyzer.vsix");
198 url: artifact.browser_download_url,
200 progressTitle: "Downloading rust-analyzer extension",
203 await vscode.commands.executeCommand("workbench.extensions.installExtension", vscode.Uri.file(dest));
204 await fs.unlink(dest);
206 await state.updateReleaseId(release.id);
207 await state.updateLastCheck(now);
208 await vscode.commands.executeCommand("workbench.action.reloadWindow");
211 async function bootstrapServer(config: Config, state: PersistentState): Promise<string> {
212 const path = await getServer(config, state);
215 "Rust Analyzer Language Server is not available. " +
216 "Please, ensure its [proper installation](https://rust-analyzer.github.io/manual.html#installation)."
220 log.info("Using server binary at", path);
222 if (!isValidExecutable(path)) {
223 throw new Error(`Failed to execute ${path} --version`);
229 async function patchelf(dest: PathLike): Promise<void> {
230 await vscode.window.withProgress(
232 location: vscode.ProgressLocation.Notification,
233 title: "Patching rust-analyzer for NixOS"
235 async (progress, _) => {
237 {src, pkgs ? import <nixpkgs> {}}:
238 pkgs.stdenv.mkDerivation {
239 name = "rust-analyzer";
241 phases = [ "installPhase" "fixupPhase" ];
242 installPhase = "cp $src $out";
245 patchelf --set-interpreter "$(cat $NIX_CC/nix-support/dynamic-linker)" $out
249 const origFile = dest + "-orig";
250 await fs.rename(dest, origFile);
251 progress.report({ message: "Patching executable", increment: 20 });
252 await new Promise((resolve, reject) => {
253 const handle = exec(`nix-build -E - --arg src '${origFile}' -o ${dest}`,
254 (err, stdout, stderr) => {
256 reject(Error(stderr));
261 handle.stdin?.write(expression);
264 await fs.unlink(origFile);
269 async function getServer(config: Config, state: PersistentState): Promise<string | undefined> {
270 const explicitPath = process.env.__RA_LSP_SERVER_DEBUG ?? config.serverPath;
272 if (explicitPath.startsWith("~/")) {
273 return os.homedir() + explicitPath.slice("~".length);
277 if (config.package.releaseTag === null) return "rust-analyzer";
279 let platform: string | undefined;
280 if (process.arch === "x64" || process.arch === "ia32") {
281 if (process.platform === "linux") platform = "linux";
282 if (process.platform === "darwin") platform = "mac";
283 if (process.platform === "win32") platform = "windows";
285 if (platform === undefined) {
286 vscode.window.showErrorMessage(
287 "Unfortunately we don't ship binaries for your platform yet. " +
288 "You need to manually clone rust-analyzer repository and " +
289 "run `cargo xtask install --server` to build the language server from sources. " +
290 "If you feel that your platform should be supported, please create an issue " +
291 "about that [here](https://github.com/rust-analyzer/rust-analyzer/issues) and we " +
296 const ext = platform === "windows" ? ".exe" : "";
297 const dest = path.join(config.globalStoragePath, `rust-analyzer-${platform}${ext}`);
298 const exists = await fs.stat(dest).then(() => true, () => false);
300 await state.updateServerVersion(undefined);
303 if (state.serverVersion === config.package.version) return dest;
305 if (config.askBeforeDownload) {
306 const userResponse = await vscode.window.showInformationMessage(
307 `Language server version ${config.package.version} for rust-analyzer is not installed.`,
310 if (userResponse !== "Download now") return dest;
313 const releaseTag = config.package.releaseTag;
314 const release = await performDownloadWithRetryDialog(async () => {
315 return await fetchRelease(releaseTag, state.githubToken);
317 const artifact = release.assets.find(artifact => artifact.name === `rust-analyzer-${platform}.gz`);
318 assert(!!artifact, `Bad release: ${JSON.stringify(release)}`);
320 // Unlinking the exe file before moving new one on its place should prevent ETXTBSY error.
321 await fs.unlink(dest).catch(err => {
322 if (err.code !== "ENOENT") throw err;
326 url: artifact.browser_download_url,
328 progressTitle: "Downloading rust-analyzer server",
333 // Patching executable if that's NixOS.
334 if (await fs.stat("/etc/nixos").then(_ => true).catch(_ => false)) {
335 await patchelf(dest);
338 await state.updateServerVersion(config.package.version);
342 async function performDownloadWithRetryDialog<T>(downloadFunc: () => Promise<T>, state: PersistentState): Promise<T> {
345 return await downloadFunc();
347 const selected = await vscode.window.showErrorMessage("Failed perform download: " + e.message, {}, {
348 title: "Update Github Auth Token",
351 title: "Retry download",
357 if (selected?.updateToken) {
358 await queryForGithubToken(state);
360 } else if (selected?.retry) {
369 async function queryForGithubToken(state: PersistentState): Promise<void> {
370 const githubTokenOptions: vscode.InputBoxOptions = {
371 value: state.githubToken,
374 This dialog allows to store a Github authorization token.
375 The usage of an authorization token allows will increase the rate
376 limit on the use of Github APIs and can thereby prevent getting
378 Auth tokens can be obtained at https://github.com/settings/tokens`,
381 const newToken = await vscode.window.showInputBox(githubTokenOptions);
383 log.info("Storing new github token");
384 await state.updateGithubToken(newToken);