diff --git a/crates/rust-analyzer/src/config.rs b/crates/rust-analyzer/src/config.rs index 6c311648a9d5..21acfe6445e2 100644 --- a/crates/rust-analyzer/src/config.rs +++ b/crates/rust-analyzer/src/config.rs @@ -130,6 +130,7 @@ pub struct ClientCapsConfig { pub code_action_group: bool, pub resolve_code_action: bool, pub hover_actions: bool, + pub status_notification: bool, } impl Config { @@ -365,6 +366,7 @@ impl Config { self.client_caps.code_action_group = get_bool("codeActionGroup"); self.client_caps.resolve_code_action = get_bool("resolveCodeAction"); self.client_caps.hover_actions = get_bool("hoverActions"); + self.client_caps.status_notification = get_bool("statusNotification"); } } } diff --git a/crates/rust-analyzer/src/global_state.rs b/crates/rust-analyzer/src/global_state.rs index b7b4edf6611f..640b3959df21 100644 --- a/crates/rust-analyzer/src/global_state.rs +++ b/crates/rust-analyzer/src/global_state.rs @@ -31,6 +31,8 @@ use crate::{ pub(crate) enum Status { Loading, Ready, + Invalid, + NeedsReload, } impl Default for Status { diff --git a/crates/rust-analyzer/src/lsp_ext.rs b/crates/rust-analyzer/src/lsp_ext.rs index 82207bbb8b49..d225ad5ff3bf 100644 --- a/crates/rust-analyzer/src/lsp_ext.rs +++ b/crates/rust-analyzer/src/lsp_ext.rs @@ -3,7 +3,7 @@ use std::{collections::HashMap, path::PathBuf}; use lsp_types::request::Request; -use lsp_types::{Position, Range, TextDocumentIdentifier}; +use lsp_types::{notification::Notification, Position, Range, TextDocumentIdentifier}; use serde::{Deserialize, Serialize}; pub enum AnalyzerStatus {} @@ -208,6 +208,22 @@ pub struct SsrParams { pub parse_only: bool, } +pub enum StatusNotification {} + +#[serde(rename_all = "camelCase")] +#[derive(Serialize, Deserialize)] +pub enum Status { + Loading, + Ready, + NeedsReload, + Invalid, +} + +impl Notification for StatusNotification { + type Params = Status; + const METHOD: &'static str = "rust-analyzer/status"; +} + pub enum CodeActionRequest {} impl Request for CodeActionRequest { diff --git a/crates/rust-analyzer/src/main_loop.rs b/crates/rust-analyzer/src/main_loop.rs index e03038b25827..d4d18a8082b1 100644 --- a/crates/rust-analyzer/src/main_loop.rs +++ b/crates/rust-analyzer/src/main_loop.rs @@ -111,6 +111,35 @@ impl GlobalState { } fn run(mut self, inbox: Receiver) -> Result<()> { + let registration_options = lsp_types::TextDocumentRegistrationOptions { + document_selector: Some(vec![ + lsp_types::DocumentFilter { + language: None, + scheme: None, + pattern: Some("**/*.rs".into()), + }, + lsp_types::DocumentFilter { + language: None, + scheme: None, + pattern: Some("**/Cargo.toml".into()), + }, + lsp_types::DocumentFilter { + language: None, + scheme: None, + pattern: Some("**/Cargo.lock".into()), + }, + ]), + }; + let registration = lsp_types::Registration { + id: "textDocument/didSave".to_string(), + method: "textDocument/didSave".to_string(), + register_options: Some(serde_json::to_value(registration_options).unwrap()), + }; + self.send_request::( + lsp_types::RegistrationParams { registrations: vec![registration] }, + |_, _| (), + ); + self.reload(); while let Some(event) = self.next_event(&inbox) { @@ -169,16 +198,16 @@ impl GlobalState { } vfs::loader::Message::Progress { n_total, n_done } => { if n_total == 0 { - self.status = Status::Ready; + self.transition(Status::Invalid); } else { let state = if n_done == 0 { - self.status = Status::Loading; + self.transition(Status::Loading); Progress::Begin } else if n_done < n_total { Progress::Report } else { assert_eq!(n_done, n_total); - self.status = Status::Ready; + self.transition(Status::Ready); Progress::End }; self.report_progress( @@ -274,6 +303,19 @@ impl GlobalState { Ok(()) } + fn transition(&mut self, new_status: Status) { + self.status = Status::Ready; + if self.config.client_caps.status_notification { + let lsp_status = match new_status { + Status::Loading => lsp_ext::Status::Loading, + Status::Ready => lsp_ext::Status::Ready, + Status::Invalid => lsp_ext::Status::Invalid, + Status::NeedsReload => lsp_ext::Status::NeedsReload, + }; + self.send_notification::(lsp_status); + } + } + fn on_request(&mut self, request_received: Instant, req: Request) -> Result<()> { self.register_request(&req, request_received); @@ -383,10 +425,16 @@ impl GlobalState { ); Ok(()) })? - .on::(|this, _params| { + .on::(|this, params| { if let Some(flycheck) = &this.flycheck { flycheck.handle.update(); } + let uri = params.text_document.uri.as_str(); + if uri.ends_with("Cargo.toml") || uri.ends_with("Cargo.lock") { + if matches!(this.status, Status::Ready | Status::Invalid) { + this.transition(Status::NeedsReload); + } + } Ok(()) })? .on::(|this, _params| { diff --git a/crates/rust-analyzer/src/reload.rs b/crates/rust-analyzer/src/reload.rs index 0c1fd1b8b5b0..523b04b97a66 100644 --- a/crates/rust-analyzer/src/reload.rs +++ b/crates/rust-analyzer/src/reload.rs @@ -78,7 +78,7 @@ impl GlobalState { .collect(), }; let registration = lsp_types::Registration { - id: "file-watcher".to_string(), + id: "workspace/didChangeWatchedFiles".to_string(), method: "workspace/didChangeWatchedFiles".to_string(), register_options: Some(serde_json::to_value(registration_options).unwrap()), }; diff --git a/crates/rust-analyzer/tests/heavy_tests/support.rs b/crates/rust-analyzer/tests/heavy_tests/support.rs index 49f194f7efb6..7bf687794c34 100644 --- a/crates/rust-analyzer/tests/heavy_tests/support.rs +++ b/crates/rust-analyzer/tests/heavy_tests/support.rs @@ -176,12 +176,19 @@ impl Server { while let Some(msg) = self.recv() { match msg { Message::Request(req) => { - if req.method != "window/workDoneProgress/create" - && !(req.method == "client/registerCapability" - && req.params.to_string().contains("workspace/didChangeWatchedFiles")) - { - panic!("unexpected request: {:?}", req) + if req.method == "window/workDoneProgress/create" { + continue; } + if req.method == "client/registerCapability" { + let params = req.params.to_string(); + if ["workspace/didChangeWatchedFiles", "textDocument/didSave"] + .iter() + .any(|&it| params.contains(it)) + { + continue; + } + } + panic!("unexpected request: {:?}", req) } Message::Notification(_) => (), Message::Response(res) => { diff --git a/docs/dev/lsp-extensions.md b/docs/dev/lsp-extensions.md index c0afb16d3b20..6d6bbac7ca1d 100644 --- a/docs/dev/lsp-extensions.md +++ b/docs/dev/lsp-extensions.md @@ -399,6 +399,18 @@ Returns internal status message, mostly for debugging purposes. Reloads project information (that is, re-executes `cargo metadata`). +## Status Notification + +**Client Capability:** `{ "statusNotification": boolean }` + +**Method:** `rust-analyzer/status` + +**Notification:** `"loading" | "ready" | "invalid" | "needsReload"` + +This notification is sent from server to client. +The client can use it to display persistent status to the user (in modline). +For `needsReload` state, the client can provide a context-menu action to run `rust-analyzer/reloadWorkspace` request. + ## Syntax Tree **Method:** `rust-analyzer/syntaxTree` diff --git a/editors/code/src/client.ts b/editors/code/src/client.ts index 65ad573d8c91..3e5915c28d4b 100644 --- a/editors/code/src/client.ts +++ b/editors/code/src/client.ts @@ -161,6 +161,7 @@ class ExperimentalFeatures implements lc.StaticFeature { caps.codeActionGroup = true; caps.resolveCodeAction = true; caps.hoverActions = true; + caps.statusNotification = true; capabilities.experimental = caps; } initialize(_capabilities: lc.ServerCapabilities, _documentSelector: lc.DocumentSelector | undefined): void { diff --git a/editors/code/src/ctx.ts b/editors/code/src/ctx.ts index 41df119910a4..6e767babf449 100644 --- a/editors/code/src/ctx.ts +++ b/editors/code/src/ctx.ts @@ -1,9 +1,11 @@ import * as vscode from 'vscode'; import * as lc from 'vscode-languageclient'; +import * as ra from './lsp_ext'; import { Config } from './config'; import { createClient } from './client'; import { isRustEditor, RustEditor } from './util'; +import { Status } from './lsp_ext'; export class Ctx { private constructor( @@ -11,6 +13,7 @@ export class Ctx { private readonly extCtx: vscode.ExtensionContext, readonly client: lc.LanguageClient, readonly serverPath: string, + readonly statusBar: vscode.StatusBarItem, ) { } @@ -22,9 +25,18 @@ export class Ctx { cwd: string, ): Promise { const client = createClient(serverPath, cwd); - const res = new Ctx(config, extCtx, client, serverPath); + + const statusBar = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Left); + extCtx.subscriptions.push(statusBar); + statusBar.text = "rust-analyzer"; + statusBar.tooltip = "ready"; + statusBar.show(); + + const res = new Ctx(config, extCtx, client, serverPath, statusBar); + res.pushCleanup(client.start()); await client.onReady(); + client.onNotification(ra.status, (status) => res.setStatus(status)); return res; } @@ -54,6 +66,35 @@ export class Ctx { return this.extCtx.subscriptions; } + setStatus(status: Status) { + switch (status) { + case "loading": + this.statusBar.text = "$(sync~spin) rust-analyzer"; + this.statusBar.tooltip = "Loading the project"; + this.statusBar.command = undefined; + this.statusBar.color = undefined; + break; + case "ready": + this.statusBar.text = "rust-analyzer"; + this.statusBar.tooltip = "Ready"; + this.statusBar.command = undefined; + this.statusBar.color = undefined; + break; + case "invalid": + this.statusBar.text = "$(error) rust-analyzer"; + this.statusBar.tooltip = "Failed to load the project"; + this.statusBar.command = undefined; + this.statusBar.color = new vscode.ThemeColor("notificationsErrorIcon.foreground"); + break; + case "needsReload": + this.statusBar.text = "$(warning) rust-analyzer"; + this.statusBar.tooltip = "Click to reload"; + this.statusBar.command = "rust-analyzer.reloadWorkspace"; + this.statusBar.color = new vscode.ThemeColor("notificationsWarningIcon.foreground"); + break; + } + } + pushCleanup(d: Disposable) { this.extCtx.subscriptions.push(d); } diff --git a/editors/code/src/lsp_ext.ts b/editors/code/src/lsp_ext.ts index 981b6f40ecbe..bf4703239d1e 100644 --- a/editors/code/src/lsp_ext.ts +++ b/editors/code/src/lsp_ext.ts @@ -6,6 +6,9 @@ import * as lc from "vscode-languageclient"; export const analyzerStatus = new lc.RequestType("rust-analyzer/analyzerStatus"); +export type Status = "loading" | "ready" | "invalid" | "needsReload"; +export const status = new lc.NotificationType("rust-analyzer/status"); + export const reloadWorkspace = new lc.RequestType("rust-analyzer/reloadWorkspace"); export interface SyntaxTreeParams {