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 // VS Code doesn't show a notification when an extension fails to activate
23 // so we do it ourselves.
24 await tryActivate(context).catch(err => {
25 void vscode.window.showErrorMessage(`Cannot activate rust-analyzer: ${err.message}`);
30 async function tryActivate(context: vscode.ExtensionContext) {
31 // Register a "dumb" onEnter command for the case where server fails to
34 // FIXME: refactor command registration code such that commands are
35 // **always** registered, even if the server does not start. Use API like
40 // factory: (Ctx) => ((Ctx) => any),
41 // fallback: () => any = () => vscode.window.showErrorMessage(
42 // "rust-analyzer is not available"
45 const defaultOnEnter = vscode.commands.registerCommand(
46 'rust-analyzer.onEnter',
47 () => vscode.commands.executeCommand('default:type', { text: '\n' }),
49 context.subscriptions.push(defaultOnEnter);
51 const config = new Config(context);
52 const state = new PersistentState(context.globalState);
53 const serverPath = await bootstrap(config, state).catch(err => {
54 let message = "bootstrap error. ";
56 if (err.code === "EBUSY" || err.code === "ETXTBSY" || err.code === "EPERM") {
57 message += "Other vscode windows might be using rust-analyzer, ";
58 message += "you should close them and reload this window to retry. ";
61 message += 'See the logs in "OUTPUT > Rust Analyzer Client" (should open automatically). ';
62 message += 'To enable verbose logs use { "rust-analyzer.trace.extension": true }';
64 log.error("Bootstrap error", err);
65 throw new Error(message);
68 const workspaceFolder = vscode.workspace.workspaceFolders?.[0];
69 if (workspaceFolder === undefined) {
70 throw new Error("no folder is opened");
73 // Note: we try to start the server before we activate type hints so that it
74 // registers its `onDidChangeDocument` handler before us.
76 // This a horribly, horribly wrong way to deal with this problem.
77 ctx = await Ctx.create(config, context, serverPath, workspaceFolder.uri.fsPath);
79 setContextValue(RUST_PROJECT_CONTEXT_NAME, true);
81 // Commands which invokes manually via command palette, shortcut, etc.
83 // Reloading is inspired by @DanTup maneuver: https://github.com/microsoft/vscode/issues/45774#issuecomment-373423895
84 ctx.registerCommand('reload', _ => async () => {
85 void vscode.window.showInformationMessage('Reloading rust-analyzer...');
87 while (context.subscriptions.length > 0) {
89 context.subscriptions.pop()!.dispose();
91 log.error("Dispose error:", err);
94 await activate(context).catch(log.error);
97 ctx.registerCommand('updateGithubToken', ctx => async () => {
98 await queryForGithubToken(new PersistentState(ctx.globalState));
101 ctx.registerCommand('analyzerStatus', commands.analyzerStatus);
102 ctx.registerCommand('memoryUsage', commands.memoryUsage);
103 ctx.registerCommand('reloadWorkspace', commands.reloadWorkspace);
104 ctx.registerCommand('matchingBrace', commands.matchingBrace);
105 ctx.registerCommand('joinLines', commands.joinLines);
106 ctx.registerCommand('parentModule', commands.parentModule);
107 ctx.registerCommand('syntaxTree', commands.syntaxTree);
108 ctx.registerCommand('expandMacro', commands.expandMacro);
109 ctx.registerCommand('run', commands.run);
110 ctx.registerCommand('debug', commands.debug);
111 ctx.registerCommand('newDebugConfig', commands.newDebugConfig);
112 ctx.registerCommand('openDocs', commands.openDocs);
113 ctx.registerCommand('openCargoToml', commands.openCargoToml);
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);
134 warnAboutExtensionConflicts();
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") && process.platform === "win32") {
291 platform = "x86_64-pc-windows-msvc";
292 } else if (process.arch === "x64" && process.platform === "linux") {
293 platform = "x86_64-unknown-linux-gnu";
294 } else if (process.arch === "x64" && process.platform === "darwin") {
295 platform = "x86_64-apple-darwin";
296 } else if (process.arch === "arm64" && process.platform === "darwin") {
297 platform = "aarch64-apple-darwin";
299 if (platform === undefined) {
300 vscode.window.showErrorMessage(
301 "Unfortunately we don't ship binaries for your platform yet. " +
302 "You need to manually clone rust-analyzer repository and " +
303 "run `cargo xtask install --server` to build the language server from sources. " +
304 "If you feel that your platform should be supported, please create an issue " +
305 "about that [here](https://github.com/rust-analyzer/rust-analyzer/issues) and we " +
310 const ext = platform.indexOf("-windows-") !== -1 ? ".exe" : "";
311 const dest = path.join(config.globalStoragePath, `rust-analyzer-${platform}${ext}`);
312 const exists = await fs.stat(dest).then(() => true, () => false);
314 await state.updateServerVersion(undefined);
317 if (state.serverVersion === config.package.version) return dest;
319 if (config.askBeforeDownload) {
320 const userResponse = await vscode.window.showInformationMessage(
321 `Language server version ${config.package.version} for rust-analyzer is not installed.`,
324 if (userResponse !== "Download now") return dest;
327 const releaseTag = config.package.releaseTag;
328 const release = await downloadWithRetryDialog(state, async () => {
329 return await fetchRelease(releaseTag, state.githubToken);
331 const artifact = release.assets.find(artifact => artifact.name === `rust-analyzer-${platform}.gz`);
332 assert(!!artifact, `Bad release: ${JSON.stringify(release)}`);
334 await downloadWithRetryDialog(state, async () => {
336 url: artifact.browser_download_url,
338 progressTitle: "Downloading rust-analyzer server",
345 // Patching executable if that's NixOS.
346 if (await isNixOs()) {
347 await patchelf(dest);
350 await state.updateServerVersion(config.package.version);
354 async function isNixOs(): Promise<boolean> {
356 const contents = await fs.readFile("/etc/os-release");
357 return contents.indexOf("ID=nixos") !== -1;
363 async function downloadWithRetryDialog<T>(state: PersistentState, downloadFunc: () => Promise<T>): Promise<T> {
366 return await downloadFunc();
368 const selected = await vscode.window.showErrorMessage("Failed to download: " + e.message, {}, {
369 title: "Update Github Auth Token",
372 title: "Retry download",
378 if (selected?.updateToken) {
379 await queryForGithubToken(state);
381 } else if (selected?.retry) {
389 async function queryForGithubToken(state: PersistentState): Promise<void> {
390 const githubTokenOptions: vscode.InputBoxOptions = {
391 value: state.githubToken,
394 This dialog allows to store a Github authorization token.
395 The usage of an authorization token will increase the rate
396 limit on the use of Github APIs and can thereby prevent getting
398 Auth tokens can be created at https://github.com/settings/tokens`,
401 const newToken = await vscode.window.showInputBox(githubTokenOptions);
402 if (newToken === undefined) {
403 // The user aborted the dialog => Do not update the stored token
407 if (newToken === "") {
408 log.info("Clearing github token");
409 await state.updateGithubToken(undefined);
411 log.info("Storing new github token");
412 await state.updateGithubToken(newToken);
416 function warnAboutExtensionConflicts() {
417 const conflicting = [
418 ["rust-analyzer", "matklad.rust-analyzer"],
419 ["Rust", "rust-lang.rust"],
420 ["Rust", "kalitaalexey.vscode-rust"],
423 const found = conflicting.filter(
424 nameId => vscode.extensions.getExtension(nameId[1]) !== undefined);
426 if (found.length > 1) {
427 const fst = found[0];
428 const sec = found[1];
429 vscode.window.showWarningMessage(
430 `You have both the ${fst[0]} (${fst[1]}) and ${sec[0]} (${sec[1]}) ` +
431 "plugins enabled. These are known to conflict and cause various functions of " +
432 "both plugins to not work correctly. You should disable one of them.", "Got it");