From db69230737842b7178b02eb7aace50b8e2e76cc0 Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Fri, 13 Dec 2024 18:03:57 +0100 Subject: [PATCH 01/63] Checkpoint --- crates/lsp/src/lsp.rs | 1 + crates/lsp/src/server_tree.rs | 89 +++++++++++++++++++++++++++++++++++ 2 files changed, 90 insertions(+) create mode 100644 crates/lsp/src/server_tree.rs diff --git a/crates/lsp/src/lsp.rs b/crates/lsp/src/lsp.rs index 65ba61f626b5e..012ed044ac2bb 100644 --- a/crates/lsp/src/lsp.rs +++ b/crates/lsp/src/lsp.rs @@ -1,4 +1,5 @@ mod input_handler; +mod server_tree; pub use lsp_types::request::*; pub use lsp_types::*; diff --git a/crates/lsp/src/server_tree.rs b/crates/lsp/src/server_tree.rs new file mode 100644 index 0000000000000..1e835b609b565 --- /dev/null +++ b/crates/lsp/src/server_tree.rs @@ -0,0 +1,89 @@ +//! This module defines an LSP Tree. +//! +//! An LSP Tree is responsible for determining which language servers apply to a given project path. +//! +//! ## RPC +//! LSP Tree is transparent to RPC peers; when clients ask host to spawn a new language server, the host will perform LSP Tree lookup for provided path; it may decide +//! to reuse existing language server. The client maintains it's own LSP Tree that is a subset of host LSP Tree. Done this way, the client does not need to +//! ask about suitable language server for each path it interacts with; it can resolve most of the queries locally. + +use std::{collections::BTreeMap, path::Path, sync::Arc}; + +use collections::HashMap; +use lsp_types::Url; + +use crate::LanguageServerId; + +pub trait LspRootFinder { + fn find_root(&self) -> () {} +} + +enum Action { + PinpointTo, + ExtendWorkspaceFolders(LanguageServerId, Url), +} + +pub type LanguageName = Arc; + +pub type AbsWorkspaceRootPath = Arc; + +#[derive(Default)] +pub struct LspTree { + /// Language servers for which we can just update workspaceFolders when we detect a new project root + umbrella_language_servers: HashMap>, + pinpoint_language_servers: + HashMap>>, +} + +impl LspTree { + fn new() -> Self { + Self::default() + } + pub fn insert_new_server( + &mut self, + name: LanguageName, + id: LanguageServerId, + root_path: Option, + ) { + if let Some(root_path) = root_path { + self.pinpoint_language_servers + .entry(name) + .or_default() + .entry(root_path) + .or_default() + .push(id); + } else { + self.umbrella_language_servers + .entry(name) + .or_default() + .push(id); + } + } + pub fn get<'a, 'b>( + &'a mut self, + language: LanguageName, + file: &'b Path, + ) -> impl Iterator + 'b + where + 'a: 'b, + { + self.pinpoint_language_servers + .get(&language) + .into_iter() + .flat_map(move |joint| { + file.ancestors().flat_map(move |ancestor| { + joint.get(ancestor).map(|servers| servers.iter().cloned()) + }) + }) + .flatten() + .chain( + self.umbrella_language_servers + .get(&language) + .into_iter() + .flat_map(|servers| servers.iter().cloned()), + ) + } +} + +#[cfg(test)] +mod tests {} From 5db0092d8b06a21a34c963f3a73618438071cac3 Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Sat, 14 Dec 2024 15:11:06 +0100 Subject: [PATCH 02/63] WIP, move ProjectTree into project --- crates/language/src/language.rs | 7 ++ crates/lsp/src/lsp.rs | 1 - crates/project/src/project.rs | 1 + crates/project/src/project_tree.rs | 82 +++++++++++++++++++ .../src/project_tree}/server_tree.rs | 6 +- .../src/project_tree/toolchain_tree.rs | 0 6 files changed, 93 insertions(+), 4 deletions(-) create mode 100644 crates/project/src/project_tree.rs rename crates/{lsp/src => project/src/project_tree}/server_tree.rs (96%) create mode 100644 crates/project/src/project_tree/toolchain_tree.rs diff --git a/crates/language/src/language.rs b/crates/language/src/language.rs index e0cd392131105..a5319a834b74b 100644 --- a/crates/language/src/language.rs +++ b/crates/language/src/language.rs @@ -259,6 +259,13 @@ impl CachedLspAdapter { .cloned() .unwrap_or_else(|| language_name.lsp_id()) } + pub fn find_closest_project_root( + &self, + worktree_id: WorktreeId, + path: Arc, + ) -> Option> { + None + } } /// [`LspAdapterDelegate`] allows [`LspAdapter]` implementations to interface with the application diff --git a/crates/lsp/src/lsp.rs b/crates/lsp/src/lsp.rs index 012ed044ac2bb..65ba61f626b5e 100644 --- a/crates/lsp/src/lsp.rs +++ b/crates/lsp/src/lsp.rs @@ -1,5 +1,4 @@ mod input_handler; -mod server_tree; pub use lsp_types::request::*; pub use lsp_types::*; diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index f00d53544dbbe..7764077fb3648 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -8,6 +8,7 @@ pub mod lsp_ext_command; pub mod lsp_store; pub mod prettier_store; pub mod project_settings; +mod project_tree; pub mod search; mod task_inventory; pub mod task_store; diff --git a/crates/project/src/project_tree.rs b/crates/project/src/project_tree.rs new file mode 100644 index 0000000000000..7c8f7115bd2ea --- /dev/null +++ b/crates/project/src/project_tree.rs @@ -0,0 +1,82 @@ +//! This module defines a Project Tree. +//! +//! A Project Tree is responsible for determining where the roots of subprojects are located in a project. + +mod server_tree; +mod toolchain_tree; + +use std::{ + collections::{BTreeMap, BTreeSet}, + path::Path, + sync::Arc, +}; + +use collections::HashMap; +use language::{LanguageName, LanguageRegistry}; +use lsp::{LanguageServerName, Url}; +use settings::WorktreeId; + +use crate::{LanguageServerId, ProjectPath}; + +pub trait LspRootFinder { + fn find_root(&self) -> () {} +} + +enum Action { + PinpointTo, + ExtendWorkspaceFolders(LanguageServerId, Url), +} + +pub type AbsWorkspaceRootPath = Arc; + +pub struct ProjectTree { + languages: Arc, + root_points: HashMap, IsRoot>>>, +} + +type IsRoot = bool; +impl ProjectTree { + fn new(languages: Arc) -> Self { + Self { + languages, + root_points: Default::default(), + } + } + fn root_for_path( + &mut self, + ProjectPath { worktree_id, path }: ProjectPath, + language_name: &LanguageName, + ) -> Vec { + let mut roots = vec![]; + let adapters = self.languages.lsp_adapters(&language_name); + let worktree_roots = self.root_points.entry(worktree_id).or_default(); + + 'adapter: for adapter in adapters { + if let Some(adapter_roots) = worktree_roots.get(&adapter.name()) { + for ancestor in path.ancestors().skip(1) { + // Look at roots detected so far. We should be executing this branch most of the time. + // Todo: only search up until worktree root. Or should we? What about anonymous worktrees created for RA when browsing std? + // they're rooted at the file being browsed. + if let Some(is_root) = adapter_roots.get(ancestor) { + if *is_root { + roots.push((worktree_id, ancestor).into()); + } + continue 'adapter; + } + } + } + // Ask adapter what the closest root is. + let root = adapter.find_closest_project_root(worktree_id, path.clone()); + let is_known_root = root.is_some(); + worktree_roots + .entry(adapter.name()) + .or_default() + .entry(root.clone().unwrap_or_else(|| Arc::from(Path::new("")))) + .or_insert(is_known_root); + if let Some(root) = root { + roots.push((worktree_id, root).into()); + } + } + roots + } +} diff --git a/crates/lsp/src/server_tree.rs b/crates/project/src/project_tree/server_tree.rs similarity index 96% rename from crates/lsp/src/server_tree.rs rename to crates/project/src/project_tree/server_tree.rs index 1e835b609b565..38aa2e014bbcc 100644 --- a/crates/lsp/src/server_tree.rs +++ b/crates/project/src/project_tree/server_tree.rs @@ -6,11 +6,13 @@ //! LSP Tree is transparent to RPC peers; when clients ask host to spawn a new language server, the host will perform LSP Tree lookup for provided path; it may decide //! to reuse existing language server. The client maintains it's own LSP Tree that is a subset of host LSP Tree. Done this way, the client does not need to //! ask about suitable language server for each path it interacts with; it can resolve most of the queries locally. +//! This module defines a Project Tree. use std::{collections::BTreeMap, path::Path, sync::Arc}; use collections::HashMap; -use lsp_types::Url; +use language::LanguageName; +use lsp::Url; use crate::LanguageServerId; @@ -23,8 +25,6 @@ enum Action { ExtendWorkspaceFolders(LanguageServerId, Url), } -pub type LanguageName = Arc; - pub type AbsWorkspaceRootPath = Arc; #[derive(Default)] diff --git a/crates/project/src/project_tree/toolchain_tree.rs b/crates/project/src/project_tree/toolchain_tree.rs new file mode 100644 index 0000000000000..e69de29bb2d1d From bf8bbdcd8dd0e0fbe9488824495fa040d4fcfe7e Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Mon, 16 Dec 2024 14:29:51 +0100 Subject: [PATCH 03/63] Checkpoint --- crates/language/src/language.rs | 3 +- crates/project/src/lsp_store.rs | 70 +++++++++++++++++------------- crates/project/src/project_tree.rs | 51 +++++++++++++--------- 3 files changed, 72 insertions(+), 52 deletions(-) diff --git a/crates/language/src/language.rs b/crates/language/src/language.rs index a5319a834b74b..9a0edbb289a63 100644 --- a/crates/language/src/language.rs +++ b/crates/language/src/language.rs @@ -264,7 +264,8 @@ impl CachedLspAdapter { worktree_id: WorktreeId, path: Arc, ) -> Option> { - None + // By default we'll root all projects at the root of a worktree. + Some(Arc::from("".as_ref())) } } diff --git a/crates/project/src/lsp_store.rs b/crates/project/src/lsp_store.rs index 471376c0e1c25..f59f8b7ebafbd 100644 --- a/crates/project/src/lsp_store.rs +++ b/crates/project/src/lsp_store.rs @@ -2477,6 +2477,42 @@ impl LocalLspStore { failure_reason: None, }) } + + fn remove_worktree( + &mut self, + id_to_remove: WorktreeId, + cx: &mut ModelContext<'_, LspStore>, + ) -> Vec { + self.diagnostics.remove(&id_to_remove); + self.prettier_store.update(cx, |prettier_store, cx| { + prettier_store.remove_worktree(id_to_remove, cx); + }); + + let mut servers_to_remove = BTreeMap::default(); + let mut servers_to_preserve = HashSet::default(); + for ((worktree_id, server_name), &server_id) in &self.language_server_ids { + if worktree_id == &id_to_remove { + servers_to_remove.insert(server_id, server_name.clone()); + } else { + servers_to_preserve.insert(server_id); + } + } + servers_to_remove.retain(|server_id, _| !servers_to_preserve.contains(server_id)); + for (server_id_to_remove, server_name) in &servers_to_remove { + self.language_server_ids + .remove(&(id_to_remove, server_name.clone())); + self.language_server_watched_paths + .remove(&server_id_to_remove); + self.language_server_paths_watched_for_rename + .remove(&server_id_to_remove); + self.last_workspace_edits_by_language_server + .remove(&server_id_to_remove); + self.language_servers.remove(&server_id_to_remove); + cx.emit(LspStoreEvent::LanguageServerRemoved(*server_id_to_remove)); + } + servers_to_remove.into_keys().collect() + } + fn rebuild_watched_paths_inner<'a>( &'a self, language_server_id: LanguageServerId, @@ -5163,39 +5199,11 @@ impl LspStore { fn remove_worktree(&mut self, id_to_remove: WorktreeId, cx: &mut ModelContext) { self.diagnostic_summaries.remove(&id_to_remove); - let to_remove = Vec::new(); if let Some(local) = self.as_local_mut() { - local.diagnostics.remove(&id_to_remove); - local.prettier_store.update(cx, |prettier_store, cx| { - prettier_store.remove_worktree(id_to_remove, cx); - }); - - let mut servers_to_remove = HashMap::default(); - let mut servers_to_preserve = HashSet::default(); - for ((worktree_id, server_name), &server_id) in &local.language_server_ids { - if worktree_id == &id_to_remove { - servers_to_remove.insert(server_id, server_name.clone()); - } else { - servers_to_preserve.insert(server_id); - } + let to_remove = local.remove_worktree(id_to_remove, cx); + for server in to_remove { + self.language_server_statuses.remove(&server); } - servers_to_remove.retain(|server_id, _| !servers_to_preserve.contains(server_id)); - for (server_id_to_remove, server_name) in servers_to_remove { - local - .language_server_ids - .remove(&(id_to_remove, server_name)); - local - .language_server_watched_paths - .remove(&server_id_to_remove); - local - .last_workspace_edits_by_language_server - .remove(&server_id_to_remove); - local.language_servers.remove(&server_id_to_remove); - cx.emit(LspStoreEvent::LanguageServerRemoved(server_id_to_remove)); - } - } - for server in to_remove { - self.language_server_statuses.remove(&server); } } diff --git a/crates/project/src/project_tree.rs b/crates/project/src/project_tree.rs index 7c8f7115bd2ea..6df358ee4070f 100644 --- a/crates/project/src/project_tree.rs +++ b/crates/project/src/project_tree.rs @@ -12,35 +12,36 @@ use std::{ }; use collections::HashMap; +use gpui::{AppContext, Context as _, Model, ModelContext, Subscription}; use language::{LanguageName, LanguageRegistry}; -use lsp::{LanguageServerName, Url}; +use lsp::LanguageServerName; use settings::WorktreeId; -use crate::{LanguageServerId, ProjectPath}; - -pub trait LspRootFinder { - fn find_root(&self) -> () {} -} - -enum Action { - PinpointTo, - ExtendWorkspaceFolders(LanguageServerId, Url), -} - -pub type AbsWorkspaceRootPath = Arc; +use crate::{ + worktree_store::{WorktreeStore, WorktreeStoreEvent}, + ProjectPath, +}; +type IsRoot = bool; pub struct ProjectTree { languages: Arc, root_points: HashMap, IsRoot>>>, + worktree_store: Model, + _subscriptions: [Subscription; 1], } -type IsRoot = bool; impl ProjectTree { - fn new(languages: Arc) -> Self { - Self { + fn new( + languages: Arc, + worktree_store: Model, + cx: &mut AppContext, + ) -> Model { + cx.new_model(|cx| Self { languages, root_points: Default::default(), - } + _subscriptions: [cx.subscribe(&worktree_store, Self::on_worktree_store_event)], + worktree_store, + }) } fn root_for_path( &mut self, @@ -54,9 +55,6 @@ impl ProjectTree { 'adapter: for adapter in adapters { if let Some(adapter_roots) = worktree_roots.get(&adapter.name()) { for ancestor in path.ancestors().skip(1) { - // Look at roots detected so far. We should be executing this branch most of the time. - // Todo: only search up until worktree root. Or should we? What about anonymous worktrees created for RA when browsing std? - // they're rooted at the file being browsed. if let Some(is_root) = adapter_roots.get(ancestor) { if *is_root { roots.push((worktree_id, ancestor).into()); @@ -79,4 +77,17 @@ impl ProjectTree { } roots } + fn on_worktree_store_event( + &mut self, + _: Model, + evt: &WorktreeStoreEvent, + _: &mut ModelContext, + ) { + match evt { + WorktreeStoreEvent::WorktreeRemoved(_, worktree_id) => { + self.root_points.remove(&worktree_id); + } + _ => {} + } + } } From c90753c05ee5d1d1f49c9d9d83e4b7092e73b7e0 Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Mon, 16 Dec 2024 18:13:12 +0100 Subject: [PATCH 04/63] Checkpoint --- crates/project/src/project_tree.rs | 121 +++++++++++++++--- .../project/src/project_tree/server_tree.rs | 9 -- 2 files changed, 102 insertions(+), 28 deletions(-) diff --git a/crates/project/src/project_tree.rs b/crates/project/src/project_tree.rs index 6df358ee4070f..5359425d68b43 100644 --- a/crates/project/src/project_tree.rs +++ b/crates/project/src/project_tree.rs @@ -6,7 +6,7 @@ mod server_tree; mod toolchain_tree; use std::{ - collections::{BTreeMap, BTreeSet}, + collections::{btree_map::Entry as TreeEntry, hash_map::Entry, BTreeMap}, path::Path, sync::Arc, }; @@ -16,6 +16,7 @@ use gpui::{AppContext, Context as _, Model, ModelContext, Subscription}; use language::{LanguageName, LanguageRegistry}; use lsp::LanguageServerName; use settings::WorktreeId; +use worktree::{Event as WorktreeEvent, Worktree}; use crate::{ worktree_store::{WorktreeStore, WorktreeStoreEvent}, @@ -23,9 +24,51 @@ use crate::{ }; type IsRoot = bool; + +struct WorktreeRoots { + roots: BTreeMap, BTreeMap>, + worktree_store: Model, + worktree_subscription: Subscription, +} + +impl WorktreeRoots { + fn new( + worktree_store: Model, + worktree: Model, + cx: &mut AppContext, + ) -> Model { + cx.new_model(|cx| Self { + roots: Default::default(), + worktree_store, + worktree_subscription: cx.subscribe(&worktree, |this: &mut Self, _, event, cx| { + match event { + WorktreeEvent::UpdatedEntries(changes) => { + for (path, _, kind) in changes.iter() { + match kind { + worktree::PathChange::Removed => { + this.roots.remove(path); + } + _ => {} + } + } + } + WorktreeEvent::UpdatedGitRepositories(_) => {} + WorktreeEvent::DeletedEntry(entry_id) => { + let Some(entry) = this.worktree_store.read(cx).entry_for_id(*entry_id, cx) + else { + return; + }; + this.roots.remove(&entry.path); + } + } + }), + }) + } +} + pub struct ProjectTree { languages: Arc, - root_points: HashMap, IsRoot>>>, + root_points: HashMap>, worktree_store: Model, _subscriptions: [Subscription; 1], } @@ -47,33 +90,73 @@ impl ProjectTree { &mut self, ProjectPath { worktree_id, path }: ProjectPath, language_name: &LanguageName, - ) -> Vec { - let mut roots = vec![]; + cx: &mut ModelContext, + ) -> BTreeMap { + let mut roots = BTreeMap::default(); let adapters = self.languages.lsp_adapters(&language_name); - let worktree_roots = self.root_points.entry(worktree_id).or_default(); + let worktree_roots = match self.root_points.entry(worktree_id) { + Entry::Occupied(occupied_entry) => occupied_entry.get_mut(), + Entry::Vacant(vacant_entry) => { + let Some(worktree) = self + .worktree_store + .read(cx) + .worktree_for_id(worktree_id, cx) + else { + return Default::default(); + }; + let roots = WorktreeRoots::new(self.worktree_store.clone(), worktree, cx); + vacant_entry.insert(roots) + } + }; - 'adapter: for adapter in adapters { - if let Some(adapter_roots) = worktree_roots.get(&adapter.name()) { + let mut filled_adapters = vec![false; adapters.len()]; + let mut adapters_with_roots = 0; + for ancestor in path.ancestors().skip(1) { + // TODO: scan up until worktree root and no further. + if adapters_with_roots == adapters.len() { + // We've found roots for all adapters, no need to continue + break; + } + if let Some(adapter_roots) = worktree_roots.read(cx).roots.get(ancestor) { + for (ix, adapter) in adapters.iter().enumerate() { + let adapter_already_found_root = filled_adapters[ix]; + if adapter_already_found_root { + continue; + } + + match adapter_roots.entry(adapter.name.clone()) { + TreeEntry::Vacant(vacant_entry) => { + let root = adapter.find_closest_project_root(worktree_id, path.clone()); + vacant_entry.insert(root.is_some()); + if let Some(root) = root { + roots.insert(adapter.name.clone(), (worktree_id, root).into()); + } + } + TreeEntry::Occupied(occupied_entry) => { + let is_root = *occupied_entry.get(); + if is_root { + roots.insert(adapter.name.clone(), (worktree_id, ancestor).into()); + } + + continue; + } + } + filled_adapters[ix] = true; + adapters_with_roots += 1; + } + } + } + 'adapter: for (ix, adapter) in adapters.iter().enumerate() { + if let Some(adapter_roots) = worktree_roots.read(cx).roots.get(&adapter.name()) { for ancestor in path.ancestors().skip(1) { if let Some(is_root) = adapter_roots.get(ancestor) { if *is_root { - roots.push((worktree_id, ancestor).into()); + roots.insert((worktree_id, ancestor).into()); } continue 'adapter; } } } - // Ask adapter what the closest root is. - let root = adapter.find_closest_project_root(worktree_id, path.clone()); - let is_known_root = root.is_some(); - worktree_roots - .entry(adapter.name()) - .or_default() - .entry(root.clone().unwrap_or_else(|| Arc::from(Path::new("")))) - .or_insert(is_known_root); - if let Some(root) = root { - roots.push((worktree_id, root).into()); - } } roots } diff --git a/crates/project/src/project_tree/server_tree.rs b/crates/project/src/project_tree/server_tree.rs index 38aa2e014bbcc..e831ed454b24e 100644 --- a/crates/project/src/project_tree/server_tree.rs +++ b/crates/project/src/project_tree/server_tree.rs @@ -16,15 +16,6 @@ use lsp::Url; use crate::LanguageServerId; -pub trait LspRootFinder { - fn find_root(&self) -> () {} -} - -enum Action { - PinpointTo, - ExtendWorkspaceFolders(LanguageServerId, Url), -} - pub type AbsWorkspaceRootPath = Arc; #[derive(Default)] From d55b9927873983d551919206fdfa5a82bfc09d18 Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Tue, 17 Dec 2024 17:06:44 +0100 Subject: [PATCH 05/63] Checkpoint --- crates/project/src/project_tree.rs | 68 ++++----- .../project/src/project_tree/server_tree.rs | 141 +++++++++++------- 2 files changed, 120 insertions(+), 89 deletions(-) diff --git a/crates/project/src/project_tree.rs b/crates/project/src/project_tree.rs index 5359425d68b43..5469f6b18f56a 100644 --- a/crates/project/src/project_tree.rs +++ b/crates/project/src/project_tree.rs @@ -90,12 +90,12 @@ impl ProjectTree { &mut self, ProjectPath { worktree_id, path }: ProjectPath, language_name: &LanguageName, - cx: &mut ModelContext, + cx: &mut AppContext, ) -> BTreeMap { let mut roots = BTreeMap::default(); let adapters = self.languages.lsp_adapters(&language_name); let worktree_roots = match self.root_points.entry(worktree_id) { - Entry::Occupied(occupied_entry) => occupied_entry.get_mut(), + Entry::Occupied(occupied_entry) => occupied_entry.get().clone(), Entry::Vacant(vacant_entry) => { let Some(worktree) = self .worktree_store @@ -105,7 +105,7 @@ impl ProjectTree { return Default::default(); }; let roots = WorktreeRoots::new(self.worktree_store.clone(), worktree, cx); - vacant_entry.insert(roots) + vacant_entry.insert(roots).clone() } }; @@ -117,46 +117,40 @@ impl ProjectTree { // We've found roots for all adapters, no need to continue break; } - if let Some(adapter_roots) = worktree_roots.read(cx).roots.get(ancestor) { - for (ix, adapter) in adapters.iter().enumerate() { - let adapter_already_found_root = filled_adapters[ix]; - if adapter_already_found_root { - continue; - } - - match adapter_roots.entry(adapter.name.clone()) { - TreeEntry::Vacant(vacant_entry) => { - let root = adapter.find_closest_project_root(worktree_id, path.clone()); - vacant_entry.insert(root.is_some()); - if let Some(root) = root { - roots.insert(adapter.name.clone(), (worktree_id, root).into()); - } + worktree_roots.update(cx, |this, _| { + if let Some(adapter_roots) = this.roots.get_mut(ancestor) { + for (ix, adapter) in adapters.iter().enumerate() { + let adapter_already_found_root = filled_adapters[ix]; + if adapter_already_found_root { + continue; } - TreeEntry::Occupied(occupied_entry) => { - let is_root = *occupied_entry.get(); - if is_root { - roots.insert(adapter.name.clone(), (worktree_id, ancestor).into()); + + match adapter_roots.entry(adapter.name.clone()) { + TreeEntry::Vacant(vacant_entry) => { + let root = + adapter.find_closest_project_root(worktree_id, path.clone()); + vacant_entry.insert(root.is_some()); + if let Some(root) = root { + roots.insert(adapter.name.clone(), (worktree_id, root).into()); + } } + TreeEntry::Occupied(occupied_entry) => { + let is_root = *occupied_entry.get(); + if is_root { + roots.insert( + adapter.name.clone(), + (worktree_id, ancestor).into(), + ); + } - continue; - } - } - filled_adapters[ix] = true; - adapters_with_roots += 1; - } - } - } - 'adapter: for (ix, adapter) in adapters.iter().enumerate() { - if let Some(adapter_roots) = worktree_roots.read(cx).roots.get(&adapter.name()) { - for ancestor in path.ancestors().skip(1) { - if let Some(is_root) = adapter_roots.get(ancestor) { - if *is_root { - roots.insert((worktree_id, ancestor).into()); + continue; + } } - continue 'adapter; + filled_adapters[ix] = true; + adapters_with_roots += 1; } } - } + }); } roots } diff --git a/crates/project/src/project_tree/server_tree.rs b/crates/project/src/project_tree/server_tree.rs index e831ed454b24e..eed1a28d35da7 100644 --- a/crates/project/src/project_tree/server_tree.rs +++ b/crates/project/src/project_tree/server_tree.rs @@ -8,71 +8,108 @@ //! ask about suitable language server for each path it interacts with; it can resolve most of the queries locally. //! This module defines a Project Tree. -use std::{collections::BTreeMap, path::Path, sync::Arc}; +use std::{ + collections::BTreeMap, + path::Path, + sync::{Arc, OnceLock}, +}; use collections::HashMap; -use language::LanguageName; -use lsp::Url; +use gpui::{AppContext, Context as _, Model}; +use language::{LanguageName, LanguageRegistry}; +use lsp::LanguageServerName; -use crate::LanguageServerId; +use crate::{LanguageServerId, ProjectPath}; + +use super::ProjectTree; pub type AbsWorkspaceRootPath = Arc; -#[derive(Default)] -pub struct LspTree { +pub struct LanguageServerTree { /// Language servers for which we can just update workspaceFolders when we detect a new project root - umbrella_language_servers: HashMap>, - pinpoint_language_servers: - HashMap>>, + project_tree: Model, + languages: Arc, + instances: HashMap>, + // shared_instances: BTreeMap>, + attach_kind_cache: HashMap, } -impl LspTree { - fn new() -> Self { - Self::default() - } - pub fn insert_new_server( - &mut self, - name: LanguageName, - id: LanguageServerId, - root_path: Option, - ) { - if let Some(root_path) = root_path { - self.pinpoint_language_servers - .entry(name) - .or_default() - .entry(root_path) - .or_default() - .push(id); - } else { - self.umbrella_language_servers - .entry(name) - .or_default() - .push(id); +#[derive(Clone, Copy, PartialEq)] +enum Attach { + /// Create a single language server instance per subproject root. + InstancePerRoot, + /// Use one shared language server instance for all subprojects within a project. + Shared, +} + +impl Attach { + fn root_path(&self, root_subproject_path: ProjectPath) -> ProjectPath { + match self { + Attach::InstancePerRoot => root_subproject_path, + Attach::Shared => ProjectPath { + worktree_id: root_subproject_path.worktree_id, + path: Arc::from(Path::new("")), + }, } } - pub fn get<'a, 'b>( +} + +#[derive(Clone)] +pub(crate) struct LanguageServerTreeNode(Arc); + +impl LanguageServerTreeNode { + fn new(attach: Attach) -> Self { + Self(Arc::new(InnerTreeNode { + id: Default::default(), + attach, + })) + } +} +struct InnerTreeNode { + id: OnceLock, + attach: Attach, +} + +impl LanguageServerTree { + fn new( + languages: Arc, + project_tree: Model, + cx: &mut AppContext, + ) -> Model { + cx.new_model(|_| Self { + project_tree, + languages, + instances: Default::default(), + attach_kind_cache: Default::default(), + }) + } + fn attach_kind(&mut self, name: LanguageServerName) -> Attach { + *self + .attach_kind_cache + .entry(name) + .or_insert_with(|| Attach::Shared) + // todo: query lspadapter for it. + } + + pub(crate) fn get<'a>( &'a mut self, + path: ProjectPath, language: LanguageName, - file: &'b Path, - ) -> impl Iterator + 'b - where - 'a: 'b, - { - self.pinpoint_language_servers - .get(&language) - .into_iter() - .flat_map(move |joint| { - file.ancestors().flat_map(move |ancestor| { - joint.get(ancestor).map(|servers| servers.iter().cloned()) - }) - }) - .flatten() - .chain( - self.umbrella_language_servers - .get(&language) - .into_iter() - .flat_map(|servers| servers.iter().cloned()), - ) + cx: &mut AppContext, + ) -> impl Iterator + 'a { + let roots = self + .project_tree + .update(cx, |this, cx| this.root_for_path(path, &language, cx)); + + roots.into_iter().map(|(adapter_name, root_path)| { + let attach = self.attach_kind(adapter_name.clone()); + self.instances + .entry(root_path) + .or_default() + .entry(adapter_name) + .or_insert_with(|| LanguageServerTreeNode::new(attach)) + .clone() + }) } } From 95a92114eea9fbe6ffb6e43080b2aee7ae748b73 Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Tue, 17 Dec 2024 18:30:41 +0100 Subject: [PATCH 06/63] Checkpoint --- crates/language/src/language.rs | 23 +++++++++++ crates/project/src/project_tree.rs | 33 ++++++++++++++-- .../project/src/project_tree/server_tree.rs | 38 +++++-------------- 3 files changed, 61 insertions(+), 33 deletions(-) diff --git a/crates/language/src/language.rs b/crates/language/src/language.rs index 9a0edbb289a63..b9b8506ec643a 100644 --- a/crates/language/src/language.rs +++ b/crates/language/src/language.rs @@ -267,6 +267,29 @@ impl CachedLspAdapter { // By default we'll root all projects at the root of a worktree. Some(Arc::from("".as_ref())) } + pub fn attach_kind(&self) -> Attach { + Attach::Shared + } +} + +#[derive(Clone, Copy, PartialEq)] +pub enum Attach { + /// Create a single language server instance per subproject root. + InstancePerRoot, + /// Use one shared language server instance for all subprojects within a project. + Shared, +} + +impl Attach { + pub fn root_path( + &self, + root_subproject_path: (WorktreeId, Arc), + ) -> (WorktreeId, Arc) { + match self { + Attach::InstancePerRoot => root_subproject_path, + Attach::Shared => (root_subproject_path.0, Arc::from(Path::new(""))), + } + } } /// [`LspAdapterDelegate`] allows [`LspAdapter]` implementations to interface with the application diff --git a/crates/project/src/project_tree.rs b/crates/project/src/project_tree.rs index 5469f6b18f56a..dba02b706a142 100644 --- a/crates/project/src/project_tree.rs +++ b/crates/project/src/project_tree.rs @@ -13,7 +13,7 @@ use std::{ use collections::HashMap; use gpui::{AppContext, Context as _, Model, ModelContext, Subscription}; -use language::{LanguageName, LanguageRegistry}; +use language::{CachedLspAdapter, LanguageName, LanguageRegistry}; use lsp::LanguageServerName; use settings::WorktreeId; use worktree::{Event as WorktreeEvent, Worktree}; @@ -73,6 +73,28 @@ pub struct ProjectTree { _subscriptions: [Subscription; 1], } +#[derive(Clone)] +struct AdapterWrapper(Arc); +impl PartialEq for AdapterWrapper { + fn eq(&self, other: &Self) -> bool { + self.0.name.eq(&other.0.name) + } +} + +impl PartialOrd for AdapterWrapper { + fn partial_cmp(&self, other: &Self) -> Option { + self.0.name.partial_cmp(&other.0.name) + } +} + +impl Ord for AdapterWrapper { + fn cmp(&self, other: &Self) -> std::cmp::Ordering { + self.0.name.cmp(&other.0.name) + } +} + +impl Eq for AdapterWrapper {} + impl ProjectTree { fn new( languages: Arc, @@ -91,7 +113,7 @@ impl ProjectTree { ProjectPath { worktree_id, path }: ProjectPath, language_name: &LanguageName, cx: &mut AppContext, - ) -> BTreeMap { + ) -> BTreeMap { let mut roots = BTreeMap::default(); let adapters = self.languages.lsp_adapters(&language_name); let worktree_roots = match self.root_points.entry(worktree_id) { @@ -131,14 +153,17 @@ impl ProjectTree { adapter.find_closest_project_root(worktree_id, path.clone()); vacant_entry.insert(root.is_some()); if let Some(root) = root { - roots.insert(adapter.name.clone(), (worktree_id, root).into()); + roots.insert( + AdapterWrapper(adapter.clone()), + (worktree_id, root).into(), + ); } } TreeEntry::Occupied(occupied_entry) => { let is_root = *occupied_entry.get(); if is_root { roots.insert( - adapter.name.clone(), + AdapterWrapper(adapter.clone()), (worktree_id, ancestor).into(), ); } diff --git a/crates/project/src/project_tree/server_tree.rs b/crates/project/src/project_tree/server_tree.rs index eed1a28d35da7..f3d31afebd629 100644 --- a/crates/project/src/project_tree/server_tree.rs +++ b/crates/project/src/project_tree/server_tree.rs @@ -9,19 +9,19 @@ //! This module defines a Project Tree. use std::{ - collections::BTreeMap, + collections::{hash_map::Entry, BTreeMap}, path::Path, sync::{Arc, OnceLock}, }; use collections::HashMap; use gpui::{AppContext, Context as _, Model}; -use language::{LanguageName, LanguageRegistry}; +use language::{Attach, LanguageName, LanguageRegistry}; use lsp::LanguageServerName; use crate::{LanguageServerId, ProjectPath}; -use super::ProjectTree; +use super::{AdapterWrapper, ProjectTree}; pub type AbsWorkspaceRootPath = Arc; @@ -34,26 +34,6 @@ pub struct LanguageServerTree { attach_kind_cache: HashMap, } -#[derive(Clone, Copy, PartialEq)] -enum Attach { - /// Create a single language server instance per subproject root. - InstancePerRoot, - /// Use one shared language server instance for all subprojects within a project. - Shared, -} - -impl Attach { - fn root_path(&self, root_subproject_path: ProjectPath) -> ProjectPath { - match self { - Attach::InstancePerRoot => root_subproject_path, - Attach::Shared => ProjectPath { - worktree_id: root_subproject_path.worktree_id, - path: Arc::from(Path::new("")), - }, - } - } -} - #[derive(Clone)] pub(crate) struct LanguageServerTreeNode(Arc); @@ -83,11 +63,11 @@ impl LanguageServerTree { attach_kind_cache: Default::default(), }) } - fn attach_kind(&mut self, name: LanguageServerName) -> Attach { + fn attach_kind(&mut self, adapter: &AdapterWrapper) -> Attach { *self .attach_kind_cache - .entry(name) - .or_insert_with(|| Attach::Shared) + .entry(adapter.0.name.clone()) + .or_insert_with(|| adapter.0.attach_kind()) // todo: query lspadapter for it. } @@ -101,12 +81,12 @@ impl LanguageServerTree { .project_tree .update(cx, |this, cx| this.root_for_path(path, &language, cx)); - roots.into_iter().map(|(adapter_name, root_path)| { - let attach = self.attach_kind(adapter_name.clone()); + roots.into_iter().map(|(adapter, root_path)| { + let attach = self.attach_kind(&adapter); self.instances .entry(root_path) .or_default() - .entry(adapter_name) + .entry(adapter.0.name.clone()) .or_insert_with(|| LanguageServerTreeNode::new(attach)) .clone() }) From 4e89e26e43638c05aa47989276eab5620b583df8 Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Tue, 17 Dec 2024 19:00:59 +0100 Subject: [PATCH 07/63] Start wiring LSPTree into LspStore --- crates/project/src/lsp_store.rs | 27 ++++++++-------- crates/project/src/project_tree.rs | 3 +- .../project/src/project_tree/server_tree.rs | 31 ++++++++++++------- 3 files changed, 35 insertions(+), 26 deletions(-) diff --git a/crates/project/src/lsp_store.rs b/crates/project/src/lsp_store.rs index f59f8b7ebafbd..b93e119f7cee8 100644 --- a/crates/project/src/lsp_store.rs +++ b/crates/project/src/lsp_store.rs @@ -6,6 +6,7 @@ use crate::{ lsp_ext_command, prettier_store::{self, PrettierStore, PrettierStoreEvent}, project_settings::{LspSettings, ProjectSettings}, + project_tree::{LanguageServerTree, ProjectTree}, relativize_path, resolve_path, toolchain_store::{EmptyToolchainStore, ToolchainStoreEvent}, worktree_store::{WorktreeStore, WorktreeStoreEvent}, @@ -171,6 +172,7 @@ pub struct LocalLspStore { buffer_snapshots: HashMap>>, // buffer_id -> server_id -> vec of snapshots _subscription: gpui::Subscription, registered_buffers: HashMap, + lsp_tree: Model, } impl LocalLspStore { @@ -1025,7 +1027,7 @@ impl LocalLspStore { }) } - pub(crate) fn language_server_ids_for_buffer( + fn language_server_ids_for_buffer( &self, buffer: &Buffer, cx: &AppContext, @@ -1045,7 +1047,7 @@ impl LocalLspStore { } } - pub(crate) fn language_servers_for_buffer<'a>( + fn language_servers_for_buffer<'a>( &'a self, buffer: &'a Buffer, cx: &'a AppContext, @@ -2964,6 +2966,7 @@ impl LspStore { let (sender, receiver) = watch::channel(); (Self::maintain_workspace_config(receiver, cx), sender) }; + let project_tree = ProjectTree::new(languages.clone(), worktree_store.clone(), cx); Self { mode: LspStoreMode::Local(LocalLspStore { worktree_store: worktree_store.clone(), @@ -2990,6 +2993,7 @@ impl LspStore { this.as_local_mut().unwrap().shutdown_language_servers(cx) }), registered_buffers: HashMap::default(), + lsp_tree: LanguageServerTree::new(languages.clone(), project_tree, cx), }), last_formatting_failure: None, downstream_client: None, @@ -6258,18 +6262,13 @@ impl LspStore { } pub fn language_server_for_id(&self, id: LanguageServerId) -> Option> { - if let Some(local_lsp_store) = self.as_local() { - if let Some(LanguageServerState::Running { server, .. }) = - local_lsp_store.language_servers.get(&id) - { - Some(server.clone()) - } else if let Some((_, server)) = - local_lsp_store.supplementary_language_servers.get(&id) - { - Some(Arc::clone(server)) - } else { - None - } + let local_lsp_store = self.as_local()?; + if let Some(LanguageServerState::Running { server, .. }) = + local_lsp_store.language_servers.get(&id) + { + Some(server.clone()) + } else if let Some((_, server)) = local_lsp_store.supplementary_language_servers.get(&id) { + Some(Arc::clone(server)) } else { None } diff --git a/crates/project/src/project_tree.rs b/crates/project/src/project_tree.rs index dba02b706a142..81737257e5059 100644 --- a/crates/project/src/project_tree.rs +++ b/crates/project/src/project_tree.rs @@ -23,6 +23,7 @@ use crate::{ ProjectPath, }; +pub(crate) use server_tree::LanguageServerTree; type IsRoot = bool; struct WorktreeRoots { @@ -96,7 +97,7 @@ impl Ord for AdapterWrapper { impl Eq for AdapterWrapper {} impl ProjectTree { - fn new( + pub(crate) fn new( languages: Arc, worktree_store: Model, cx: &mut AppContext, diff --git a/crates/project/src/project_tree/server_tree.rs b/crates/project/src/project_tree/server_tree.rs index f3d31afebd629..99351e39f4a99 100644 --- a/crates/project/src/project_tree/server_tree.rs +++ b/crates/project/src/project_tree/server_tree.rs @@ -9,7 +9,7 @@ //! This module defines a Project Tree. use std::{ - collections::{hash_map::Entry, BTreeMap}, + collections::BTreeMap, path::Path, sync::{Arc, OnceLock}, }; @@ -23,8 +23,6 @@ use crate::{LanguageServerId, ProjectPath}; use super::{AdapterWrapper, ProjectTree}; -pub type AbsWorkspaceRootPath = Arc; - pub struct LanguageServerTree { /// Language servers for which we can just update workspaceFolders when we detect a new project root project_tree: Model, @@ -34,24 +32,39 @@ pub struct LanguageServerTree { attach_kind_cache: HashMap, } +/// A node in language server tree represents either: +/// - A language server that has already been initialized/updated for a given project +/// - A soon-to-be-initialized language server. #[derive(Clone)] pub(crate) struct LanguageServerTreeNode(Arc); impl LanguageServerTreeNode { - fn new(attach: Attach) -> Self { + fn new(attach: Attach, path: ProjectPath) -> Self { Self(Arc::new(InnerTreeNode { id: Default::default(), attach, + path, })) } + pub(crate) fn server_id( + &self, + init: impl FnOnce(Attach, ProjectPath) -> LanguageServerId, + ) -> LanguageServerId { + *self + .0 + .id + .get_or_init(|| init(self.0.attach, self.0.path.clone())) + } } + struct InnerTreeNode { id: OnceLock, attach: Attach, + path: ProjectPath, } impl LanguageServerTree { - fn new( + pub(crate) fn new( languages: Arc, project_tree: Model, cx: &mut AppContext, @@ -68,7 +81,6 @@ impl LanguageServerTree { .attach_kind_cache .entry(adapter.0.name.clone()) .or_insert_with(|| adapter.0.attach_kind()) - // todo: query lspadapter for it. } pub(crate) fn get<'a>( @@ -84,14 +96,11 @@ impl LanguageServerTree { roots.into_iter().map(|(adapter, root_path)| { let attach = self.attach_kind(&adapter); self.instances - .entry(root_path) + .entry(root_path.clone()) .or_default() .entry(adapter.0.name.clone()) - .or_insert_with(|| LanguageServerTreeNode::new(attach)) + .or_insert_with(|| LanguageServerTreeNode::new(attach, root_path)) .clone() }) } } - -#[cfg(test)] -mod tests {} From d6776ba58ce981aa0f7baf720ba1619795d508f5 Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Tue, 17 Dec 2024 19:13:13 +0100 Subject: [PATCH 08/63] :bug: --- crates/project/src/lsp_store.rs | 13 +++++++ crates/project/src/project_tree.rs | 58 +++++++++++++++--------------- 2 files changed, 42 insertions(+), 29 deletions(-) diff --git a/crates/project/src/lsp_store.rs b/crates/project/src/lsp_store.rs index b93e119f7cee8..9f1072d19550b 100644 --- a/crates/project/src/lsp_store.rs +++ b/crates/project/src/lsp_store.rs @@ -3220,6 +3220,19 @@ impl LspStore { self.detect_language_for_buffer(buffer, cx); if let Some(local) = self.as_local_mut() { + local.lsp_tree.update(cx, |this, cx| { + maybe!({ + let buffs = this + .get( + buffer.read(cx).project_path(cx)?, + buffer.read(cx).language()?.name(), + cx, + ) + .collect::>(); + println!("{}", buffs.len()); + Some(()) + }); + }); local.initialize_buffer(buffer, cx); } diff --git a/crates/project/src/project_tree.rs b/crates/project/src/project_tree.rs index 81737257e5059..287b6528325c8 100644 --- a/crates/project/src/project_tree.rs +++ b/crates/project/src/project_tree.rs @@ -132,6 +132,7 @@ impl ProjectTree { } }; + dbg!(adapters.len()); let mut filled_adapters = vec![false; adapters.len()]; let mut adapters_with_roots = 0; for ancestor in path.ancestors().skip(1) { @@ -141,43 +142,42 @@ impl ProjectTree { break; } worktree_roots.update(cx, |this, _| { - if let Some(adapter_roots) = this.roots.get_mut(ancestor) { - for (ix, adapter) in adapters.iter().enumerate() { - let adapter_already_found_root = filled_adapters[ix]; - if adapter_already_found_root { - continue; - } + let adapter_roots = this.roots.entry(ancestor.into()).or_default(); + for (ix, adapter) in adapters.iter().enumerate() { + let adapter_already_found_root = filled_adapters[ix]; + if adapter_already_found_root { + continue; + } - match adapter_roots.entry(adapter.name.clone()) { - TreeEntry::Vacant(vacant_entry) => { - let root = - adapter.find_closest_project_root(worktree_id, path.clone()); - vacant_entry.insert(root.is_some()); - if let Some(root) = root { - roots.insert( - AdapterWrapper(adapter.clone()), - (worktree_id, root).into(), - ); - } + match adapter_roots.entry(adapter.name.clone()) { + TreeEntry::Vacant(vacant_entry) => { + let root = adapter.find_closest_project_root(worktree_id, path.clone()); + vacant_entry.insert(root.is_some()); + if let Some(root) = root { + roots.insert( + AdapterWrapper(adapter.clone()), + (worktree_id, root).into(), + ); } - TreeEntry::Occupied(occupied_entry) => { - let is_root = *occupied_entry.get(); - if is_root { - roots.insert( - AdapterWrapper(adapter.clone()), - (worktree_id, ancestor).into(), - ); - } - - continue; + } + TreeEntry::Occupied(occupied_entry) => { + let is_root = *occupied_entry.get(); + if is_root { + roots.insert( + AdapterWrapper(adapter.clone()), + (worktree_id, ancestor).into(), + ); } + + continue; } - filled_adapters[ix] = true; - adapters_with_roots += 1; } + filled_adapters[ix] = true; + adapters_with_roots += 1; } }); } + dbg!(&roots.len()); roots } fn on_worktree_store_event( From d687d4b39cb3e197d7ff80f999b1b3f36d0bf058 Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Wed, 18 Dec 2024 12:35:04 +0100 Subject: [PATCH 09/63] Remove dbg!s --- crates/project/src/project_tree.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/crates/project/src/project_tree.rs b/crates/project/src/project_tree.rs index 287b6528325c8..ff161dfe15bc6 100644 --- a/crates/project/src/project_tree.rs +++ b/crates/project/src/project_tree.rs @@ -132,7 +132,6 @@ impl ProjectTree { } }; - dbg!(adapters.len()); let mut filled_adapters = vec![false; adapters.len()]; let mut adapters_with_roots = 0; for ancestor in path.ancestors().skip(1) { @@ -177,7 +176,7 @@ impl ProjectTree { } }); } - dbg!(&roots.len()); + roots } fn on_worktree_store_event( From cd15dac04ab55334bf087bb8abbcbc4ca8bd0f30 Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Thu, 19 Dec 2024 16:24:31 +0100 Subject: [PATCH 10/63] Shredding for the new path trie --- crates/project/src/project_tree.rs | 9 ++- crates/project/src/project_tree/path_trie.rs | 66 +++++++++++++++++++ .../project/src/project_tree/server_tree.rs | 1 - 3 files changed, 72 insertions(+), 4 deletions(-) create mode 100644 crates/project/src/project_tree/path_trie.rs diff --git a/crates/project/src/project_tree.rs b/crates/project/src/project_tree.rs index ff161dfe15bc6..87bc695a142d2 100644 --- a/crates/project/src/project_tree.rs +++ b/crates/project/src/project_tree.rs @@ -2,6 +2,7 @@ //! //! A Project Tree is responsible for determining where the roots of subprojects are located in a project. +mod path_trie; mod server_tree; mod toolchain_tree; @@ -132,10 +133,12 @@ impl ProjectTree { } }; - let mut filled_adapters = vec![false; adapters.len()]; - let mut adapters_with_roots = 0; + // Forward scan to find topmost roots for each adapter + let ancestors = path.ancestors().skip(1).collect::>(); + for component in ancestors.iter().rev() { + worktree_roots.update(cx, |this, cx| {}) + } for ancestor in path.ancestors().skip(1) { - // TODO: scan up until worktree root and no further. if adapters_with_roots == adapters.len() { // We've found roots for all adapters, no need to continue break; diff --git a/crates/project/src/project_tree/path_trie.rs b/crates/project/src/project_tree/path_trie.rs new file mode 100644 index 0000000000000..20fb27abc2b8a --- /dev/null +++ b/crates/project/src/project_tree/path_trie.rs @@ -0,0 +1,66 @@ +use std::{ + collections::{btree_map::Entry, BTreeMap}, + ffi::OsStr, + path::Path, + sync::Arc, +}; + +/// `PathTrie` is a Trie composed of worktree path components. +pub(super) struct PathTrie { + path_component: Arc, + value: Option<()>, + children: BTreeMap, PathTrie>, +} + +impl PathTrie { + pub(crate) fn new() -> Self { + Self::new_with_key(None) + } + fn new_with_key(value: Option>) -> Self { + PathTrie { + path_component: value.unwrap_or_else(|| Arc::from(OsStr::new(""))), + value: None, + children: BTreeMap::new(), + } + } + fn insert(&mut self, path: &TriePath, value: ()) { + let mut current = self; + + for key in path.0.iter() { + current = match current.children.entry(key.clone()) { + Entry::Vacant(vacant_entry) => { + vacant_entry.insert(PathTrie::new_with_key(Some(key.clone()))) + } + Entry::Occupied(occupied_entry) => occupied_entry.into_mut(), + }; + } + current.value = Some(value); + } + fn closest_ancestor(&self, path: &TriePath) -> Option<()> { + let mut current = self; + let mut last_value = None; + for key in path.0.iter() { + if current.value.is_some() { + last_value = current.value; + } + current = match current.children.get(key) { + Some(child) => child, + None => return last_value, + }; + } + if current.value.is_some() { + last_value = current.value; + } + last_value + } +} + +/// [TriePath] is a [Path] preprocessed for amortizing the cost of doing multiple lookups in distinct `PathTrie`s. +#[derive(Clone)] +pub(crate) struct TriePath(Arc<[Arc]>); + +impl From<&Path> for TriePath { + fn from(value: &Path) -> Self { + TriePath(value.components().map(|c| c.as_os_str().into()).collect()) + } +} diff --git a/crates/project/src/project_tree/server_tree.rs b/crates/project/src/project_tree/server_tree.rs index 99351e39f4a99..acdb8942b6dfa 100644 --- a/crates/project/src/project_tree/server_tree.rs +++ b/crates/project/src/project_tree/server_tree.rs @@ -10,7 +10,6 @@ use std::{ collections::BTreeMap, - path::Path, sync::{Arc, OnceLock}, }; From 909fb66208d41de429c9d069255a856624ff4f6c Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Mon, 23 Dec 2024 11:06:50 +0100 Subject: [PATCH 11/63] Renames --- crates/project/src/project_tree/path_trie.rs | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/crates/project/src/project_tree/path_trie.rs b/crates/project/src/project_tree/path_trie.rs index 20fb27abc2b8a..a590699775535 100644 --- a/crates/project/src/project_tree/path_trie.rs +++ b/crates/project/src/project_tree/path_trie.rs @@ -5,19 +5,24 @@ use std::{ sync::Arc, }; -/// `PathTrie` is a Trie composed of worktree path components. -pub(super) struct PathTrie { +/// [RootPathTrie] is a workhorse of [super::ProjectTree]. It is responsible for determining the closest known project root for a given path. +/// We can not +/// It also determines how much of a given path is unexplored, thus letting callers fill in that gap if needed. +/// A path is unexplored when the closest ancestor of a path is not the path itself; that means that we have not yet ran the scan on that path. +/// For example, if there's a project root at path `python/project` and we query for a path `python/project/subdir/another_subdir/file.py`, there is +/// a known root at `python/project` and the unexplored part is `subdir/another_subdir` - we need to run a scan on these 2 directories +pub(super) struct RootPathTrie { path_component: Arc, value: Option<()>, - children: BTreeMap, PathTrie>, + children: BTreeMap, RootPathTrie>, } -impl PathTrie { +impl RootPathTrie { pub(crate) fn new() -> Self { Self::new_with_key(None) } fn new_with_key(value: Option>) -> Self { - PathTrie { + RootPathTrie { path_component: value.unwrap_or_else(|| Arc::from(OsStr::new(""))), value: None, children: BTreeMap::new(), @@ -29,7 +34,7 @@ impl PathTrie { for key in path.0.iter() { current = match current.children.entry(key.clone()) { Entry::Vacant(vacant_entry) => { - vacant_entry.insert(PathTrie::new_with_key(Some(key.clone()))) + vacant_entry.insert(RootPathTrie::new_with_key(Some(key.clone()))) } Entry::Occupied(occupied_entry) => occupied_entry.into_mut(), }; @@ -55,7 +60,7 @@ impl PathTrie { } } -/// [TriePath] is a [Path] preprocessed for amortizing the cost of doing multiple lookups in distinct `PathTrie`s. +/// [TriePath] is a [Path] preprocessed for amortizing the cost of doing multiple lookups in distinct [RootPathTrie]s. #[derive(Clone)] pub(crate) struct TriePath(Arc<[Arc]>); From cb24881aaaa1eac40de5ebf3ffc05e27ce07c7f5 Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Mon, 23 Dec 2024 12:06:22 +0100 Subject: [PATCH 12/63] Add labels to pathtrie --- crates/project/src/project_tree/path_trie.rs | 46 ++++++++++---------- 1 file changed, 24 insertions(+), 22 deletions(-) diff --git a/crates/project/src/project_tree/path_trie.rs b/crates/project/src/project_tree/path_trie.rs index a590699775535..4c054eb0fa4ed 100644 --- a/crates/project/src/project_tree/path_trie.rs +++ b/crates/project/src/project_tree/path_trie.rs @@ -1,62 +1,64 @@ use std::{ - collections::{btree_map::Entry, BTreeMap}, + collections::{btree_map::Entry, BTreeMap, BTreeSet}, ffi::OsStr, path::Path, sync::Arc, }; /// [RootPathTrie] is a workhorse of [super::ProjectTree]. It is responsible for determining the closest known project root for a given path. -/// We can not /// It also determines how much of a given path is unexplored, thus letting callers fill in that gap if needed. /// A path is unexplored when the closest ancestor of a path is not the path itself; that means that we have not yet ran the scan on that path. /// For example, if there's a project root at path `python/project` and we query for a path `python/project/subdir/another_subdir/file.py`, there is /// a known root at `python/project` and the unexplored part is `subdir/another_subdir` - we need to run a scan on these 2 directories -pub(super) struct RootPathTrie { +pub(super) struct RootPathTrie