Repository: rust-analyzer/rust-analyzer
Pull request #8355 (2021-04-05 19:31:55)

internal: do not drop errors from cargo metadata/check
Work towards #3155


Patch:
diff --git a/crates/project_model/src/build_data.rs b/crates/project_model/src/build_data.rs
index f7050be4e7..c2c87b207b 100644
--- a/crates/project_model/src/build_data.rs
+++ b/crates/project_model/src/build_data.rs
@@ -52,7 +52,7 @@ pub struct BuildDataCollector {
     configs: FxHashMap<AbsPathBuf, BuildDataConfig>,
 }
 
-#[derive(Debug, Default, PartialEq, Eq)]
+#[derive(Debug, Default, PartialEq, Eq, Clone)]
 pub struct BuildDataResult {
     data: FxHashMap<AbsPathBuf, BuildDataMap>,
 }
diff --git a/crates/rust-analyzer/src/config.rs b/crates/rust-analyzer/src/config.rs
index cda272fd48..e012b4452f 100644
--- a/crates/rust-analyzer/src/config.rs
+++ b/crates/rust-analyzer/src/config.rs
@@ -445,8 +445,8 @@ impl Config {
     pub fn hover_actions(&self) -> bool {
         self.experimental("hoverActions")
     }
-    pub fn status_notification(&self) -> bool {
-        self.experimental("statusNotification")
+    pub fn server_status_notification(&self) -> bool {
+        self.experimental("serverStatusNotification")
     }
 
     pub fn publish_diagnostics(&self) -> bool {
diff --git a/crates/rust-analyzer/src/global_state.rs b/crates/rust-analyzer/src/global_state.rs
index 8679c85996..adeb7a97e8 100644
--- a/crates/rust-analyzer/src/global_state.rs
+++ b/crates/rust-analyzer/src/global_state.rs
@@ -23,6 +23,7 @@ use crate::{
     document::DocumentData,
     from_proto,
     line_index::{LineEndings, LineIndex},
+    lsp_ext,
     main_loop::Task,
     op_queue::OpQueue,
     reload::SourceRootConfig,
@@ -32,20 +33,6 @@ use crate::{
     Result,
 };
 
-#[derive(Eq, PartialEq, Copy, Clone)]
-pub(crate) enum Status {
-    Loading,
-    Ready { partial: bool },
-    Invalid,
-    NeedsReload,
-}
-
-impl Default for Status {
-    fn default() -> Self {
-        Status::Loading
-    }
-}
-
 // Enforces drop order
 pub(crate) struct Handle<H, C> {
     pub(crate) handle: H,
@@ -67,26 +54,36 @@ pub(crate) struct GlobalState {
     req_queue: ReqQueue,
     pub(crate) task_pool: Handle<TaskPool<Task>, Receiver<Task>>,
     pub(crate) loader: Handle<Box<dyn vfs::loader::Handle>, Receiver<vfs::loader::Message>>,
-    pub(crate) vfs_config_version: u32,
-    pub(crate) flycheck: Vec<FlycheckHandle>,
-    pub(crate) flycheck_sender: Sender<flycheck::Message>,
-    pub(crate) flycheck_receiver: Receiver<flycheck::Message>,
     pub(crate) config: Arc<Config>,
     pub(crate) analysis_host: AnalysisHost,
     pub(crate) diagnostics: DiagnosticCollection,
     pub(crate) mem_docs: FxHashMap<VfsPath, DocumentData>,
     pub(crate) semantic_tokens_cache: Arc<Mutex<FxHashMap<Url, SemanticTokens>>>,
-    pub(crate) vfs: Arc<RwLock<(vfs::Vfs, FxHashMap<FileId, LineEndings>)>>,
     pub(crate) shutdown_requested: bool,
-    pub(crate) status: Status,
+    pub(crate) last_reported_status: Option<lsp_ext::ServerStatusParams>,
     pub(crate) source_root_config: SourceRootConfig,
     pub(crate) proc_macro_client: Option<ProcMacroClient>,
 
-    pub(crate) workspaces: Arc<Vec<ProjectWorkspace>>,
-    pub(crate) fetch_workspaces_queue: OpQueue<(), ()>,
+    pub(crate) flycheck: Vec<FlycheckHandle>,
+    pub(crate) flycheck_sender: Sender<flycheck::Message>,
+    pub(crate) flycheck_receiver: Receiver<flycheck::Message>,
 
+    pub(crate) vfs: Arc<RwLock<(vfs::Vfs, FxHashMap<FileId, LineEndings>)>>,
+    pub(crate) vfs_config_version: u32,
+    pub(crate) vfs_progress_config_version: u32,
+    pub(crate) vfs_progress_n_total: usize,
+    pub(crate) vfs_progress_n_done: usize,
+
+    /// For both `workspaces` and `workspace_build_data`, the field stores the
+    /// data we actually use, while the `OpQueue` stores the result of the last
+    /// fetch.
+    ///
+    /// If the fetch (partially) fails, we do not update the values.
+    pub(crate) workspaces: Arc<Vec<ProjectWorkspace>>,
+    pub(crate) fetch_workspaces_queue: OpQueue<(), Vec<anyhow::Result<ProjectWorkspace>>>,
     pub(crate) workspace_build_data: Option<BuildDataResult>,
-    pub(crate) fetch_build_data_queue: OpQueue<BuildDataCollector, ()>,
+    pub(crate) fetch_build_data_queue:
+        OpQueue<BuildDataCollector, Option<anyhow::Result<BuildDataResult>>>,
 
     latest_requests: Arc<RwLock<LatestRequests>>,
 }
@@ -124,25 +121,32 @@ impl GlobalState {
         GlobalState {
             sender,
             req_queue: ReqQueue::default(),
-            vfs_config_version: 0,
             task_pool,
             loader,
-            flycheck: Vec::new(),
-            flycheck_sender,
-            flycheck_receiver,
             config: Arc::new(config),
             analysis_host,
             diagnostics: Default::default(),
             mem_docs: FxHashMap::default(),
             semantic_tokens_cache: Arc::new(Default::default()),
-            vfs: Arc::new(RwLock::new((vfs::Vfs::default(), FxHashMap::default()))),
             shutdown_requested: false,
-            status: Status::default(),
+            last_reported_status: None,
             source_root_config: SourceRootConfig::default(),
             proc_macro_client: None,
+
+            flycheck: Vec::new(),
+            flycheck_sender,
+            flycheck_receiver,
+
+            vfs: Arc::new(RwLock::new((vfs::Vfs::default(), FxHashMap::default()))),
+            vfs_config_version: 0,
+            vfs_progress_config_version: 0,
+            vfs_progress_n_total: 0,
+            vfs_progress_n_done: 0,
+
             workspaces: Arc::new(Vec::new()),
             fetch_workspaces_queue: OpQueue::default(),
             workspace_build_data: None,
+
             fetch_build_data_queue: OpQueue::default(),
             latest_requests: Default::default(),
         }
diff --git a/crates/rust-analyzer/src/lsp_ext.rs b/crates/rust-analyzer/src/lsp_ext.rs
index 0e1fec2095..81a6f22f16 100644
--- a/crates/rust-analyzer/src/lsp_ext.rs
+++ b/crates/rust-analyzer/src/lsp_ext.rs
@@ -241,26 +241,26 @@ pub struct SsrParams {
     pub selections: Vec<lsp_types::Range>,
 }
 
-pub enum StatusNotification {}
+pub enum ServerStatusNotification {}
 
-#[derive(Serialize, Deserialize)]
-#[serde(rename_all = "camelCase")]
-pub enum Status {
-    Loading,
-    ReadyPartial,
-    Ready,
-    NeedsReload,
-    Invalid,
+impl Notification for ServerStatusNotification {
+    type Params = ServerStatusParams;
+    const METHOD: &'static str = "experimental/serverStatus";
 }
 
-#[derive(Deserialize, Serialize)]
-pub struct StatusParams {
-    pub status: Status,
+#[derive(Deserialize, Serialize, PartialEq, Eq, Clone)]
+pub struct ServerStatusParams {
+    pub health: Health,
+    pub quiescent: bool,
+    pub message: Option<String>,
 }
 
-impl Notification for StatusNotification {
-    type Params = StatusParams;
-    const METHOD: &'static str = "rust-analyzer/status";
+#[derive(Serialize, Deserialize, Clone, Copy, PartialEq, Eq)]
+#[serde(rename_all = "camelCase")]
+pub enum Health {
+    Ok,
+    Warning,
+    Error,
 }
 
 pub enum CodeActionRequest {}
diff --git a/crates/rust-analyzer/src/main_loop.rs b/crates/rust-analyzer/src/main_loop.rs
index e88f16cc11..a5655116b4 100644
--- a/crates/rust-analyzer/src/main_loop.rs
+++ b/crates/rust-analyzer/src/main_loop.rs
@@ -2,6 +2,7 @@
 //! requests/replies and notifications back to the client.
 use std::{
     env, fmt,
+    sync::Arc,
     time::{Duration, Instant},
 };
 
@@ -12,6 +13,7 @@ use ide::{Canceled, FileId};
 use ide_db::base_db::VfsPath;
 use lsp_server::{Connection, Notification, Request, Response};
 use lsp_types::notification::Notification as _;
+use project_model::BuildDataCollector;
 use vfs::ChangeKind;
 
 use crate::{
@@ -19,7 +21,7 @@ use crate::{
     dispatch::{NotificationDispatcher, RequestDispatcher},
     document::DocumentData,
     from_proto,
-    global_state::{file_id_to_url, url_to_file_id, GlobalState, Status},
+    global_state::{file_id_to_url, url_to_file_id, GlobalState},
     handlers, lsp_ext,
     lsp_utils::{apply_document_changes, is_canceled, notification_is, Progress},
     reload::{BuildDataProgress, ProjectWorkspaceProgress},
@@ -187,7 +189,7 @@ impl GlobalState {
             log::info!("task queue len: {}", task_queue_len);
         }
 
-        let mut new_status = self.status;
+        let was_quiescent = self.is_quiescent();
         match event {
             Event::Lsp(msg) => match msg {
                 lsp_server::Message::Request(req) => self.on_request(loop_start, req)?,
@@ -227,11 +229,24 @@ impl GlobalState {
                                     (Progress::Report, Some(msg))
                                 }
                                 ProjectWorkspaceProgress::End(workspaces) => {
-                                    self.fetch_workspaces_completed();
-                                    self.switch_workspaces(workspaces, None);
+                                    self.fetch_workspaces_completed(workspaces);
+
+                                    let old = Arc::clone(&self.workspaces);
+                                    self.switch_workspaces();
+                                    let workspaces_updated = !Arc::ptr_eq(&old, &self.workspaces);
+
+                                    if self.config.run_build_scripts() && workspaces_updated {
+                                        let mut collector = BuildDataCollector::default();
+                                        for ws in self.workspaces.iter() {
+                                            ws.collect_build_data_configs(&mut collector);
+                                        }
+                                        self.fetch_build_data_request(collector)
+                                    }
+
                                     (Progress::End, None)
                                 }
                             };
+
                             self.report_progress("fetching", state, msg, None);
                         }
                         Task::FetchBuildData(progress) => {
@@ -240,19 +255,21 @@ impl GlobalState {
                                 BuildDataProgress::Report(msg) => {
                                     (Some(Progress::Report), Some(msg))
                                 }
-                                BuildDataProgress::End(collector) => {
-                                    self.fetch_build_data_completed();
-                                    let workspaces =
-                                        (*self.workspaces).clone().into_iter().map(Ok).collect();
-                                    self.switch_workspaces(workspaces, Some(collector));
+                                BuildDataProgress::End(build_data_result) => {
+                                    self.fetch_build_data_completed(build_data_result);
+
+                                    self.switch_workspaces();
+
                                     (Some(Progress::End), None)
                                 }
                             };
+
                             if let Some(state) = state {
                                 self.report_progress("loading", state, msg, None);
                             }
                         }
                     }
+
                     // Coalesce multiple task events into one loop turn
                     task = match self.task_pool.receiver.try_recv() {
                         Ok(task) => task,
@@ -298,30 +315,25 @@ impl GlobalState {
                         }
                         vfs::loader::Message::Progress { n_total, n_done, config_version } => {
                             always!(config_version <= self.vfs_config_version);
-                            if n_total == 0 {
-                                new_status = Status::Invalid;
+
+                            self.vfs_progress_config_version = config_version;
+                            self.vfs_progress_n_total = n_total;
+                            self.vfs_progress_n_done = n_done;
+
+                            let state = if n_done == 0 {
+                                Progress::Begin
+                            } else if n_done < n_total {
+                                Progress::Report
                             } else {
-                                let state = if n_done == 0 {
-                                    new_status = Status::Loading;
-                                    Progress::Begin
-                                } else if n_done < n_total {
-                                    Progress::Report
-                                } else {
-                                    assert_eq!(n_done, n_total);
-                                    new_status = Status::Ready {
-                                        partial: self.config.run_build_scripts()
-                                            && self.workspace_build_data.is_none()
-                                            || config_version < self.vfs_config_version,
-                                    };
-                                    Progress::End
-                                };
-                                self.report_progress(
-                                    "roots scanned",
-                                    state,
-                                    Some(format!("{}/{}", n_done, n_total)),
-                                    Some(Progress::fraction(n_done, n_total)),
-                                )
-                            }
+                                assert_eq!(n_done, n_total);
+                                Progress::End
+                            };
+                            self.report_progress(
+                                "roots scanned",
+                                state,
+                                Some(format!("{}/{}", n_done, n_total)),
+                                Some(Progress::fraction(n_done, n_total)),
+                            )
                         }
                     }
                     // Coalesce many VFS event into a single loop turn
@@ -397,18 +409,14 @@ impl GlobalState {
         }
 
         let state_changed = self.process_changes();
-        let prev_status = self.status;
-        if prev_status != new_status {
-            self.transition(new_status);
-        }
-        let is_ready = matches!(self.status, Status::Ready { .. });
-        if prev_status == Status::Loading && is_ready {
+
+        if self.is_quiescent() && !was_quiescent {
             for flycheck in &self.flycheck {
                 flycheck.update();
             }
         }
 
-        if is_ready && (state_changed || prev_status == Status::Loading) {
+        if self.is_quiescent() && (!was_quiescent || state_changed) {
             self.update_file_notifications_on_threadpool();
 
             // Refresh semantic tokens if the client supports it.
@@ -437,9 +445,13 @@ impl GlobalState {
             }
         }
 
-        self.fetch_workspaces_if_needed();
+        if self.config.cargo_autoreload() {
+            self.fetch_workspaces_if_needed();
+        }
         self.fetch_build_data_if_needed();
 
+        self.report_new_status_if_needed();
+
         let loop_duration = loop_start.elapsed();
         if loop_duration > Duration::from_millis(100) {
             log::warn!("overly long loop turn: {:?}", loop_duration);
@@ -466,7 +478,8 @@ impl GlobalState {
             return Ok(());
         }
 
-        if self.status == Status::Loading && req.method != "shutdown" {
+        // Avoid flashing a bunch of unresolved references during initial load.
+        if self.workspaces.is_empty() && !self.is_quiescent() {
             self.respond(lsp_server::Response::new_err(
                 req.id,
                 // FIXME: i32 should impl From<ErrorCode> (from() guarantees lossless conversion)
@@ -477,7 +490,11 @@ impl GlobalState {
         }
 
         RequestDispatcher { req: Some(req), global_state: self }
-            .on_sync::<lsp_ext::ReloadWorkspace>(|s, ()| Ok(s.fetch_workspaces_request()))?
+            .on_sync::<lsp_ext::ReloadWorkspace>(|s, ()| {
+                s.fetch_workspaces_request();
+                s.fetch_workspaces_if_needed();
+                Ok(())
+            })?
             .on_sync::<lsp_ext::JoinLines>(|s, p| handlers::handle_join_lines(s.snapshot(), p))?
             .on_sync::<lsp_ext::OnEnter>(|s, p| handlers::handle_on_enter(s.snapshot(), p))?
             .on_sync::<lsp_types::request::Shutdown>(|s, ()| {
diff --git a/crates/rust-analyzer/src/op_queue.rs b/crates/rust-analyzer/src/op_queue.rs
index f71b718bc0..1d612a9337 100644
--- a/crates/rust-analyzer/src/op_queue.rs
+++ b/crates/rust-analyzer/src/op_queue.rs
@@ -2,27 +2,27 @@
 //! at a time.
 
 pub(crate) struct OpQueue<Args, Output> {
-    op_scheduled: Option<Args>,
+    op_requested: Option<Args>,
     op_in_progress: bool,
     last_op_result: Output,
 }
 
 impl<Args, Output: Default> Default for OpQueue<Args, Output> {
     fn default() -> Self {
-        Self { op_scheduled: None, op_in_progress: false, last_op_result: Default::default() }
+        Self { op_requested: None, op_in_progress: false, last_op_result: Default::default() }
     }
 }
 
 impl<Args, Output> OpQueue<Args, Output> {
     pub(crate) fn request_op(&mut self, data: Args) {
-        self.op_scheduled = Some(data);
+        self.op_requested = Some(data);
     }
     pub(crate) fn should_start_op(&mut self) -> Option<Args> {
         if self.op_in_progress {
             return None;
         }
-        self.op_in_progress = self.op_scheduled.is_some();
-        self.op_scheduled.take()
+        self.op_in_progress = self.op_requested.is_some();
+        self.op_requested.take()
     }
     pub(crate) fn op_completed(&mut self, result: Output) {
         assert!(self.op_in_progress);
@@ -34,4 +34,10 @@ impl<Args, Output> OpQueue<Args, Output> {
     pub(crate) fn last_op_result(&self) -> &Output {
         &self.last_op_result
     }
+    pub(crate) fn op_in_progress(&self) -> bool {
+        self.op_in_progress
+    }
+    pub(crate) fn op_requested(&self) -> bool {
+        self.op_requested.is_some()
+    }
 }
diff --git a/crates/rust-analyzer/src/reload.rs b/crates/rust-analyzer/src/reload.rs
index d12f891bbc..301c7003b9 100644
--- a/crates/rust-analyzer/src/reload.rs
+++ b/crates/rust-analyzer/src/reload.rs
@@ -9,11 +9,10 @@ use vfs::{file_set::FileSetConfig, AbsPath, AbsPathBuf, ChangeKind};
 
 use crate::{
     config::{Config, FilesWatcher, LinkedProject},
-    global_state::{GlobalState, Status},
+    global_state::GlobalState,
     lsp_ext,
     main_loop::Task,
 };
-use lsp_ext::StatusParams;
 
 #[derive(Debug)]
 pub(crate) enum ProjectWorkspaceProgress {
@@ -30,6 +29,13 @@ pub(crate) enum BuildDataProgress {
 }
 
 impl GlobalState {
+    pub(crate) fn is_quiescent(&self) -> bool {
+        !(self.fetch_workspaces_queue.op_in_progress()
+            || self.fetch_build_data_queue.op_in_progress()
+            || self.vfs_progress_config_version < self.vfs_config_version
+            || self.vfs_progress_n_done < self.vfs_progress_n_total)
+    }
+
     pub(crate) fn update_configuration(&mut self, config: Config) {
         let _p = profile::span("GlobalState::update_configuration");
         let old_config = mem::replace(&mut self.config, Arc::new(config));
@@ -46,25 +52,17 @@ impl GlobalState {
         if !changes.iter().any(|(path, kind)| is_interesting(path, *kind)) {
             return;
         }
-        match self.status {
-            Status::Loading | Status::NeedsReload => return,
-            Status::Ready { .. } | Status::Invalid => (),
-        }
         log::info!(
-            "Reloading workspace because of the following changes: {}",
+            "Requesting workspace reload because of the following changes: {}",
             itertools::join(
                 changes
                     .iter()
                     .filter(|(path, kind)| is_interesting(path, *kind))
-                    .map(|(path, kind)| format!("{}/{:?}", path.display(), kind)),
+                    .map(|(path, kind)| format!("{}: {:?}", path.display(), kind)),
                 ", "
             )
         );
-        if self.config.cargo_autoreload() {
-            self.fetch_workspaces_request();
-        } else {
-            self.transition(Status::NeedsReload);
-        }
+        self.fetch_workspaces_request();
 
         fn is_interesting(path: &AbsPath, change_kind: ChangeKind) -> bool {
             const IMPLICIT_TARGET_FILES: &[&str] = &["build.rs", "src/main.rs", "src/lib.rs"];
@@ -101,46 +99,32 @@ impl GlobalState {
             false
         }
     }
-    pub(crate) fn transition(&mut self, new_status: Status) {
-        self.status = new_status;
-        if self.config.status_notification() {
-            let lsp_status = match new_status {
-                Status::Loading => lsp_ext::Status::Loading,
-                Status::Ready { partial: true } => lsp_ext::Status::ReadyPartial,
-                Status::Ready { partial: false } => lsp_ext::Status::Ready,
-                Status::Invalid => lsp_ext::Status::Invalid,
-                Status::NeedsReload => lsp_ext::Status::NeedsReload,
-            };
-            self.send_notification::<lsp_ext::StatusNotification>(StatusParams {
-                status: lsp_status,
-            });
+    pub(crate) fn report_new_status_if_needed(&mut self) {
+        if !self.config.server_status_notification() {
+            return;
         }
-    }
-
-    pub(crate) fn fetch_build_data_request(&mut self, build_data_collector: BuildDataCollector) {
-        self.fetch_build_data_queue.request_op(build_data_collector);
-    }
 
-    pub(crate) fn fetch_build_data_if_needed(&mut self) {
-        let mut build_data_collector = match self.fetch_build_data_queue.should_start_op() {
-            Some(it) => it,
-            None => return,
+        let mut status = lsp_ext::ServerStatusParams {
+            health: lsp_ext::Health::Ok,
+            quiescent: self.is_quiescent(),
+            message: None,
         };
-        self.task_pool.handle.spawn_with_sender(move |sender| {
-            sender.send(Task::FetchBuildData(BuildDataProgress::Begin)).unwrap();
+        if !self.config.cargo_autoreload()
+            && self.is_quiescent()
+            && self.fetch_workspaces_queue.op_requested()
+        {
+            status.health = lsp_ext::Health::Warning;
+            status.message = Some("Workspace reload required".to_string())
+        }
+        if let Some(error) = self.loading_error() {
+            status.health = lsp_ext::Health::Error;
+            status.message = Some(format!("Workspace reload failed: {}", error))
+        }
 
-            let progress = {
-                let sender = sender.clone();
-                move |msg| {
-                    sender.send(Task::FetchBuildData(BuildDataProgress::Report(msg))).unwrap()
-                }
-            };
-            let res = build_data_collector.collect(&progress);
-            sender.send(Task::FetchBuildData(BuildDataProgress::End(res))).unwrap();
-        });
-    }
-    pub(crate) fn fetch_build_data_completed(&mut self) {
-        self.fetch_build_data_queue.op_completed(())
+        if self.last_reported_status.as_ref() != Some(&status) {
+            self.last_reported_status = Some(status.clone());
+            self.send_notification::<lsp_ext::ServerStatusNotification>(status);
+        }
     }
 
     pub(crate) fn fetch_workspaces_request(&mut self) {
@@ -194,57 +178,69 @@ impl GlobalState {
             }
         });
     }
-    pub(crate) fn fetch_workspaces_completed(&mut self) {
-        self.fetch_workspaces_queue.op_completed(())
+    pub(crate) fn fetch_workspaces_completed(
+        &mut self,
+        workspaces: Vec<anyhow::Result<ProjectWorkspace>>,
+    ) {
+        self.fetch_workspaces_queue.op_completed(workspaces)
     }
 
-    pub(crate) fn switch_workspaces(
+    pub(crate) fn fetch_build_data_request(&mut self, build_data_collector: BuildDataCollector) {
+        self.fetch_build_data_queue.request_op(build_data_collector);
+    }
+    pub(crate) fn fetch_build_data_if_needed(&mut self) {
+        let mut build_data_collector = match self.fetch_build_data_queue.should_start_op() {
+            Some(it) => it,
+            None => return,
+        };
+        self.task_pool.handle.spawn_with_sender(move |sender| {
+            sender.send(Task::FetchBuildData(BuildDataProgress::Begin)).unwrap();
+
+            let progress = {
+                let sender = sender.clone();
+                move |msg| {
+                    sender.send(Task::FetchBuildData(BuildDataProgress::Report(msg))).unwrap()
+                }
+            };
+            let res = build_data_collector.collect(&progress);
+            sender.send(Task::FetchBuildData(BuildDataProgress::End(res))).unwrap();
+        });
+    }
+    pub(crate) fn fetch_build_data_completed(
         &mut self,
-        workspaces: Vec<anyhow::Result<ProjectWorkspace>>,
-        workspace_build_data: Option<anyhow::Result<BuildDataResult>>,
+        build_data: anyhow::Result<BuildDataResult>,
     ) {
-        let _p = profile::span("GlobalState::switch_workspaces");
-        log::info!("will switch workspaces: {:?}", workspaces);
+        self.fetch_build_data_queue.op_completed(Some(build_data))
+    }
 
-        let mut has_errors = false;
-        let workspaces = workspaces
-            .into_iter()
-            .filter_map(|res| {
-                res.map_err(|err| {
-                    has_errors = true;
-                    log::error!("failed to load workspace: {:#}", err);
-                    if self.workspaces.is_empty() {
-                        self.show_message(
-                            lsp_types::MessageType::Error,
-                            format!("rust-analyzer failed to load workspace: {:#}", err),
-                        );
-                    }
-                })
-                .ok()
-            })
-            .collect::<Vec<_>>();
+    pub(crate) fn switch_workspaces(&mut self) {
+        let _p = profile::span("GlobalState::switch_workspaces");
+        log::info!("will switch workspaces");
 
-        let workspace_build_data = match workspace_build_data {
-            Some(Ok(it)) => Some(it),
-            Some(Err(err)) => {
-                log::error!("failed to fetch build data: {:#}", err);
-                self.show_message(
-                    lsp_types::MessageType::Error,
-                    format!("rust-analyzer failed to fetch build data: {:#}", err),
-                );
+        if let Some(error_message) = self.loading_error() {
+            log::error!("failed to switch workspaces: {}", error_message);
+            self.show_message(lsp_types::MessageType::Error, error_message);
+            if !self.workspaces.is_empty() {
                 return;
             }
-            None => None,
+        }
+
+        let workspaces = self
+            .fetch_workspaces_queue
+            .last_op_result()
+            .iter()
+            .filter_map(|res| res.as_ref().ok().cloned())
+            .collect::<Vec<_>>();
+
+        let workspace_build_data = match self.fetch_build_data_queue.last_op_result() {
+            Some(Ok(it)) => Some(it.clone()),
+            None | Some(Err(_)) => None,
         };
 
         if *self.workspaces == workspaces && self.workspace_build_data == workspace_build_data {
             return;
         }
 
-        if !self.workspaces.is_empty() && has_errors {
-            return;
-        }
-
         if let FilesWatcher::Client = self.config.files().watcher {
             if self.config.did_change_watched_files_dynamic_registration() {
                 let registration_options = lsp_types::DidChangeWatchedFilesRegistrationOptions {
@@ -337,14 +333,6 @@ impl GlobalState {
         };
         change.set_crate_graph(crate_graph);
 
-        if self.config.run_build_scripts() && workspace_build_data.is_none() {
-            let mut collector = BuildDataCollector::default();
-            for ws in &workspaces {
-                ws.collect_build_data_configs(&mut collector);
-            }
-            self.fetch_build_data_request(collector)
-        }
-
         self.source_root_config = project_folders.source_root_config;
         self.workspaces = Arc::new(workspaces);
         self.workspace_build_data = workspace_build_data;
@@ -355,6 +343,24 @@ impl GlobalState {
         log::info!("did switch workspaces");
     }
 
+    fn loading_error(&self) -> Option<String> {
+        let mut message = None;
+
+        for ws in self.fetch_workspaces_queue.last_op_result() {
+            if let Err(err) = ws {
+                let message = message.get_or_insert_with(String::new);
+                stdx::format_to!(message, "rust-analyzer failed to load workspace: {:#}\n", err);
+            }
+        }
+
+        if let Some(Err(err)) = self.fetch_build_data_queue.last_op_result() {
+            let message = message.get_or_insert_with(String::new);
+            stdx::format_to!(message, "rust-analyzer failed to fetch build data: {:#}\n", err);
+        }
+
+        message
+    }
+
     fn reload_flycheck(&mut self) {
         let _p = profile::span("GlobalState::reload_flycheck");
         let config = match self.config.flycheck() {
diff --git a/docs/dev/lsp-extensions.md b/docs/dev/lsp-extensions.md
index 73be59a82e..989771ac6f 100644
--- a/docs/dev/lsp-extensions.md
+++ b/docs/dev/lsp-extensions.md
@@ -1,5 +1,5 @@
 <!---
-lsp_ext.rs hash: e8a7502bd2b2c2f5
+lsp_ext.rs hash: faae991334a151d0
 
 If you need to change the above hash to make the test pass, please check if you
 need to adjust this doc as well and ping this  issue:
@@ -419,23 +419,37 @@ Returns internal status message, mostly for debugging purposes.
 
 Reloads project information (that is, re-executes `cargo metadata`).
 
-## Status Notification
+## Server Status
 
-**Experimental Client Capability:** `{ "statusNotification": boolean }`
+**Experimental Client Capability:** `{ "serverStatus": boolean }`
 
-**Method:** `rust-analyzer/status`
+**Method:** `experimental/serverStatus`
 
 **Notification:**
 
 ```typescript
-interface StatusParams {
-    status: "loading" | "readyPartial" | "ready" | "invalid" | "needsReload",
+interface ServerStatusParams {
+    /// `ok` means that the server is completely functional.
+    ///
+    /// `warning` means that the server is partially functional.
+    /// It can server requests, but some results might be wrong due to,
+    /// for example, some missing dependencies.
+    ///
+    /// `error` means that the server is not functional. For example,
+    /// there's a fatal build configuration problem.
+    health: "ok" | "warning" | "error",
+    /// Is there any pending background work which might change the status?
+    /// For example, are dependencies being downloaded?
+    quiescent: bool,
+    /// Explanatory message to show on hover.
+    message?: string,
 }
 ```
 
 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.
+The client can use it to display *persistent* status to the user (in modline).
+It is similar to the `showMessage`, but is intended for stares rather than point-in-time events.
+
 
 ## Syntax Tree
 
diff --git a/editors/code/src/client.ts b/editors/code/src/client.ts
index 0771ca3b69..116f41df6e 100644
--- a/editors/code/src/client.ts
+++ b/editors/code/src/client.ts
@@ -159,7 +159,7 @@ class ExperimentalFeatures implements lc.StaticFeature {
         caps.snippetTextEdit = true;
         caps.codeActionGroup = true;
         caps.hoverActions = true;
-        caps.statusNotification = true;
+        caps.serverStatusNotification = true;
         capabilities.experimental = caps;
     }
     initialize(_capabilities: lc.ServerCapabilities<any>, _documentSelector: lc.DocumentSelector | undefined): void {
diff --git a/editors/code/src/ctx.ts b/editors/code/src/ctx.ts
index c07583cfa2..bd023f803e 100644
--- a/editors/code/src/ctx.ts
+++ b/editors/code/src/ctx.ts
@@ -5,7 +5,7 @@ import * as ra from './lsp_ext';
 import { Config } from './config';
 import { createClient } from './client';
 import { isRustEditor, RustEditor } from './util';
-import { Status } from './lsp_ext';
+import { ServerStatusParams } from './lsp_ext';
 
 export class Ctx {
     private constructor(
@@ -36,7 +36,7 @@ export class Ctx {
 
         res.pushCleanup(client.start());
         await client.onReady();
-        client.onNotification(ra.status, (params) => res.setStatus(params.status));
+        client.onNotification(ra.serverStatus, (params) => res.setServerStatus(params));
         return res;
     }
 
@@ -66,39 +66,28 @@ 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;
+    setServerStatus(status: ServerStatusParams) {
+        this.statusBar.tooltip = status.message ?? "Ready";
+        let icon = "";
+        switch (status.health) {
+            case "ok":
                 this.statusBar.color = undefined;
                 break;
-            case "readyPartial":
-                this.statusBar.text = "rust-analyzer";
-                this.statusBar.tooltip = "Ready (Partial)";
-                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";
+            case "warning":
+                this.statusBar.tooltip += "\nClick to reload.";
                 this.statusBar.command = "rust-analyzer.reloadWorkspace";
                 this.statusBar.color = new vscode.ThemeColor("notificationsWarningIcon.foreground");
+                icon = "$(warning) ";
+                break;
+            case "error":
+                this.statusBar.tooltip += "\nClick to reload.";
+                this.statusBar.command = "rust-analyzer.reloadWorkspace";
+                this.statusBar.color = new vscode.ThemeColor("notificationsErrorIcon.foreground");
+                icon = "$(error) ";
                 break;
         }
+        if (!status.quiescent) icon = "$(sync~spin) ";
+        this.statusBar.text = `${icon} rust-analyzer`;
     }
 
     pushCleanup(d: Disposable) {
diff --git a/editors/code/src/lsp_ext.ts b/editors/code/src/lsp_ext.ts
index 00e128b8c3..e453bb9e0b 100644
--- a/editors/code/src/lsp_ext.ts
+++ b/editors/code/src/lsp_ext.ts
@@ -10,11 +10,12 @@ export interface AnalyzerStatusParams {
 export const analyzerStatus = new lc.RequestType<AnalyzerStatusParams, string, void>("rust-analyzer/analyzerStatus");
 export const memoryUsage = new lc.RequestType0<string, void>("rust-analyzer/memoryUsage");
 
-export type Status = "loading" | "ready" | "readyPartial" | "invalid" | "needsReload";
-export interface StatusParams {
-    status: Status;
+export interface ServerStatusParams {
+    health: "ok" | "warning" | "error";
+    quiescent: boolean;
+    message?: string;
 }
-export const status = new lc.NotificationType<StatusParams>("rust-analyzer/status");
+export const serverStatus = new lc.NotificationType<ServerStatusParams>("experimental/serverStatus");
 
 export const reloadWorkspace = new lc.RequestType0<null, void>("rust-analyzer/reloadWorkspace");
 


Test patch:
diff --git a/crates/rust-analyzer/tests/rust-analyzer/support.rs b/crates/rust-analyzer/tests/rust-analyzer/support.rs
index 95bf26f016..8d68f1b7d5 100644
--- a/crates/rust-analyzer/tests/rust-analyzer/support.rs
+++ b/crates/rust-analyzer/tests/rust-analyzer/support.rs
@@ -103,7 +103,7 @@ impl<'a> Project<'a> {
                     ..Default::default()
                 }),
                 experimental: Some(json!({
-                    "statusNotification": true,
+                    "serverStatusNotification": true,
                 })),
                 ..Default::default()
             },
@@ -213,13 +213,12 @@ impl Server {
     }
     pub(crate) fn wait_until_workspace_is_loaded(self) -> Server {
         self.wait_for_message_cond(1, &|msg: &Message| match msg {
-            Message::Notification(n) if n.method == "rust-analyzer/status" => {
+            Message::Notification(n) if n.method == "experimental/serverStatus" => {
                 let status = n
                     .clone()
-                    .extract::<lsp_ext::StatusParams>("rust-analyzer/status")
-                    .unwrap()
-                    .status;
-                matches!(status, lsp_ext::Status::Ready)
+                    .extract::<lsp_ext::ServerStatusParams>("experimental/serverStatus")
+                    .unwrap();
+                status.quiescent
             }
             _ => false,
         })