fix: Allow multiple discover operations

Previously, rust-analyzer would drop discover requests that arrived
before we'd finished processing the previous request.

Fix this by allowing multiple discover requests to be active. Keep
track of the number of discover operations for the quiescence check,
and keep the process handles until they terminate.
This commit is contained in:
Wilfred Hughes 2025-11-28 15:18:15 +00:00
parent ad61e76c58
commit 39cd4a1da1
5 changed files with 69 additions and 30 deletions

View file

@ -197,4 +197,22 @@ impl<T: Sized + Send + 'static> CommandHandle<T> {
)))
}
}
pub(crate) fn has_exited(&mut self) -> bool {
match self.child.0.try_wait() {
Ok(Some(_exit_code)) => {
// We have an exit code.
true
}
Ok(None) => {
// Hasn't exited yet.
false
}
Err(_) => {
// Couldn't get an exit code. Assume that we've
// exited.
true
}
}
}
}

View file

@ -67,7 +67,7 @@ impl DiscoverCommand {
cmd.args(args);
Ok(DiscoverHandle {
_handle: CommandHandle::spawn(cmd, DiscoverProjectParser, self.sender.clone(), None)?,
handle: CommandHandle::spawn(cmd, DiscoverProjectParser, self.sender.clone(), None)?,
span: info_span!("discover_command").entered(),
})
}
@ -76,7 +76,7 @@ impl DiscoverCommand {
/// A handle to a spawned [Discover].
#[derive(Debug)]
pub(crate) struct DiscoverHandle {
_handle: CommandHandle<DiscoverProjectMessage>,
pub(crate) handle: CommandHandle<DiscoverProjectMessage>,
#[allow(dead_code)] // not accessed, but used to log on drop.
span: EnteredSpan,
}

View file

@ -121,9 +121,10 @@ pub(crate) struct GlobalState {
pub(crate) test_run_remaining_jobs: usize,
// Project loading
pub(crate) discover_handle: Option<discover::DiscoverHandle>,
pub(crate) discover_handles: Vec<discover::DiscoverHandle>,
pub(crate) discover_sender: Sender<discover::DiscoverProjectMessage>,
pub(crate) discover_receiver: Receiver<discover::DiscoverProjectMessage>,
pub(crate) discover_jobs_active: u32,
// Debouncing channel for fetching the workspace
// we want to delay it until the VFS looks stable-ish (and thus is not currently in the middle
@ -175,7 +176,6 @@ pub(crate) struct GlobalState {
pub(crate) fetch_build_data_queue: OpQueue<(), FetchBuildDataResponse>,
pub(crate) fetch_proc_macros_queue: OpQueue<(ChangeWithProcMacros, Vec<ProcMacroPaths>), bool>,
pub(crate) prime_caches_queue: OpQueue,
pub(crate) discover_workspace_queue: OpQueue,
/// A deferred task queue.
///
@ -291,9 +291,10 @@ impl GlobalState {
test_run_receiver,
test_run_remaining_jobs: 0,
discover_handle: None,
discover_handles: vec![],
discover_sender,
discover_receiver,
discover_jobs_active: 0,
fetch_ws_receiver: None,
@ -312,7 +313,6 @@ impl GlobalState {
fetch_proc_macros_queue: OpQueue::default(),
prime_caches_queue: OpQueue::default(),
discover_workspace_queue: OpQueue::default(),
deferred_task_queue: task_queue,
incomplete_crate_graph: false,

View file

@ -531,6 +531,8 @@ impl GlobalState {
}
}
self.cleanup_discover_handles();
if let Some(diagnostic_changes) = self.diagnostics.take_changes() {
for file_id in diagnostic_changes {
let uri = file_id_to_url(&self.vfs.read().0, file_id);
@ -806,33 +808,34 @@ impl GlobalState {
self.report_progress("Fetching", state, msg, None, None);
}
Task::DiscoverLinkedProjects(arg) => {
if let Some(cfg) = self.config.discover_workspace_config()
&& !self.discover_workspace_queue.op_in_progress()
{
if let Some(cfg) = self.config.discover_workspace_config() {
// the clone is unfortunately necessary to avoid a borrowck error when
// `self.report_progress` is called later
let title = &cfg.progress_label.clone();
let command = cfg.command.clone();
let discover = DiscoverCommand::new(self.discover_sender.clone(), command);
self.report_progress(title, Progress::Begin, None, None, None);
self.discover_workspace_queue
.request_op("Discovering workspace".to_owned(), ());
let _ = self.discover_workspace_queue.should_start_op();
if self.discover_jobs_active == 0 {
self.report_progress(title, Progress::Begin, None, None, None);
}
self.discover_jobs_active += 1;
let arg = match arg {
DiscoverProjectParam::Buildfile(it) => DiscoverArgument::Buildfile(it),
DiscoverProjectParam::Path(it) => DiscoverArgument::Path(it),
};
let handle = discover.spawn(
arg,
&std::env::current_dir()
.expect("Failed to get cwd during project discovery"),
);
self.discover_handle = Some(handle.unwrap_or_else(|e| {
panic!("Failed to spawn project discovery command: {e}")
}));
let handle = discover
.spawn(
arg,
&std::env::current_dir()
.expect("Failed to get cwd during project discovery"),
)
.unwrap_or_else(|e| {
panic!("Failed to spawn project discovery command: {e}")
});
self.discover_handles.push(handle);
}
}
Task::FetchBuildData(progress) => {
@ -1036,27 +1039,45 @@ impl GlobalState {
.expect("No title could be found; this is a bug");
match message {
DiscoverProjectMessage::Finished { project, buildfile } => {
self.discover_handle = None;
self.report_progress(&title, Progress::End, None, None, None);
self.discover_workspace_queue.op_completed(());
self.discover_jobs_active = self.discover_jobs_active.saturating_sub(1);
if self.discover_jobs_active == 0 {
self.report_progress(&title, Progress::End, None, None, None);
}
let mut config = Config::clone(&*self.config);
config.add_discovered_project_from_command(project, buildfile);
self.update_configuration(config);
}
DiscoverProjectMessage::Progress { message } => {
self.report_progress(&title, Progress::Report, Some(message), None, None)
if self.discover_jobs_active > 0 {
self.report_progress(&title, Progress::Report, Some(message), None, None)
}
}
DiscoverProjectMessage::Error { error, source } => {
self.discover_handle = None;
let message = format!("Project discovery failed: {error}");
self.discover_workspace_queue.op_completed(());
self.show_and_log_error(message.clone(), source);
self.report_progress(&title, Progress::End, Some(message), None, None)
self.discover_jobs_active = self.discover_jobs_active.saturating_sub(1);
if self.discover_jobs_active == 0 {
self.report_progress(&title, Progress::End, Some(message), None, None)
}
}
}
}
/// Drop any discover command processes that have exited, due to
/// finishing or erroring.
fn cleanup_discover_handles(&mut self) {
let mut active_handles = vec![];
for mut discover_handle in self.discover_handles.drain(..) {
if !discover_handle.handle.has_exited() {
active_handles.push(discover_handle);
}
}
self.discover_handles = active_handles;
}
fn handle_cargo_test_msg(&mut self, message: CargoTestMessage) {
match message.output {
CargoTestOutput::Test { name, state } => {

View file

@ -74,7 +74,7 @@ impl GlobalState {
&& !self.fetch_workspaces_queue.op_in_progress()
&& !self.fetch_build_data_queue.op_in_progress()
&& !self.fetch_proc_macros_queue.op_in_progress()
&& !self.discover_workspace_queue.op_in_progress()
&& self.discover_jobs_active == 0
&& self.vfs_progress_config_version >= self.vfs_config_version
}
@ -297,7 +297,7 @@ impl GlobalState {
.collect();
let cargo_config = self.config.cargo(None);
let discover_command = self.config.discover_workspace_config().cloned();
let is_quiescent = !(self.discover_workspace_queue.op_in_progress()
let is_quiescent = !(self.discover_jobs_active > 0
|| self.vfs_progress_config_version < self.vfs_config_version
|| !self.vfs_done);