project: Track manifest locations per unique manifest locator (#27194)

This pull request paves way for exposing manifest tracking to
extensions.
- Project tree was renamed to manifest tree to better reflect it's
intent (and avoid confusion).
- Language server adapters now provide a name of their *manifest
locator*. If multiple language servers refer to the same locator, the
locating code will run just once for a given path.

Release Notes:

- N/A *or* Added/Fixed/Improved ...

---------

Co-authored-by: Anthony <anthony@zed.dev>
This commit is contained in:
Piotr Osiewicz 2025-03-21 15:22:36 +01:00 committed by GitHub
parent 6bced3a834
commit 05aa8880a4
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 241 additions and 154 deletions

View file

@ -6,9 +6,9 @@ use crate::{
buffer_store::{BufferStore, BufferStoreEvent},
environment::ProjectEnvironment,
lsp_command::{self, *},
manifest_tree::{AdapterQuery, LanguageServerTree, LaunchDisposition, ManifestTree},
prettier_store::{self, PrettierStore, PrettierStoreEvent},
project_settings::{LspSettings, ProjectSettings},
project_tree::{AdapterQuery, LanguageServerTree, LaunchDisposition, ProjectTree},
relativize_path, resolve_path,
toolchain_store::{EmptyToolchainStore, ToolchainStoreEvent},
worktree_store::{WorktreeStore, WorktreeStoreEvent},
@ -3349,7 +3349,7 @@ impl LspStore {
sender,
)
};
let project_tree = ProjectTree::new(worktree_store.clone(), cx);
let manifest_tree = ManifestTree::new(worktree_store.clone(), cx);
Self {
mode: LspStoreMode::Local(LocalLspStore {
weak: cx.weak_entity(),
@ -3375,7 +3375,7 @@ impl LspStore {
_subscription: cx.on_app_quit(|this, cx| {
this.as_local_mut().unwrap().shutdown_language_servers(cx)
}),
lsp_tree: LanguageServerTree::new(project_tree, languages.clone(), cx),
lsp_tree: LanguageServerTree::new(manifest_tree, languages.clone(), cx),
registered_buffers: Default::default(),
}),
last_formatting_failure: None,

View file

@ -1,7 +1,9 @@
//! This module defines a Project Tree.
//! This module defines a Manifest Tree.
//!
//! A Project Tree is responsible for determining where the roots of subprojects are located in a project.
//! A Manifest Tree is responsible for determining where the manifests for subprojects are located in a project.
//! This then is used to provide those locations to language servers & determine locations eligible for toolchain selection.
mod manifest_store;
mod path_trie;
mod server_tree;
@ -14,8 +16,8 @@ use std::{
use collections::HashMap;
use gpui::{App, AppContext as _, Context, Entity, EventEmitter, Subscription};
use language::{CachedLspAdapter, LspAdapterDelegate};
use lsp::LanguageServerName;
use language::{LspAdapterDelegate, ManifestName, ManifestQuery};
pub use manifest_store::ManifestProviders;
use path_trie::{LabelPresence, RootPathTrie, TriePath};
use settings::{SettingsStore, WorktreeId};
use worktree::{Event as WorktreeEvent, Worktree};
@ -28,7 +30,7 @@ use crate::{
pub(crate) use server_tree::{AdapterQuery, LanguageServerTree, LaunchDisposition};
struct WorktreeRoots {
roots: RootPathTrie<LanguageServerName>,
roots: RootPathTrie<ManifestName>,
worktree_store: Entity<WorktreeStore>,
_worktree_subscription: Subscription,
}
@ -70,55 +72,21 @@ impl WorktreeRoots {
}
}
pub struct ProjectTree {
pub struct ManifestTree {
root_points: HashMap<WorktreeId, Entity<WorktreeRoots>>,
worktree_store: Entity<WorktreeStore>,
_subscriptions: [Subscription; 2],
}
#[derive(Debug, Clone)]
struct AdapterWrapper(Arc<CachedLspAdapter>);
impl PartialEq for AdapterWrapper {
fn eq(&self, other: &Self) -> bool {
self.0.name.eq(&other.0.name)
}
}
impl Eq for AdapterWrapper {}
impl std::hash::Hash for AdapterWrapper {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
self.0.name.hash(state);
}
}
impl PartialOrd for AdapterWrapper {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
Some(self.0.name.cmp(&other.0.name))
}
}
impl Ord for AdapterWrapper {
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
self.0.name.cmp(&other.0.name)
}
}
impl Borrow<LanguageServerName> for AdapterWrapper {
fn borrow(&self) -> &LanguageServerName {
&self.0.name
}
}
#[derive(PartialEq)]
pub(crate) enum ProjectTreeEvent {
pub(crate) enum ManifestTreeEvent {
WorktreeRemoved(WorktreeId),
Cleared,
}
impl EventEmitter<ProjectTreeEvent> for ProjectTree {}
impl EventEmitter<ManifestTreeEvent> for ManifestTree {}
impl ProjectTree {
impl ManifestTree {
pub(crate) fn new(worktree_store: Entity<WorktreeStore>, cx: &mut App) -> Entity<Self> {
cx.new(|cx| Self {
root_points: Default::default(),
@ -130,26 +98,22 @@ impl ProjectTree {
worktree_roots.roots = RootPathTrie::new();
})
}
cx.emit(ProjectTreeEvent::Cleared);
cx.emit(ManifestTreeEvent::Cleared);
}),
],
worktree_store,
})
}
#[allow(clippy::mutable_key_type)]
fn root_for_path(
&mut self,
ProjectPath { worktree_id, path }: ProjectPath,
adapters: Vec<Arc<CachedLspAdapter>>,
manifests: &mut dyn Iterator<Item = ManifestName>,
delegate: Arc<dyn LspAdapterDelegate>,
cx: &mut App,
) -> BTreeMap<AdapterWrapper, ProjectPath> {
) -> BTreeMap<ManifestName, ProjectPath> {
debug_assert_eq!(delegate.worktree_id(), worktree_id);
#[allow(clippy::mutable_key_type)]
let mut roots = BTreeMap::from_iter(
adapters
.into_iter()
.map(|adapter| (AdapterWrapper(adapter), (None, LabelPresence::KnownAbsent))),
manifests.map(|manifest| (manifest, (None, LabelPresence::KnownAbsent))),
);
let worktree_roots = match self.root_points.entry(worktree_id) {
Entry::Occupied(occupied_entry) => occupied_entry.get().clone(),
@ -182,7 +146,8 @@ impl ProjectTree {
ControlFlow::Continue(())
});
});
for (adapter, (root_path, presence)) in &mut roots {
for (manifest_name, (root_path, presence)) in &mut roots {
if *presence == LabelPresence::Present {
continue;
}
@ -198,12 +163,22 @@ impl ProjectTree {
.unwrap_or_else(|| path.components().count() + 1);
if depth > 0 {
let root = adapter.0.find_project_root(&path, depth, &delegate);
let Some(provider) = ManifestProviders::global(cx).get(manifest_name.borrow())
else {
log::warn!("Manifest provider `{}` not found", manifest_name.as_ref());
continue;
};
let root = provider.search(ManifestQuery {
path: path.clone(),
depth,
delegate: delegate.clone(),
});
match root {
Some(known_root) => worktree_roots.update(cx, |this, _| {
let root = TriePath::from(&*known_root);
this.roots
.insert(&root, adapter.0.name(), LabelPresence::Present);
.insert(&root, manifest_name.clone(), LabelPresence::Present);
*presence = LabelPresence::Present;
*root_path = Some(ProjectPath {
worktree_id,
@ -212,7 +187,7 @@ impl ProjectTree {
}),
None => worktree_roots.update(cx, |this, _| {
this.roots
.insert(&key, adapter.0.name(), LabelPresence::KnownAbsent);
.insert(&key, manifest_name.clone(), LabelPresence::KnownAbsent);
}),
}
}
@ -235,7 +210,7 @@ impl ProjectTree {
match evt {
WorktreeStoreEvent::WorktreeRemoved(_, worktree_id) => {
self.root_points.remove(&worktree_id);
cx.emit(ProjectTreeEvent::WorktreeRemoved(*worktree_id));
cx.emit(ManifestTreeEvent::WorktreeRemoved(*worktree_id));
}
_ => {}
}

View file

@ -0,0 +1,48 @@
use collections::HashMap;
use gpui::{App, Global, SharedString};
use parking_lot::RwLock;
use std::{ops::Deref, sync::Arc};
use language::{ManifestName, ManifestProvider};
#[derive(Default)]
struct ManifestProvidersState {
providers: HashMap<ManifestName, Arc<dyn ManifestProvider>>,
}
#[derive(Clone, Default)]
pub struct ManifestProviders(Arc<RwLock<ManifestProvidersState>>);
#[derive(Default)]
struct GlobalManifestProvider(ManifestProviders);
impl Deref for GlobalManifestProvider {
type Target = ManifestProviders;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl Global for GlobalManifestProvider {}
impl ManifestProviders {
/// Returns the global [`ManifestStore`].
///
/// Inserts a default [`ManifestStore`] if one does not yet exist.
pub fn global(cx: &mut App) -> Self {
cx.default_global::<GlobalManifestProvider>().0.clone()
}
pub fn register(&self, provider: Arc<dyn ManifestProvider>) {
self.0.write().providers.insert(provider.name(), provider);
}
pub fn unregister(&self, name: &SharedString) {
self.0.write().providers.remove(name);
}
pub(super) fn get(&self, name: &SharedString) -> Option<Arc<dyn ManifestProvider>> {
self.0.read().providers.get(name).cloned()
}
}

View file

@ -6,7 +6,7 @@ use std::{
sync::Arc,
};
/// [RootPathTrie] is a workhorse of [super::ProjectTree]. It is responsible for determining the closest known project root for a given path.
/// [RootPathTrie] is a workhorse of [super::ManifestTree]. It is responsible for determining the closest known project root for a given path.
/// It also determines how much of a given path is unexplored, thus letting callers fill in that gap if needed.
/// Conceptually, it allows one to annotate Worktree entries with arbitrary extra metadata and run closest-ancestor searches.
///

View file

@ -6,7 +6,6 @@
//! 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, BTreeSet},
@ -16,10 +15,9 @@ use std::{
use collections::{HashMap, IndexMap};
use gpui::{App, AppContext as _, Entity, Subscription};
use itertools::Itertools;
use language::{
language_settings::AllLanguageSettings, Attach, LanguageName, LanguageRegistry,
LspAdapterDelegate,
language_settings::AllLanguageSettings, Attach, CachedLspAdapter, LanguageName,
LanguageRegistry, LspAdapterDelegate,
};
use lsp::LanguageServerName;
use settings::{Settings, SettingsLocation, WorktreeId};
@ -27,7 +25,7 @@ use std::sync::OnceLock;
use crate::{project_settings::LspSettings, LanguageServerId, ProjectPath};
use super::{AdapterWrapper, ProjectTree, ProjectTreeEvent};
use super::{ManifestTree, ManifestTreeEvent};
#[derive(Debug, Default)]
struct ServersForWorktree {
@ -38,7 +36,7 @@ struct ServersForWorktree {
}
pub struct LanguageServerTree {
project_tree: Entity<ProjectTree>,
manifest_tree: Entity<ManifestTree>,
instances: BTreeMap<WorktreeId, ServersForWorktree>,
attach_kind_cache: HashMap<LanguageServerName, Attach>,
languages: Arc<LanguageRegistry>,
@ -133,30 +131,20 @@ pub(crate) enum AdapterQuery<'a> {
impl LanguageServerTree {
pub(crate) fn new(
project_tree: Entity<ProjectTree>,
manifest_tree: Entity<ManifestTree>,
languages: Arc<LanguageRegistry>,
cx: &mut App,
) -> Entity<Self> {
cx.new(|cx| Self {
_subscriptions: cx.subscribe(
&project_tree,
|_: &mut Self, _, event, _| {
if event == &ProjectTreeEvent::Cleared {}
},
),
project_tree,
_subscriptions: cx.subscribe(&manifest_tree, |_: &mut Self, _, event, _| {
if event == &ManifestTreeEvent::Cleared {}
}),
manifest_tree,
instances: Default::default(),
attach_kind_cache: Default::default(),
languages,
})
}
/// Memoize calls to attach_kind on LspAdapter (which might be a WASM extension, thus ~expensive to call).
fn attach_kind(&mut self, adapter: &AdapterWrapper) -> Attach {
*self
.attach_kind_cache
.entry(adapter.0.name.clone())
.or_insert_with(|| adapter.0.attach_kind())
}
/// Get all language server root points for a given path and language; the language servers might already be initialized at a given path.
pub(crate) fn get<'a>(
@ -174,10 +162,14 @@ impl LanguageServerTree {
AdapterQuery::Language(language_name) => {
self.adapters_for_language(settings_location, language_name, cx)
}
AdapterQuery::Adapter(language_server_name) => IndexMap::from_iter(
self.adapter_for_name(language_server_name)
.map(|adapter| (adapter, (LspSettings::default(), BTreeSet::new()))),
),
AdapterQuery::Adapter(language_server_name) => {
IndexMap::from_iter(self.adapter_for_name(language_server_name).map(|adapter| {
(
adapter.name(),
(LspSettings::default(), BTreeSet::new(), adapter),
)
}))
}
};
self.get_with_adapters(path, adapters, delegate, cx)
}
@ -185,41 +177,48 @@ impl LanguageServerTree {
fn get_with_adapters<'a>(
&'a mut self,
path: ProjectPath,
adapters: IndexMap<AdapterWrapper, (LspSettings, BTreeSet<LanguageName>)>,
adapters: IndexMap<
LanguageServerName,
(LspSettings, BTreeSet<LanguageName>, Arc<CachedLspAdapter>),
>,
delegate: Arc<dyn LspAdapterDelegate>,
cx: &mut App,
) -> impl Iterator<Item = LanguageServerTreeNode> + 'a {
let worktree_id = path.worktree_id;
#[allow(clippy::mutable_key_type)]
let mut roots = self.project_tree.update(cx, |this, cx| {
let mut manifest_to_adapters = BTreeMap::default();
for (_, _, adapter) in adapters.values() {
if let Some(manifest_name) = adapter.manifest_name() {
manifest_to_adapters
.entry(manifest_name)
.or_insert_with(Vec::default)
.push(adapter.clone());
}
}
let roots = self.manifest_tree.update(cx, |this, cx| {
this.root_for_path(
path,
adapters
.iter()
.map(|(adapter, _)| adapter.0.clone())
.collect(),
&mut manifest_to_adapters.keys().cloned(),
delegate,
cx,
)
});
let mut root_path = None;
// Backwards-compat: Fill in any adapters for which we did not detect the root as having the project root at the root of a worktree.
for (adapter, _) in adapters.iter() {
roots.entry(adapter.clone()).or_insert_with(|| {
root_path
.get_or_insert_with(|| ProjectPath {
worktree_id,
path: Arc::from("".as_ref()),
})
.clone()
});
}
roots
let root_path = std::cell::LazyCell::new(move || ProjectPath {
worktree_id,
path: Arc::from("".as_ref()),
});
adapters
.into_iter()
.filter_map(move |(adapter, root_path)| {
let attach = self.attach_kind(&adapter);
let (index, _, (settings, new_languages)) = adapters.get_full(&adapter)?;
.map(move |(_, (settings, new_languages, adapter))| {
// Backwards-compat: Fill in any adapters for which we did not detect the root as having the project root at the root of a worktree.
let root_path = adapter
.manifest_name()
.and_then(|name| roots.get(&name))
.cloned()
.unwrap_or_else(|| root_path.clone());
let attach = adapter.attach_kind();
let inner_node = self
.instances
.entry(root_path.worktree_id)
@ -227,27 +226,25 @@ impl LanguageServerTree {
.roots
.entry(root_path.path.clone())
.or_default()
.entry(adapter.0.name.clone());
let (node, languages) = inner_node.or_insert_with(move || {
.entry(adapter.name());
let (node, languages) = inner_node.or_insert_with(|| {
(
Arc::new(InnerTreeNode::new(
adapter.0.name(),
adapter.name(),
attach,
root_path,
root_path.clone(),
settings.clone(),
)),
Default::default(),
)
});
languages.extend(new_languages.iter().cloned());
Some((index, Arc::downgrade(&node).into()))
Arc::downgrade(&node).into()
})
.sorted_by_key(|(index, _)| *index)
.map(|(_, node)| node)
}
fn adapter_for_name(&self, name: &LanguageServerName) -> Option<AdapterWrapper> {
self.languages.adapter_for_name(name).map(AdapterWrapper)
fn adapter_for_name(&self, name: &LanguageServerName) -> Option<Arc<CachedLspAdapter>> {
self.languages.adapter_for_name(name)
}
fn adapters_for_language(
@ -255,7 +252,8 @@ impl LanguageServerTree {
settings_location: SettingsLocation,
language_name: &LanguageName,
cx: &App,
) -> IndexMap<AdapterWrapper, (LspSettings, BTreeSet<LanguageName>)> {
) -> IndexMap<LanguageServerName, (LspSettings, BTreeSet<LanguageName>, Arc<CachedLspAdapter>)>
{
let settings = AllLanguageSettings::get(Some(settings_location), cx).language(
Some(settings_location),
Some(language_name),
@ -297,10 +295,11 @@ impl LanguageServerTree {
.cloned()
.unwrap_or_default();
Some((
AdapterWrapper(adapter),
adapter.name(),
(
adapter_settings,
BTreeSet::from_iter([language_name.clone()]),
adapter,
),
))
})
@ -314,8 +313,8 @@ impl LanguageServerTree {
self.languages.reorder_language_servers(
&language_name,
adapters_with_settings
.keys()
.map(|wrapper| wrapper.0.clone())
.values()
.map(|(_, _, adapter)| adapter.clone())
.collect(),
);
@ -392,11 +391,16 @@ impl<'tree> ServerTreeRebase<'tree> {
self.new_tree
.adapters_for_language(settings_location, language_name, cx)
}
AdapterQuery::Adapter(language_server_name) => IndexMap::from_iter(
self.new_tree
.adapter_for_name(language_server_name)
.map(|adapter| (adapter, (LspSettings::default(), BTreeSet::new()))),
),
AdapterQuery::Adapter(language_server_name) => {
IndexMap::from_iter(self.new_tree.adapter_for_name(language_server_name).map(
|adapter| {
(
adapter.name(),
(LspSettings::default(), BTreeSet::new(), adapter),
)
},
))
}
};
self.new_tree

View file

@ -7,9 +7,9 @@ pub mod git_store;
pub mod image_store;
pub mod lsp_command;
pub mod lsp_store;
mod manifest_tree;
pub mod prettier_store;
pub mod project_settings;
mod project_tree;
pub mod search;
mod task_inventory;
pub mod task_store;
@ -73,6 +73,7 @@ use lsp::{
};
use lsp_command::*;
use lsp_store::{CompletionDocumentation, LspFormatTarget, OpenLspBufferHandle};
pub use manifest_tree::ManifestProviders;
use node_runtime::NodeRuntime;
use parking_lot::Mutex;
pub use prettier_store::PrettierStore;