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('viewHir', commands.viewHir);
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);
135 warnAboutExtensionConflicts();
137 vscode.workspace.onDidChangeConfiguration(
138 _ => ctx?.client?.sendNotification('workspace/didChangeConfiguration', { settings: "" }),
144 export async function deactivate() {
145 setContextValue(RUST_PROJECT_CONTEXT_NAME, undefined);
146 await ctx?.client.stop();
150 async function bootstrap(config: Config, state: PersistentState): Promise<string> {
151 await fs.mkdir(config.globalStoragePath, { recursive: true });
153 await bootstrapExtension(config, state);
154 const path = await bootstrapServer(config, state);
159 async function bootstrapExtension(config: Config, state: PersistentState): Promise<void> {
160 if (config.package.releaseTag === null) return;
161 if (config.channel === "stable") {
162 if (config.package.releaseTag === NIGHTLY_TAG) {
163 void vscode.window.showWarningMessage(
164 `You are running a nightly version of rust-analyzer extension. ` +
165 `To switch to stable, uninstall the extension and re-install it from the marketplace`
171 const now = Date.now();
172 if (config.package.releaseTag === NIGHTLY_TAG) {
173 // Check if we should poll github api for the new nightly version
174 // if we haven't done it during the past hour
175 const lastCheck = state.lastCheck;
177 const anHour = 60 * 60 * 1000;
178 const shouldCheckForNewNightly = state.releaseId === undefined || (now - (lastCheck ?? 0)) > anHour;
180 if (!shouldCheckForNewNightly) return;
183 const release = await downloadWithRetryDialog(state, async () => {
184 return await fetchRelease("nightly", state.githubToken);
187 if (state.releaseId === undefined) { // Show error only for the initial download
188 vscode.window.showErrorMessage(`Failed to download rust-analyzer nightly ${e}`);
192 if (release === undefined || release.id === state.releaseId) return;
194 const userResponse = await vscode.window.showInformationMessage(
195 "New version of rust-analyzer (nightly) is available (requires reload).",
198 if (userResponse !== "Update") return;
200 const artifact = release.assets.find(artifact => artifact.name === "rust-analyzer.vsix");
201 assert(!!artifact, `Bad release: ${JSON.stringify(release)}`);
203 const dest = path.join(config.globalStoragePath, "rust-analyzer.vsix");
205 await downloadWithRetryDialog(state, async () => {
207 url: artifact.browser_download_url,
209 progressTitle: "Downloading rust-analyzer extension",
214 await vscode.commands.executeCommand("workbench.extensions.installExtension", vscode.Uri.file(dest));
215 await fs.unlink(dest);
217 await state.updateReleaseId(release.id);
218 await state.updateLastCheck(now);
219 await vscode.commands.executeCommand("workbench.action.reloadWindow");
222 async function bootstrapServer(config: Config, state: PersistentState): Promise<string> {
223 const path = await getServer(config, state);
226 "Rust Analyzer Language Server is not available. " +
227 "Please, ensure its [proper installation](https://rust-analyzer.github.io/manual.html#installation)."
231 log.info("Using server binary at", path);
233 if (!isValidExecutable(path)) {
234 throw new Error(`Failed to execute ${path} --version`);
240 async function patchelf(dest: PathLike): Promise<void> {
241 await vscode.window.withProgress(
243 location: vscode.ProgressLocation.Notification,
244 title: "Patching rust-analyzer for NixOS"
246 async (progress, _) => {
248 {src, pkgs ? import <nixpkgs> {}}:
249 pkgs.stdenv.mkDerivation {
250 name = "rust-analyzer";
252 phases = [ "installPhase" "fixupPhase" ];
253 installPhase = "cp $src $out";
256 patchelf --set-interpreter "$(cat $NIX_CC/nix-support/dynamic-linker)" $out
260 const origFile = dest + "-orig";
261 await fs.rename(dest, origFile);
262 progress.report({ message: "Patching executable", increment: 20 });
263 await new Promise((resolve, reject) => {
264 const handle = exec(`nix-build -E - --arg src '${origFile}' -o ${dest}`,
265 (err, stdout, stderr) => {
267 reject(Error(stderr));
272 handle.stdin?.write(expression);
275 await fs.unlink(origFile);
280 async function getServer(config: Config, state: PersistentState): Promise<string | undefined> {
281 const explicitPath = process.env.__RA_LSP_SERVER_DEBUG ?? config.serverPath;
283 if (explicitPath.startsWith("~/")) {
284 return os.homedir() + explicitPath.slice("~".length);
288 if (config.package.releaseTag === null) return "rust-analyzer";
290 const platforms: { [key: string]: string } = {
291 "ia32 win32": "x86_64-pc-windows-msvc",
292 "x64 win32": "x86_64-pc-windows-msvc",
293 "x64 linux": "x86_64-unknown-linux-gnu",
294 "x64 darwin": "x86_64-apple-darwin",
295 "arm64 win32": "aarch64-pc-windows-msvc",
296 "arm64 darwin": "aarch64-apple-darwin",
298 const platform = platforms[`${process.arch} ${process.platform}`];
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");