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);
114 defaultOnEnter.dispose();
115 ctx.registerCommand('onEnter', commands.onEnter);
117 ctx.registerCommand('ssr', commands.ssr);
118 ctx.registerCommand('serverVersion', commands.serverVersion);
119 ctx.registerCommand('toggleInlayHints', commands.toggleInlayHints);
121 // Internal commands which are invoked by the server.
122 ctx.registerCommand('runSingle', commands.runSingle);
123 ctx.registerCommand('debugSingle', commands.debugSingle);
124 ctx.registerCommand('showReferences', commands.showReferences);
125 ctx.registerCommand('applySnippetWorkspaceEdit', commands.applySnippetWorkspaceEditCommand);
126 ctx.registerCommand('resolveCodeAction', commands.resolveCodeAction);
127 ctx.registerCommand('applyActionGroup', commands.applyActionGroup);
128 ctx.registerCommand('gotoLocation', commands.gotoLocation);
130 ctx.pushCleanup(activateTaskProvider(workspaceFolder, ctx.config));
132 activateInlayHints(ctx);
134 vscode.workspace.onDidChangeConfiguration(
135 _ => ctx?.client?.sendNotification('workspace/didChangeConfiguration', { settings: "" }),
141 export async function deactivate() {
142 setContextValue(RUST_PROJECT_CONTEXT_NAME, undefined);
143 await ctx?.client.stop();
147 async function bootstrap(config: Config, state: PersistentState): Promise<string> {
148 await fs.mkdir(config.globalStoragePath, { recursive: true });
150 await bootstrapExtension(config, state);
151 const path = await bootstrapServer(config, state);
156 async function bootstrapExtension(config: Config, state: PersistentState): Promise<void> {
157 if (config.package.releaseTag === null) return;
158 if (config.channel === "stable") {
159 if (config.package.releaseTag === NIGHTLY_TAG) {
160 void vscode.window.showWarningMessage(
161 `You are running a nightly version of rust-analyzer extension. ` +
162 `To switch to stable, uninstall the extension and re-install it from the marketplace`
168 const now = Date.now();
169 if (config.package.releaseTag === NIGHTLY_TAG) {
170 // Check if we should poll github api for the new nightly version
171 // if we haven't done it during the past hour
172 const lastCheck = state.lastCheck;
174 const anHour = 60 * 60 * 1000;
175 const shouldCheckForNewNightly = state.releaseId === undefined || (now - (lastCheck ?? 0)) > anHour;
177 if (!shouldCheckForNewNightly) return;
180 const release = await performDownloadWithRetryDialog(async () => {
181 return await fetchRelease("nightly", state.githubToken);
182 }, state).catch((e) => {
184 if (state.releaseId === undefined) { // Show error only for the initial download
185 vscode.window.showErrorMessage(`Failed to download rust-analyzer nightly ${e}`);
189 if (release === undefined || release.id === state.releaseId) return;
191 const userResponse = await vscode.window.showInformationMessage(
192 "New version of rust-analyzer (nightly) is available (requires reload).",
195 if (userResponse !== "Update") return;
197 const artifact = release.assets.find(artifact => artifact.name === "rust-analyzer.vsix");
198 assert(!!artifact, `Bad release: ${JSON.stringify(release)}`);
200 const dest = path.join(config.globalStoragePath, "rust-analyzer.vsix");
202 await performDownloadWithRetryDialog(async () => {
203 // Unlinking the exe file before moving new one on its place should prevent ETXTBSY error.
204 await fs.unlink(dest).catch(err => {
205 if (err.code !== "ENOENT") throw err;
209 url: artifact.browser_download_url,
211 progressTitle: "Downloading rust-analyzer extension",
215 await vscode.commands.executeCommand("workbench.extensions.installExtension", vscode.Uri.file(dest));
216 await fs.unlink(dest);
218 await state.updateReleaseId(release.id);
219 await state.updateLastCheck(now);
220 await vscode.commands.executeCommand("workbench.action.reloadWindow");
223 async function bootstrapServer(config: Config, state: PersistentState): Promise<string> {
224 const path = await getServer(config, state);
227 "Rust Analyzer Language Server is not available. " +
228 "Please, ensure its [proper installation](https://rust-analyzer.github.io/manual.html#installation)."
232 log.info("Using server binary at", path);
234 if (!isValidExecutable(path)) {
235 throw new Error(`Failed to execute ${path} --version`);
241 async function patchelf(dest: PathLike): Promise<void> {
242 await vscode.window.withProgress(
244 location: vscode.ProgressLocation.Notification,
245 title: "Patching rust-analyzer for NixOS"
247 async (progress, _) => {
249 {src, pkgs ? import <nixpkgs> {}}:
250 pkgs.stdenv.mkDerivation {
251 name = "rust-analyzer";
253 phases = [ "installPhase" "fixupPhase" ];
254 installPhase = "cp $src $out";
257 patchelf --set-interpreter "$(cat $NIX_CC/nix-support/dynamic-linker)" $out
261 const origFile = dest + "-orig";
262 await fs.rename(dest, origFile);
263 progress.report({ message: "Patching executable", increment: 20 });
264 await new Promise((resolve, reject) => {
265 const handle = exec(`nix-build -E - --arg src '${origFile}' -o ${dest}`,
266 (err, stdout, stderr) => {
268 reject(Error(stderr));
273 handle.stdin?.write(expression);
276 await fs.unlink(origFile);
281 async function getServer(config: Config, state: PersistentState): Promise<string | undefined> {
282 const explicitPath = process.env.__RA_LSP_SERVER_DEBUG ?? config.serverPath;
284 if (explicitPath.startsWith("~/")) {
285 return os.homedir() + explicitPath.slice("~".length);
289 if (config.package.releaseTag === null) return "rust-analyzer";
291 let platform: string | undefined;
292 if (process.arch === "x64" || process.arch === "ia32") {
293 if (process.platform === "linux") platform = "linux";
294 if (process.platform === "darwin") platform = "mac";
295 if (process.platform === "win32") platform = "windows";
297 if (platform === undefined) {
298 vscode.window.showErrorMessage(
299 "Unfortunately we don't ship binaries for your platform yet. " +
300 "You need to manually clone rust-analyzer repository and " +
301 "run `cargo xtask install --server` to build the language server from sources. " +
302 "If you feel that your platform should be supported, please create an issue " +
303 "about that [here](https://github.com/rust-analyzer/rust-analyzer/issues) and we " +
308 const ext = platform === "windows" ? ".exe" : "";
309 const dest = path.join(config.globalStoragePath, `rust-analyzer-${platform}${ext}`);
310 const exists = await fs.stat(dest).then(() => true, () => false);
312 await state.updateServerVersion(undefined);
315 if (state.serverVersion === config.package.version) return dest;
317 if (config.askBeforeDownload) {
318 const userResponse = await vscode.window.showInformationMessage(
319 `Language server version ${config.package.version} for rust-analyzer is not installed.`,
322 if (userResponse !== "Download now") return dest;
325 const releaseTag = config.package.releaseTag;
326 const release = await performDownloadWithRetryDialog(async () => {
327 return await fetchRelease(releaseTag, state.githubToken);
329 const artifact = release.assets.find(artifact => artifact.name === `rust-analyzer-${platform}.gz`);
330 assert(!!artifact, `Bad release: ${JSON.stringify(release)}`);
332 await performDownloadWithRetryDialog(async () => {
333 // Unlinking the exe file before moving new one on its place should prevent ETXTBSY error.
334 await fs.unlink(dest).catch(err => {
335 if (err.code !== "ENOENT") throw err;
339 url: artifact.browser_download_url,
341 progressTitle: "Downloading rust-analyzer server",
347 // Patching executable if that's NixOS.
348 if (await fs.stat("/etc/nixos").then(_ => true).catch(_ => false)) {
349 await patchelf(dest);
352 await state.updateServerVersion(config.package.version);
356 async function performDownloadWithRetryDialog<T>(downloadFunc: () => Promise<T>, state: PersistentState): Promise<T> {
359 return await downloadFunc();
361 const selected = await vscode.window.showErrorMessage("Failed perform download: " + e.message, {}, {
362 title: "Update Github Auth Token",
365 title: "Retry download",
371 if (selected?.updateToken) {
372 await queryForGithubToken(state);
374 } else if (selected?.retry) {
383 async function queryForGithubToken(state: PersistentState): Promise<void> {
384 const githubTokenOptions: vscode.InputBoxOptions = {
385 value: state.githubToken,
388 This dialog allows to store a Github authorization token.
389 The usage of an authorization token will increase the rate
390 limit on the use of Github APIs and can thereby prevent getting
392 Auth tokens can be created at https://github.com/settings/tokens`,
395 const newToken = await vscode.window.showInputBox(githubTokenOptions);
397 log.info("Storing new github token");
398 await state.updateGithubToken(newToken);