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 warnAboutRustLangExtensionConflict();
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") {
291 if (process.platform === "linux") platform = "linux";
292 if (process.platform === "darwin") platform = "mac";
293 if (process.platform === "win32") platform = "windows";
295 if (platform === undefined) {
296 vscode.window.showErrorMessage(
297 "Unfortunately we don't ship binaries for your platform yet. " +
298 "You need to manually clone rust-analyzer repository and " +
299 "run `cargo xtask install --server` to build the language server from sources. " +
300 "If you feel that your platform should be supported, please create an issue " +
301 "about that [here](https://github.com/rust-analyzer/rust-analyzer/issues) and we " +
306 const ext = platform === "windows" ? ".exe" : "";
307 const dest = path.join(config.globalStoragePath, `rust-analyzer-${platform}${ext}`);
308 const exists = await fs.stat(dest).then(() => true, () => false);
310 await state.updateServerVersion(undefined);
313 if (state.serverVersion === config.package.version) return dest;
315 if (config.askBeforeDownload) {
316 const userResponse = await vscode.window.showInformationMessage(
317 `Language server version ${config.package.version} for rust-analyzer is not installed.`,
320 if (userResponse !== "Download now") return dest;
323 const releaseTag = config.package.releaseTag;
324 const release = await downloadWithRetryDialog(state, async () => {
325 return await fetchRelease(releaseTag, state.githubToken);
327 const artifact = release.assets.find(artifact => artifact.name === `rust-analyzer-${platform}.gz`);
328 assert(!!artifact, `Bad release: ${JSON.stringify(release)}`);
330 await downloadWithRetryDialog(state, async () => {
332 url: artifact.browser_download_url,
334 progressTitle: "Downloading rust-analyzer server",
341 // Patching executable if that's NixOS.
342 if (await isNixOs()) {
343 await patchelf(dest);
346 await state.updateServerVersion(config.package.version);
350 async function isNixOs(): Promise<boolean> {
352 const contents = await fs.readFile("/etc/os-release");
353 return contents.indexOf("ID=nixos") !== -1;
359 async function downloadWithRetryDialog<T>(state: PersistentState, downloadFunc: () => Promise<T>): Promise<T> {
362 return await downloadFunc();
364 const selected = await vscode.window.showErrorMessage("Failed to download: " + e.message, {}, {
365 title: "Update Github Auth Token",
368 title: "Retry download",
374 if (selected?.updateToken) {
375 await queryForGithubToken(state);
377 } else if (selected?.retry) {
385 async function queryForGithubToken(state: PersistentState): Promise<void> {
386 const githubTokenOptions: vscode.InputBoxOptions = {
387 value: state.githubToken,
390 This dialog allows to store a Github authorization token.
391 The usage of an authorization token will increase the rate
392 limit on the use of Github APIs and can thereby prevent getting
394 Auth tokens can be created at https://github.com/settings/tokens`,
397 const newToken = await vscode.window.showInputBox(githubTokenOptions);
398 if (newToken === undefined) {
399 // The user aborted the dialog => Do not update the stored token
403 if (newToken === "") {
404 log.info("Clearing github token");
405 await state.updateGithubToken(undefined);
407 log.info("Storing new github token");
408 await state.updateGithubToken(newToken);
412 function warnAboutRustLangExtensionConflict() {
413 const rustLangExt = vscode.extensions.getExtension("rust-lang.rust");
414 if (rustLangExt !== undefined) {
415 vscode.window.showWarningMessage(
416 "You have both rust-analyzer (matklad.rust-analyzer) and Rust (rust-lang.rust) " +
417 "plugins enabled. These are known to conflict and cause various functions of " +
418 "both plugins to not work correctly. You should disable one of them.", "Got it");