From f28056fd009f3566af8d71aeb5973e5c22c129da Mon Sep 17 00:00:00 2001 From: Wilfred Hughes Date: Thu, 19 Feb 2026 14:17:31 +0000 Subject: [PATCH] fix: Use the correct project root when there are multiple workspaces Previously, Config::root_path() would always return the LSP rootUri of the first workspace folder. This can cause issues when the user has multiple workspaces open in their editor, especially if the first one in the list isn't a Rust project. This was noted as an issue in rust-lang/rust-analyzer#21483, and added comments suggesting that we should deprecate root_path(). This change splits root_path() into a `workspace_root_for()` function that handles the multiple workspace case correctly, and a `default_root_path()` fallback. This is particularly useful when the user has configured project-relative paths to e.g. their discover command or rustfmt, but it's the correct behaviour in general. AI disclosure: First draft was written with Claude Opus. --- .../crates/rust-analyzer/src/config.rs | 30 +++++++++++++++---- .../rust-analyzer/src/handlers/request.rs | 9 +++++- .../crates/rust-analyzer/src/main_loop.rs | 11 +++++-- .../crates/rust-analyzer/src/reload.rs | 4 +-- 4 files changed, 44 insertions(+), 10 deletions(-) diff --git a/src/tools/rust-analyzer/crates/rust-analyzer/src/config.rs b/src/tools/rust-analyzer/crates/rust-analyzer/src/config.rs index 1bc164b157a7..90857a307310 100644 --- a/src/tools/rust-analyzer/crates/rust-analyzer/src/config.rs +++ b/src/tools/rust-analyzer/crates/rust-analyzer/src/config.rs @@ -1070,6 +1070,7 @@ struct ClientInfo { version: Option, } +/// The configuration of this rust-analyzer instance. #[derive(Clone)] pub struct Config { /// Projects that have a Cargo.toml or a rust-project.json in a @@ -1079,11 +1080,16 @@ pub struct Config { /// Projects whose configuration was generated by a command /// configured in discoverConfig. discovered_projects_from_command: Vec, - /// The workspace roots as registered by the LSP client + /// The workspace roots as registered by the LSP client. workspace_roots: Vec, caps: ClientCapabilities, - /// The LSP root path, deprecated in favor of `workspace_roots` + + /// The root of the first project encountered. This is deprecated + /// because rust-analyzer might be handling multiple projects. + /// + /// Prefer `workspace_roots` and `workspace_root_for()`. root_path: AbsPathBuf, + snippets: Vec, client_info: Option, @@ -1787,9 +1793,23 @@ fn sort_objects_by_field(json: &mut serde_json::Value) { s } - pub fn root_path(&self) -> &AbsPathBuf { - // We should probably use `workspace_roots` here if set - &self.root_path + /// Find the workspace root that contains the given path, using the + /// longest prefix match. + pub fn workspace_root_for(&self, path: &AbsPath) -> &AbsPathBuf { + self.workspace_roots + .iter() + .filter(|root| path.starts_with(root.as_path())) + .max_by_key(|root| root.as_str().len()) + .unwrap_or(self.default_root_path()) + } + + /// Best-effort root path for the current project. + /// + /// Use `workspace_root_for` where possible, because + /// `default_root_path` may return the wrong path when a user has + /// multiple workspaces. + pub fn default_root_path(&self) -> &AbsPathBuf { + self.workspace_roots.first().unwrap_or(&self.root_path) } pub fn caps(&self) -> &ClientCapabilities { diff --git a/src/tools/rust-analyzer/crates/rust-analyzer/src/handlers/request.rs b/src/tools/rust-analyzer/crates/rust-analyzer/src/handlers/request.rs index c24591b7ab75..9c2e0a5f321b 100644 --- a/src/tools/rust-analyzer/crates/rust-analyzer/src/handlers/request.rs +++ b/src/tools/rust-analyzer/crates/rust-analyzer/src/handlers/request.rs @@ -2453,7 +2453,14 @@ fn run_rustfmt( let cmd_path = if command.contains(std::path::MAIN_SEPARATOR) || (cfg!(windows) && command.contains('/')) { - snap.config.root_path().join(cmd).into() + let project_root = Utf8PathBuf::from_path_buf(current_dir.clone()) + .ok() + .and_then(|p| AbsPathBuf::try_from(p).ok()); + let project_root = project_root + .as_ref() + .map(|dir| snap.config.workspace_root_for(dir)) + .unwrap_or(snap.config.default_root_path()); + project_root.join(cmd).into() } else { cmd }; diff --git a/src/tools/rust-analyzer/crates/rust-analyzer/src/main_loop.rs b/src/tools/rust-analyzer/crates/rust-analyzer/src/main_loop.rs index 7c494de6f73d..a8c3d062d041 100644 --- a/src/tools/rust-analyzer/crates/rust-analyzer/src/main_loop.rs +++ b/src/tools/rust-analyzer/crates/rust-analyzer/src/main_loop.rs @@ -830,12 +830,19 @@ fn handle_task(&mut self, prime_caches_progress: &mut Vec, let command = cfg.command.clone(); let discover = DiscoverCommand::new(self.discover_sender.clone(), command); + let discover_path = match &arg { + DiscoverProjectParam::Buildfile(it) => it, + DiscoverProjectParam::Path(it) => it, + }; + let current_dir = + self.config.workspace_root_for(discover_path.as_path()).clone(); + let arg = match arg { DiscoverProjectParam::Buildfile(it) => DiscoverArgument::Buildfile(it), DiscoverProjectParam::Path(it) => DiscoverArgument::Path(it), }; - match discover.spawn(arg, self.config.root_path().as_ref()) { + match discover.spawn(arg, current_dir.as_ref()) { Ok(handle) => { if self.discover_jobs_active == 0 { let title = &cfg.progress_label.clone(); @@ -953,7 +960,7 @@ fn handle_vfs_msg( if let Some(dir) = dir { message += &format!( ": {}", - match dir.strip_prefix(self.config.root_path()) { + match dir.strip_prefix(self.config.workspace_root_for(&dir)) { Some(relative_path) => relative_path.as_utf8_path(), None => dir.as_ref(), } diff --git a/src/tools/rust-analyzer/crates/rust-analyzer/src/reload.rs b/src/tools/rust-analyzer/crates/rust-analyzer/src/reload.rs index 83f4a19b39fa..71accbed4ef1 100644 --- a/src/tools/rust-analyzer/crates/rust-analyzer/src/reload.rs +++ b/src/tools/rust-analyzer/crates/rust-analyzer/src/reload.rs @@ -390,7 +390,7 @@ pub(crate) fn fetch_build_data(&mut self, cause: Cause) { info!(%cause, "will fetch build data"); let workspaces = Arc::clone(&self.workspaces); let config = self.config.cargo(None); - let root_path = self.config.root_path().clone(); + let root_path = self.config.default_root_path().clone(); self.task_pool.handle.spawn_with_sender(ThreadIntent::Worker, move |sender| { sender.send(Task::FetchBuildData(BuildDataProgress::Begin)).unwrap(); @@ -883,7 +883,7 @@ fn reload_flycheck(&mut self) { config, crate::flycheck::FlycheckConfigJson::default(), None, - self.config.root_path().clone(), + self.config.default_root_path().clone(), None, None, )]