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 } from './config';
10 import { log, assert, isValidExecutable, isRustDocument } 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 const config = new Config(context);
32 const state = new PersistentState(context.globalState);
33 const serverPath = await bootstrap(config, state).catch(err => {
34 let message = "bootstrap error. ";
36 if (err.code === "EBUSY" || err.code === "ETXTBSY" || err.code === "EPERM") {
37 message += "Other vscode windows might be using rust-analyzer, ";
38 message += "you should close them and reload this window to retry. ";
41 message += 'See the logs in "OUTPUT > Rust Analyzer Client" (should open automatically). ';
42 message += 'To enable verbose logs use { "rust-analyzer.trace.extension": true }';
44 log.error("Bootstrap error", err);
45 throw new Error(message);
48 const workspaceFolder = vscode.workspace.workspaceFolders?.[0];
49 if (workspaceFolder === undefined) {
50 let rustDocuments = vscode.workspace.textDocuments.filter(document => isRustDocument(document));
51 if (rustDocuments.length > 0) {
52 ctx = await Ctx.create(config, context, serverPath, { kind: 'Detached files', files: rustDocuments });
54 throw new Error("no rust files are opened");
57 // Note: we try to start the server before we activate type hints so that it
58 // registers its `onDidChangeDocument` handler before us.
60 // This a horribly, horribly wrong way to deal with this problem.
61 ctx = await Ctx.create(config, context, serverPath, { kind: "Workspace Folder", folder: workspaceFolder.uri });
62 ctx.pushCleanup(activateTaskProvider(workspaceFolder, ctx.config));
64 await initCommonContext(context, ctx);
66 activateInlayHints(ctx);
67 warnAboutExtensionConflicts();
69 vscode.workspace.onDidChangeConfiguration(
70 _ => ctx?.client?.sendNotification('workspace/didChangeConfiguration', { settings: "" }),
76 async function initCommonContext(context: vscode.ExtensionContext, ctx: Ctx) {
77 // Register a "dumb" onEnter command for the case where server fails to
80 // FIXME: refactor command registration code such that commands are
81 // **always** registered, even if the server does not start. Use API like
86 // factory: (Ctx) => ((Ctx) => any),
87 // fallback: () => any = () => vscode.window.showErrorMessage(
88 // "rust-analyzer is not available"
91 const defaultOnEnter = vscode.commands.registerCommand(
92 'rust-analyzer.onEnter',
93 () => vscode.commands.executeCommand('default:type', { text: '\n' }),
95 context.subscriptions.push(defaultOnEnter);
97 await setContextValue(RUST_PROJECT_CONTEXT_NAME, true);
99 // Commands which invokes manually via command palette, shortcut, etc.
101 // Reloading is inspired by @DanTup maneuver: https://github.com/microsoft/vscode/issues/45774#issuecomment-373423895
102 ctx.registerCommand('reload', _ => async () => {
103 void vscode.window.showInformationMessage('Reloading rust-analyzer...');
105 while (context.subscriptions.length > 0) {
107 context.subscriptions.pop()!.dispose();
109 log.error("Dispose error:", err);
112 await activate(context).catch(log.error);
115 ctx.registerCommand('updateGithubToken', ctx => async () => {
116 await queryForGithubToken(new PersistentState(ctx.globalState));
119 ctx.registerCommand('analyzerStatus', commands.analyzerStatus);
120 ctx.registerCommand('memoryUsage', commands.memoryUsage);
121 ctx.registerCommand('reloadWorkspace', commands.reloadWorkspace);
122 ctx.registerCommand('matchingBrace', commands.matchingBrace);
123 ctx.registerCommand('joinLines', commands.joinLines);
124 ctx.registerCommand('parentModule', commands.parentModule);
125 ctx.registerCommand('syntaxTree', commands.syntaxTree);
126 ctx.registerCommand('viewHir', commands.viewHir);
127 ctx.registerCommand('viewItemTree', commands.viewItemTree);
128 ctx.registerCommand('viewCrateGraph', commands.viewCrateGraph);
129 ctx.registerCommand('expandMacro', commands.expandMacro);
130 ctx.registerCommand('run', commands.run);
131 ctx.registerCommand('copyRunCommandLine', commands.copyRunCommandLine);
132 ctx.registerCommand('debug', commands.debug);
133 ctx.registerCommand('newDebugConfig', commands.newDebugConfig);
134 ctx.registerCommand('openDocs', commands.openDocs);
135 ctx.registerCommand('openCargoToml', commands.openCargoToml);
136 ctx.registerCommand('peekTests', commands.peekTests);
137 ctx.registerCommand('moveItemUp', commands.moveItemUp);
138 ctx.registerCommand('moveItemDown', commands.moveItemDown);
140 defaultOnEnter.dispose();
141 ctx.registerCommand('onEnter', commands.onEnter);
143 ctx.registerCommand('ssr', commands.ssr);
144 ctx.registerCommand('serverVersion', commands.serverVersion);
145 ctx.registerCommand('toggleInlayHints', commands.toggleInlayHints);
147 // Internal commands which are invoked by the server.
148 ctx.registerCommand('runSingle', commands.runSingle);
149 ctx.registerCommand('debugSingle', commands.debugSingle);
150 ctx.registerCommand('showReferences', commands.showReferences);
151 ctx.registerCommand('applySnippetWorkspaceEdit', commands.applySnippetWorkspaceEditCommand);
152 ctx.registerCommand('resolveCodeAction', commands.resolveCodeAction);
153 ctx.registerCommand('applyActionGroup', commands.applyActionGroup);
154 ctx.registerCommand('gotoLocation', commands.gotoLocation);
157 export async function deactivate() {
158 await setContextValue(RUST_PROJECT_CONTEXT_NAME, undefined);
159 await ctx?.client.stop();
163 async function bootstrap(config: Config, state: PersistentState): Promise<string> {
164 await fs.mkdir(config.globalStoragePath, { recursive: true });
166 if (!config.currentExtensionIsNightly) {
167 await state.updateNightlyReleaseId(undefined);
169 await bootstrapExtension(config, state);
170 const path = await bootstrapServer(config, state);
174 async function bootstrapExtension(config: Config, state: PersistentState): Promise<void> {
175 if (config.package.releaseTag === null) return;
176 if (config.channel === "stable") {
177 if (config.currentExtensionIsNightly) {
178 void vscode.window.showWarningMessage(
179 `You are running a nightly version of rust-analyzer extension. ` +
180 `To switch to stable, uninstall the extension and re-install it from the marketplace`
185 if (serverPath(config)) return;
187 const now = Date.now();
188 const isInitialNightlyDownload = state.nightlyReleaseId === undefined;
189 if (config.currentExtensionIsNightly) {
190 // Check if we should poll github api for the new nightly version
191 // if we haven't done it during the past hour
192 const lastCheck = state.lastCheck;
194 const anHour = 60 * 60 * 1000;
195 const shouldCheckForNewNightly = isInitialNightlyDownload || (now - (lastCheck ?? 0)) > anHour;
197 if (!shouldCheckForNewNightly) return;
200 const latestNightlyRelease = await downloadWithRetryDialog(state, async () => {
201 return await fetchRelease("nightly", state.githubToken, config.httpProxy);
202 }).catch(async (e) => {
204 if (isInitialNightlyDownload) {
205 await vscode.window.showErrorMessage(`Failed to download rust-analyzer nightly: ${e}`);
209 if (latestNightlyRelease === undefined) {
210 if (isInitialNightlyDownload) {
211 await vscode.window.showErrorMessage("Failed to download rust-analyzer nightly: empty release contents returned");
215 if (config.currentExtensionIsNightly && latestNightlyRelease.id === state.nightlyReleaseId) return;
217 const userResponse = await vscode.window.showInformationMessage(
218 "New version of rust-analyzer (nightly) is available (requires reload).",
221 if (userResponse !== "Update") return;
223 const artifact = latestNightlyRelease.assets.find(artifact => artifact.name === "rust-analyzer.vsix");
224 assert(!!artifact, `Bad release: ${JSON.stringify(latestNightlyRelease)}`);
226 const dest = path.join(config.globalStoragePath, "rust-analyzer.vsix");
228 await downloadWithRetryDialog(state, async () => {
230 url: artifact.browser_download_url,
232 progressTitle: "Downloading rust-analyzer extension",
233 httpProxy: config.httpProxy,
237 await vscode.commands.executeCommand("workbench.extensions.installExtension", vscode.Uri.file(dest));
238 await fs.unlink(dest);
240 await state.updateNightlyReleaseId(latestNightlyRelease.id);
241 await state.updateLastCheck(now);
242 await vscode.commands.executeCommand("workbench.action.reloadWindow");
245 async function bootstrapServer(config: Config, state: PersistentState): Promise<string> {
246 const path = await getServer(config, state);
249 "Rust Analyzer Language Server is not available. " +
250 "Please, ensure its [proper installation](https://rust-analyzer.github.io/manual.html#installation)."
254 log.info("Using server binary at", path);
256 if (!isValidExecutable(path)) {
257 throw new Error(`Failed to execute ${path} --version`);
263 async function patchelf(dest: PathLike): Promise<void> {
264 await vscode.window.withProgress(
266 location: vscode.ProgressLocation.Notification,
267 title: "Patching rust-analyzer for NixOS"
269 async (progress, _) => {
271 {srcStr, pkgs ? import <nixpkgs> {}}:
272 pkgs.stdenv.mkDerivation {
273 name = "rust-analyzer";
275 phases = [ "installPhase" "fixupPhase" ];
276 installPhase = "cp $src $out";
279 patchelf --set-interpreter "$(cat $NIX_CC/nix-support/dynamic-linker)" $out
283 const origFile = dest + "-orig";
284 await fs.rename(dest, origFile);
285 progress.report({ message: "Patching executable", increment: 20 });
286 await new Promise((resolve, reject) => {
287 const handle = exec(`nix-build -E - --argstr srcStr '${origFile}' -o '${dest}'`,
288 (err, stdout, stderr) => {
290 reject(Error(stderr));
295 handle.stdin?.write(expression);
298 await fs.unlink(origFile);
303 async function getServer(config: Config, state: PersistentState): Promise<string | undefined> {
304 const explicitPath = serverPath(config);
306 if (explicitPath.startsWith("~/")) {
307 return os.homedir() + explicitPath.slice("~".length);
311 if (config.package.releaseTag === null) return "rust-analyzer";
313 const platforms: { [key: string]: string } = {
314 "ia32 win32": "x86_64-pc-windows-msvc",
315 "x64 win32": "x86_64-pc-windows-msvc",
316 "x64 linux": "x86_64-unknown-linux-gnu",
317 "x64 darwin": "x86_64-apple-darwin",
318 "arm64 win32": "aarch64-pc-windows-msvc",
319 "arm64 linux": "aarch64-unknown-linux-gnu",
320 "arm64 darwin": "aarch64-apple-darwin",
322 let platform = platforms[`${process.arch} ${process.platform}`];
323 if (platform === undefined) {
324 await vscode.window.showErrorMessage(
325 "Unfortunately we don't ship binaries for your platform yet. " +
326 "You need to manually clone rust-analyzer repository and " +
327 "run `cargo xtask install --server` to build the language server from sources. " +
328 "If you feel that your platform should be supported, please create an issue " +
329 "about that [here](https://github.com/rust-analyzer/rust-analyzer/issues) and we " +
334 if (platform === "x86_64-unknown-linux-gnu" && isMusl()) {
335 platform = "x86_64-unknown-linux-musl";
337 const ext = platform.indexOf("-windows-") !== -1 ? ".exe" : "";
338 const dest = path.join(config.globalStoragePath, `rust-analyzer-${platform}${ext}`);
339 const exists = await fs.stat(dest).then(() => true, () => false);
341 await state.updateServerVersion(undefined);
344 if (state.serverVersion === config.package.version) return dest;
346 if (config.askBeforeDownload) {
347 const userResponse = await vscode.window.showInformationMessage(
348 `Language server version ${config.package.version} for rust-analyzer is not installed.`,
351 if (userResponse !== "Download now") return dest;
354 const releaseTag = config.package.releaseTag;
355 const release = await downloadWithRetryDialog(state, async () => {
356 return await fetchRelease(releaseTag, state.githubToken, config.httpProxy);
358 const artifact = release.assets.find(artifact => artifact.name === `rust-analyzer-${platform}.gz`);
359 assert(!!artifact, `Bad release: ${JSON.stringify(release)}`);
361 await downloadWithRetryDialog(state, async () => {
363 url: artifact.browser_download_url,
365 progressTitle: "Downloading rust-analyzer server",
368 httpProxy: config.httpProxy,
372 // Patching executable if that's NixOS.
373 if (await isNixOs()) {
374 await patchelf(dest);
377 await state.updateServerVersion(config.package.version);
381 function serverPath(config: Config): string | null {
382 return process.env.__RA_LSP_SERVER_DEBUG ?? config.serverPath;
385 async function isNixOs(): Promise<boolean> {
387 const contents = await fs.readFile("/etc/os-release");
388 return contents.indexOf("ID=nixos") !== -1;
394 function isMusl(): boolean {
395 // We can detect Alpine by checking `/etc/os-release` but not Void Linux musl.
396 // Instead, we run `ldd` since it advertises the libc which it belongs to.
397 const res = spawnSync("ldd", ["--version"]);
398 return res.stderr != null && res.stderr.indexOf("musl libc") >= 0;
401 async function downloadWithRetryDialog<T>(state: PersistentState, downloadFunc: () => Promise<T>): Promise<T> {
404 return await downloadFunc();
406 const selected = await vscode.window.showErrorMessage("Failed to download: " + e.message, {}, {
407 title: "Update Github Auth Token",
410 title: "Retry download",
416 if (selected?.updateToken) {
417 await queryForGithubToken(state);
419 } else if (selected?.retry) {
427 async function queryForGithubToken(state: PersistentState): Promise<void> {
428 const githubTokenOptions: vscode.InputBoxOptions = {
429 value: state.githubToken,
432 This dialog allows to store a Github authorization token.
433 The usage of an authorization token will increase the rate
434 limit on the use of Github APIs and can thereby prevent getting
436 Auth tokens can be created at https://github.com/settings/tokens`,
439 const newToken = await vscode.window.showInputBox(githubTokenOptions);
440 if (newToken === undefined) {
441 // The user aborted the dialog => Do not update the stored token
445 if (newToken === "") {
446 log.info("Clearing github token");
447 await state.updateGithubToken(undefined);
449 log.info("Storing new github token");
450 await state.updateGithubToken(newToken);
454 function warnAboutExtensionConflicts() {
455 const conflicting = [
456 ["rust-analyzer", "matklad.rust-analyzer"],
457 ["Rust", "rust-lang.rust"],
458 ["Rust", "kalitaalexey.vscode-rust"],
461 const found = conflicting.filter(
462 nameId => vscode.extensions.getExtension(nameId[1]) !== undefined);
464 if (found.length > 1) {
465 const fst = found[0];
466 const sec = found[1];
467 vscode.window.showWarningMessage(
468 `You have both the ${fst[0]} (${fst[1]}) and ${sec[0]} (${sec[1]}) ` +
469 "plugins enabled. These are known to conflict and cause various functions of " +
470 "both plugins to not work correctly. You should disable one of them.", "Got it")
471 .then(() => { }, console.error);