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`
170 if (serverPath(config)) return;
172 const now = Date.now();
173 if (config.package.releaseTag === NIGHTLY_TAG) {
174 // Check if we should poll github api for the new nightly version
175 // if we haven't done it during the past hour
176 const lastCheck = state.lastCheck;
178 const anHour = 60 * 60 * 1000;
179 const shouldCheckForNewNightly = state.releaseId === undefined || (now - (lastCheck ?? 0)) > anHour;
181 if (!shouldCheckForNewNightly) return;
184 const release = await downloadWithRetryDialog(state, async () => {
185 return await fetchRelease("nightly", state.githubToken);
188 if (state.releaseId === undefined) { // Show error only for the initial download
189 vscode.window.showErrorMessage(`Failed to download rust-analyzer nightly ${e}`);
193 if (release === undefined || release.id === state.releaseId) return;
195 const userResponse = await vscode.window.showInformationMessage(
196 "New version of rust-analyzer (nightly) is available (requires reload).",
199 if (userResponse !== "Update") return;
201 const artifact = release.assets.find(artifact => artifact.name === "rust-analyzer.vsix");
202 assert(!!artifact, `Bad release: ${JSON.stringify(release)}`);
204 const dest = path.join(config.globalStoragePath, "rust-analyzer.vsix");
206 await downloadWithRetryDialog(state, async () => {
208 url: artifact.browser_download_url,
210 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 = serverPath(config);
284 if (explicitPath.startsWith("~/")) {
285 return os.homedir() + explicitPath.slice("~".length);
289 if (config.package.releaseTag === null) return "rust-analyzer";
291 const platforms: { [key: string]: string } = {
292 "ia32 win32": "x86_64-pc-windows-msvc",
293 "x64 win32": "x86_64-pc-windows-msvc",
294 "x64 linux": "x86_64-unknown-linux-gnu",
295 "x64 darwin": "x86_64-apple-darwin",
296 "arm64 win32": "aarch64-pc-windows-msvc",
297 "arm64 linux": "aarch64-unknown-linux-gnu",
298 "arm64 darwin": "aarch64-apple-darwin",
300 const platform = platforms[`${process.arch} ${process.platform}`];
301 if (platform === undefined) {
302 vscode.window.showErrorMessage(
303 "Unfortunately we don't ship binaries for your platform yet. " +
304 "You need to manually clone rust-analyzer repository and " +
305 "run `cargo xtask install --server` to build the language server from sources. " +
306 "If you feel that your platform should be supported, please create an issue " +
307 "about that [here](https://github.com/rust-analyzer/rust-analyzer/issues) and we " +
312 const ext = platform.indexOf("-windows-") !== -1 ? ".exe" : "";
313 const dest = path.join(config.globalStoragePath, `rust-analyzer-${platform}${ext}`);
314 const exists = await fs.stat(dest).then(() => true, () => false);
316 await state.updateServerVersion(undefined);
319 if (state.serverVersion === config.package.version) return dest;
321 if (config.askBeforeDownload) {
322 const userResponse = await vscode.window.showInformationMessage(
323 `Language server version ${config.package.version} for rust-analyzer is not installed.`,
326 if (userResponse !== "Download now") return dest;
329 const releaseTag = config.package.releaseTag;
330 const release = await downloadWithRetryDialog(state, async () => {
331 return await fetchRelease(releaseTag, state.githubToken);
333 const artifact = release.assets.find(artifact => artifact.name === `rust-analyzer-${platform}.gz`);
334 assert(!!artifact, `Bad release: ${JSON.stringify(release)}`);
336 await downloadWithRetryDialog(state, async () => {
338 url: artifact.browser_download_url,
340 progressTitle: "Downloading rust-analyzer server",
347 // Patching executable if that's NixOS.
348 if (await isNixOs()) {
349 await patchelf(dest);
352 await state.updateServerVersion(config.package.version);
356 function serverPath(config: Config): string | null {
357 return process.env.__RA_LSP_SERVER_DEBUG ?? config.serverPath;
360 async function isNixOs(): Promise<boolean> {
362 const contents = await fs.readFile("/etc/os-release");
363 return contents.indexOf("ID=nixos") !== -1;
369 async function downloadWithRetryDialog<T>(state: PersistentState, downloadFunc: () => Promise<T>): Promise<T> {
372 return await downloadFunc();
374 const selected = await vscode.window.showErrorMessage("Failed to download: " + e.message, {}, {
375 title: "Update Github Auth Token",
378 title: "Retry download",
384 if (selected?.updateToken) {
385 await queryForGithubToken(state);
387 } else if (selected?.retry) {
395 async function queryForGithubToken(state: PersistentState): Promise<void> {
396 const githubTokenOptions: vscode.InputBoxOptions = {
397 value: state.githubToken,
400 This dialog allows to store a Github authorization token.
401 The usage of an authorization token will increase the rate
402 limit on the use of Github APIs and can thereby prevent getting
404 Auth tokens can be created at https://github.com/settings/tokens`,
407 const newToken = await vscode.window.showInputBox(githubTokenOptions);
408 if (newToken === undefined) {
409 // The user aborted the dialog => Do not update the stored token
413 if (newToken === "") {
414 log.info("Clearing github token");
415 await state.updateGithubToken(undefined);
417 log.info("Storing new github token");
418 await state.updateGithubToken(newToken);
422 function warnAboutExtensionConflicts() {
423 const conflicting = [
424 ["rust-analyzer", "matklad.rust-analyzer"],
425 ["Rust", "rust-lang.rust"],
426 ["Rust", "kalitaalexey.vscode-rust"],
429 const found = conflicting.filter(
430 nameId => vscode.extensions.getExtension(nameId[1]) !== undefined);
432 if (found.length > 1) {
433 const fst = found[0];
434 const sec = found[1];
435 vscode.window.showWarningMessage(
436 `You have both the ${fst[0]} (${fst[1]}) and ${sec[0]} (${sec[1]}) ` +
437 "plugins enabled. These are known to conflict and cause various functions of " +
438 "both plugins to not work correctly. You should disable one of them.", "Got it");