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, spawnSync } 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 await 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('viewItemTree', commands.viewItemTree);
110 ctx.registerCommand('viewCrateGraph', commands.viewCrateGraph);
111 ctx.registerCommand('expandMacro', commands.expandMacro);
112 ctx.registerCommand('run', commands.run);
113 ctx.registerCommand('copyRunCommandLine', commands.copyRunCommandLine);
114 ctx.registerCommand('debug', commands.debug);
115 ctx.registerCommand('newDebugConfig', commands.newDebugConfig);
116 ctx.registerCommand('openDocs', commands.openDocs);
117 ctx.registerCommand('openCargoToml', commands.openCargoToml);
118 ctx.registerCommand('peekTests', commands.peekTests);
119 ctx.registerCommand('moveItemUp', commands.moveItemUp);
120 ctx.registerCommand('moveItemDown', commands.moveItemDown);
122 defaultOnEnter.dispose();
123 ctx.registerCommand('onEnter', commands.onEnter);
125 ctx.registerCommand('ssr', commands.ssr);
126 ctx.registerCommand('serverVersion', commands.serverVersion);
127 ctx.registerCommand('toggleInlayHints', commands.toggleInlayHints);
129 // Internal commands which are invoked by the server.
130 ctx.registerCommand('runSingle', commands.runSingle);
131 ctx.registerCommand('debugSingle', commands.debugSingle);
132 ctx.registerCommand('showReferences', commands.showReferences);
133 ctx.registerCommand('applySnippetWorkspaceEdit', commands.applySnippetWorkspaceEditCommand);
134 ctx.registerCommand('resolveCodeAction', commands.resolveCodeAction);
135 ctx.registerCommand('applyActionGroup', commands.applyActionGroup);
136 ctx.registerCommand('gotoLocation', commands.gotoLocation);
138 ctx.pushCleanup(activateTaskProvider(workspaceFolder, ctx.config));
140 activateInlayHints(ctx);
141 warnAboutExtensionConflicts();
143 vscode.workspace.onDidChangeConfiguration(
144 _ => ctx?.client?.sendNotification('workspace/didChangeConfiguration', { settings: "" }),
150 export async function deactivate() {
151 await setContextValue(RUST_PROJECT_CONTEXT_NAME, undefined);
152 await ctx?.client.stop();
156 async function bootstrap(config: Config, state: PersistentState): Promise<string> {
157 await fs.mkdir(config.globalStoragePath, { recursive: true });
159 if (config.package.releaseTag != NIGHTLY_TAG) {
160 await state.removeReleaseId();
162 await bootstrapExtension(config, state);
163 const path = await bootstrapServer(config, state);
167 async function bootstrapExtension(config: Config, state: PersistentState): Promise<void> {
168 if (config.package.releaseTag === null) return;
169 if (config.channel === "stable") {
170 if (config.package.releaseTag === NIGHTLY_TAG) {
171 void vscode.window.showWarningMessage(
172 `You are running a nightly version of rust-analyzer extension. ` +
173 `To switch to stable, uninstall the extension and re-install it from the marketplace`
178 if (serverPath(config)) return;
180 const now = Date.now();
181 if (config.package.releaseTag === NIGHTLY_TAG) {
182 // Check if we should poll github api for the new nightly version
183 // if we haven't done it during the past hour
184 const lastCheck = state.lastCheck;
186 const anHour = 60 * 60 * 1000;
187 const shouldCheckForNewNightly = state.releaseId === undefined || (now - (lastCheck ?? 0)) > anHour;
189 if (!shouldCheckForNewNightly) return;
192 const release = await downloadWithRetryDialog(state, async () => {
193 return await fetchRelease("nightly", state.githubToken, config.httpProxy);
194 }).catch(async (e) => {
196 if (state.releaseId === undefined) { // Show error only for the initial download
197 await vscode.window.showErrorMessage(`Failed to download rust-analyzer nightly: ${e}`);
201 if (release === undefined) {
202 if (state.releaseId === undefined) { // Show error only for the initial download
203 await vscode.window.showErrorMessage("Failed to download rust-analyzer nightly: empty release contents returned");
207 // If currently used extension is nightly and its release id matches the downloaded release id, we're already on the latest nightly version
208 if (config.package.releaseTag === NIGHTLY_TAG && release.id === state.releaseId) return;
210 const userResponse = await vscode.window.showInformationMessage(
211 "New version of rust-analyzer (nightly) is available (requires reload).",
214 if (userResponse !== "Update") return;
216 const artifact = release.assets.find(artifact => artifact.name === "rust-analyzer.vsix");
217 assert(!!artifact, `Bad release: ${JSON.stringify(release)}`);
219 const dest = path.join(config.globalStoragePath, "rust-analyzer.vsix");
221 await downloadWithRetryDialog(state, async () => {
223 url: artifact.browser_download_url,
225 progressTitle: "Downloading rust-analyzer extension",
226 httpProxy: config.httpProxy,
230 await vscode.commands.executeCommand("workbench.extensions.installExtension", vscode.Uri.file(dest));
231 await fs.unlink(dest);
233 await state.updateReleaseId(release.id);
234 await state.updateLastCheck(now);
235 await vscode.commands.executeCommand("workbench.action.reloadWindow");
238 async function bootstrapServer(config: Config, state: PersistentState): Promise<string> {
239 const path = await getServer(config, state);
242 "Rust Analyzer Language Server is not available. " +
243 "Please, ensure its [proper installation](https://rust-analyzer.github.io/manual.html#installation)."
247 log.info("Using server binary at", path);
249 if (!isValidExecutable(path)) {
250 throw new Error(`Failed to execute ${path} --version`);
256 async function patchelf(dest: PathLike): Promise<void> {
257 await vscode.window.withProgress(
259 location: vscode.ProgressLocation.Notification,
260 title: "Patching rust-analyzer for NixOS"
262 async (progress, _) => {
264 {srcStr, pkgs ? import <nixpkgs> {}}:
265 pkgs.stdenv.mkDerivation {
266 name = "rust-analyzer";
268 phases = [ "installPhase" "fixupPhase" ];
269 installPhase = "cp $src $out";
272 patchelf --set-interpreter "$(cat $NIX_CC/nix-support/dynamic-linker)" $out
276 const origFile = dest + "-orig";
277 await fs.rename(dest, origFile);
278 progress.report({ message: "Patching executable", increment: 20 });
279 await new Promise((resolve, reject) => {
280 const handle = exec(`nix-build -E - --argstr srcStr '${origFile}' -o '${dest}'`,
281 (err, stdout, stderr) => {
283 reject(Error(stderr));
288 handle.stdin?.write(expression);
291 await fs.unlink(origFile);
296 async function getServer(config: Config, state: PersistentState): Promise<string | undefined> {
297 const explicitPath = serverPath(config);
299 if (explicitPath.startsWith("~/")) {
300 return os.homedir() + explicitPath.slice("~".length);
304 if (config.package.releaseTag === null) return "rust-analyzer";
306 const platforms: { [key: string]: string } = {
307 "ia32 win32": "x86_64-pc-windows-msvc",
308 "x64 win32": "x86_64-pc-windows-msvc",
309 "x64 linux": "x86_64-unknown-linux-gnu",
310 "x64 darwin": "x86_64-apple-darwin",
311 "arm64 win32": "aarch64-pc-windows-msvc",
312 "arm64 linux": "aarch64-unknown-linux-gnu",
313 "arm64 darwin": "aarch64-apple-darwin",
315 let platform = platforms[`${process.arch} ${process.platform}`];
316 if (platform === undefined) {
317 await vscode.window.showErrorMessage(
318 "Unfortunately we don't ship binaries for your platform yet. " +
319 "You need to manually clone rust-analyzer repository and " +
320 "run `cargo xtask install --server` to build the language server from sources. " +
321 "If you feel that your platform should be supported, please create an issue " +
322 "about that [here](https://github.com/rust-analyzer/rust-analyzer/issues) and we " +
327 if (platform === "x86_64-unknown-linux-gnu" && isMusl()) {
328 platform = "x86_64-unknown-linux-musl";
330 const ext = platform.indexOf("-windows-") !== -1 ? ".exe" : "";
331 const dest = path.join(config.globalStoragePath, `rust-analyzer-${platform}${ext}`);
332 const exists = await fs.stat(dest).then(() => true, () => false);
334 await state.updateServerVersion(undefined);
337 if (state.serverVersion === config.package.version) return dest;
339 if (config.askBeforeDownload) {
340 const userResponse = await vscode.window.showInformationMessage(
341 `Language server version ${config.package.version} for rust-analyzer is not installed.`,
344 if (userResponse !== "Download now") return dest;
347 const releaseTag = config.package.releaseTag;
348 const release = await downloadWithRetryDialog(state, async () => {
349 return await fetchRelease(releaseTag, state.githubToken, config.httpProxy);
351 const artifact = release.assets.find(artifact => artifact.name === `rust-analyzer-${platform}.gz`);
352 assert(!!artifact, `Bad release: ${JSON.stringify(release)}`);
354 await downloadWithRetryDialog(state, async () => {
356 url: artifact.browser_download_url,
358 progressTitle: "Downloading rust-analyzer server",
361 httpProxy: config.httpProxy,
365 // Patching executable if that's NixOS.
366 if (await isNixOs()) {
367 await patchelf(dest);
370 await state.updateServerVersion(config.package.version);
374 function serverPath(config: Config): string | null {
375 return process.env.__RA_LSP_SERVER_DEBUG ?? config.serverPath;
378 async function isNixOs(): Promise<boolean> {
380 const contents = await fs.readFile("/etc/os-release");
381 return contents.indexOf("ID=nixos") !== -1;
387 function isMusl(): boolean {
388 // We can detect Alpine by checking `/etc/os-release` but not Void Linux musl.
389 // Instead, we run `ldd` since it advertises the libc which it belongs to.
390 const res = spawnSync("ldd", ["--version"]);
391 return res.stderr != null && res.stderr.indexOf("musl libc") >= 0;
394 async function downloadWithRetryDialog<T>(state: PersistentState, downloadFunc: () => Promise<T>): Promise<T> {
397 return await downloadFunc();
399 const selected = await vscode.window.showErrorMessage("Failed to download: " + e.message, {}, {
400 title: "Update Github Auth Token",
403 title: "Retry download",
409 if (selected?.updateToken) {
410 await queryForGithubToken(state);
412 } else if (selected?.retry) {
420 async function queryForGithubToken(state: PersistentState): Promise<void> {
421 const githubTokenOptions: vscode.InputBoxOptions = {
422 value: state.githubToken,
425 This dialog allows to store a Github authorization token.
426 The usage of an authorization token will increase the rate
427 limit on the use of Github APIs and can thereby prevent getting
429 Auth tokens can be created at https://github.com/settings/tokens`,
432 const newToken = await vscode.window.showInputBox(githubTokenOptions);
433 if (newToken === undefined) {
434 // The user aborted the dialog => Do not update the stored token
438 if (newToken === "") {
439 log.info("Clearing github token");
440 await state.updateGithubToken(undefined);
442 log.info("Storing new github token");
443 await state.updateGithubToken(newToken);
447 function warnAboutExtensionConflicts() {
448 const conflicting = [
449 ["rust-analyzer", "matklad.rust-analyzer"],
450 ["Rust", "rust-lang.rust"],
451 ["Rust", "kalitaalexey.vscode-rust"],
454 const found = conflicting.filter(
455 nameId => vscode.extensions.getExtension(nameId[1]) !== undefined);
457 if (found.length > 1) {
458 const fst = found[0];
459 const sec = found[1];
460 vscode.window.showWarningMessage(
461 `You have both the ${fst[0]} (${fst[1]}) and ${sec[0]} (${sec[1]}) ` +
462 "plugins enabled. These are known to conflict and cause various functions of " +
463 "both plugins to not work correctly. You should disable one of them.", "Got it")
464 .then(() => { }, console.error);