diff --git a/crates/project_model/src/build_data.rs b/crates/project_model/src/build_data.rs index f7050be4e7f..c2c87b207b1 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, } -#[derive(Debug, Default, PartialEq, Eq)] +#[derive(Debug, Default, PartialEq, Eq, Clone)] pub struct BuildDataResult { data: FxHashMap, } diff --git a/crates/rust-analyzer/src/config.rs b/crates/rust-analyzer/src/config.rs index cda272fd48a..e012b4452fa 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 8679c859961..adeb7a97e85 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 { pub(crate) handle: H, @@ -67,26 +54,36 @@ pub(crate) struct GlobalState { req_queue: ReqQueue, pub(crate) task_pool: Handle, Receiver>, pub(crate) loader: Handle, Receiver>, - pub(crate) vfs_config_version: u32, - pub(crate) flycheck: Vec, - pub(crate) flycheck_sender: Sender, - pub(crate) flycheck_receiver: Receiver, pub(crate) config: Arc, pub(crate) analysis_host: AnalysisHost, pub(crate) diagnostics: DiagnosticCollection, pub(crate) mem_docs: FxHashMap, pub(crate) semantic_tokens_cache: Arc>>, - pub(crate) vfs: Arc)>>, pub(crate) shutdown_requested: bool, - pub(crate) status: Status, + pub(crate) last_reported_status: Option, pub(crate) source_root_config: SourceRootConfig, pub(crate) proc_macro_client: Option, - pub(crate) workspaces: Arc>, - pub(crate) fetch_workspaces_queue: OpQueue<(), ()>, + pub(crate) flycheck: Vec, + pub(crate) flycheck_sender: Sender, + pub(crate) flycheck_receiver: Receiver, + pub(crate) vfs: Arc)>>, + 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>, + pub(crate) fetch_workspaces_queue: OpQueue<(), Vec>>, pub(crate) workspace_build_data: Option, - pub(crate) fetch_build_data_queue: OpQueue, + pub(crate) fetch_build_data_queue: + OpQueue>>, latest_requests: Arc>, } @@ -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 0e1fec2095c..81a6f22f16c 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, } -pub enum StatusNotification {} +pub enum ServerStatusNotification {} -#[derive(Serialize, Deserialize)] +impl Notification for ServerStatusNotification { + type Params = ServerStatusParams; + const METHOD: &'static str = "experimental/serverStatus"; +} + +#[derive(Deserialize, Serialize, PartialEq, Eq, Clone)] +pub struct ServerStatusParams { + pub health: Health, + pub quiescent: bool, + pub message: Option, +} + +#[derive(Serialize, Deserialize, Clone, Copy, PartialEq, Eq)] #[serde(rename_all = "camelCase")] -pub enum Status { - Loading, - ReadyPartial, - Ready, - NeedsReload, - Invalid, -} - -#[derive(Deserialize, Serialize)] -pub struct StatusParams { - pub status: Status, -} - -impl Notification for StatusNotification { - type Params = StatusParams; - const METHOD: &'static str = "rust-analyzer/status"; +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 e88f16cc11d..a5655116b49 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 (from() guarantees lossless conversion) @@ -477,7 +490,11 @@ impl GlobalState { } RequestDispatcher { req: Some(req), global_state: self } - .on_sync::(|s, ()| Ok(s.fetch_workspaces_request()))? + .on_sync::(|s, ()| { + s.fetch_workspaces_request(); + s.fetch_workspaces_if_needed(); + Ok(()) + })? .on_sync::(|s, p| handlers::handle_join_lines(s.snapshot(), p))? .on_sync::(|s, p| handlers::handle_on_enter(s.snapshot(), p))? .on_sync::(|s, ()| { diff --git a/crates/rust-analyzer/src/op_queue.rs b/crates/rust-analyzer/src/op_queue.rs index f71b718bc08..1d612a93376 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 { - op_scheduled: Option, + op_requested: Option, op_in_progress: bool, last_op_result: Output, } impl Default for OpQueue { 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 OpQueue { 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 { 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 OpQueue { 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 d12f891bbce..301c7003b99 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::(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::(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 switch_workspaces( + pub(crate) fn fetch_workspaces_completed( &mut self, workspaces: Vec>, - workspace_build_data: Option>, ) { + self.fetch_workspaces_queue.op_completed(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, + build_data: anyhow::Result, + ) { + self.fetch_build_data_queue.op_completed(Some(build_data)) + } + + pub(crate) fn switch_workspaces(&mut self) { let _p = profile::span("GlobalState::switch_workspaces"); - log::info!("will switch workspaces: {:?}", workspaces); + log::info!("will switch workspaces"); - 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::>(); - - 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::>(); + + 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 { + 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/crates/rust-analyzer/tests/rust-analyzer/support.rs b/crates/rust-analyzer/tests/rust-analyzer/support.rs index 95bf26f0165..8d68f1b7d53 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::("rust-analyzer/status") - .unwrap() - .status; - matches!(status, lsp_ext::Status::Ready) + .extract::("experimental/serverStatus") + .unwrap(); + status.quiescent } _ => false, }) diff --git a/docs/dev/lsp-extensions.md b/docs/dev/lsp-extensions.md index 73be59a82e0..989771ac6ff 100644 --- a/docs/dev/lsp-extensions.md +++ b/docs/dev/lsp-extensions.md @@ -1,5 +1,5 @@