project: Allow running multiple instances of a single language server within a single worktree (#23473)

This PR introduces a new entity called Project Tree which is responsible
for finding subprojects within a worktree;
a subproject is a language-specific subset of a worktree which should be
accurately tracked on the language server side. We'll have an ability to
set multiple disjoint workspaceFolders on language server side OR spawn
multiple instances of a single language server (which will be the case
with e.g. Python language servers, as they need to interact with
multiple disjoint virtual environments).
Project Tree assumes that projects of the same LspAdapter kind cannot
overlap. Additionally project nesting is not allowed within the scope of
a single LspAdapter.

Closes https://github.com/zed-industries/zed/issues/5108
Re-lands #22182 which I had to revert due to merging it into todays
Preview.

Release Notes:

- Language servers now track their working directory more accurately.

---------

Co-authored-by: João <joao@zed.dev>
This commit is contained in:
Piotr Osiewicz 2025-01-22 21:19:02 +01:00 committed by GitHub
parent 2c2a3ef13d
commit 08b3c03241
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
29 changed files with 2151 additions and 943 deletions

View file

@ -0,0 +1,243 @@
//! This module defines a Project Tree.
//!
//! A Project Tree is responsible for determining where the roots of subprojects are located in a project.
mod path_trie;
mod server_tree;
use std::{
borrow::Borrow,
collections::{hash_map::Entry, BTreeMap},
ops::ControlFlow,
sync::Arc,
};
use collections::HashMap;
use gpui::{AppContext, Context as _, EventEmitter, Model, ModelContext, Subscription};
use language::{CachedLspAdapter, LspAdapterDelegate};
use lsp::LanguageServerName;
use path_trie::{LabelPresence, RootPathTrie, TriePath};
use settings::{SettingsStore, WorktreeId};
use worktree::{Event as WorktreeEvent, Worktree};
use crate::{
worktree_store::{WorktreeStore, WorktreeStoreEvent},
ProjectPath,
};
pub(crate) use server_tree::{LanguageServerTree, LaunchDisposition};
struct WorktreeRoots {
roots: RootPathTrie<LanguageServerName>,
worktree_store: Model<WorktreeStore>,
_worktree_subscription: Subscription,
}
impl WorktreeRoots {
fn new(
worktree_store: Model<WorktreeStore>,
worktree: Model<Worktree>,
cx: &mut AppContext,
) -> Model<Self> {
cx.new_model(|cx| Self {
roots: RootPathTrie::new(),
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 => {
let path = TriePath::from(path.as_ref());
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;
};
let path = TriePath::from(entry.path.as_ref());
this.roots.remove(&path);
}
}
}),
})
}
}
pub struct ProjectTree {
root_points: HashMap<WorktreeId, Model<WorktreeRoots>>,
worktree_store: Model<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 {
WorktreeRemoved(WorktreeId),
Cleared,
}
impl EventEmitter<ProjectTreeEvent> for ProjectTree {}
impl ProjectTree {
pub(crate) fn new(worktree_store: Model<WorktreeStore>, cx: &mut AppContext) -> Model<Self> {
cx.new_model(|cx| Self {
root_points: Default::default(),
_subscriptions: [
cx.subscribe(&worktree_store, Self::on_worktree_store_event),
cx.observe_global::<SettingsStore>(|this, cx| {
for (_, roots) in &mut this.root_points {
roots.update(cx, |worktree_roots, _| {
worktree_roots.roots = RootPathTrie::new();
})
}
cx.emit(ProjectTreeEvent::Cleared);
}),
],
worktree_store,
})
}
#[allow(clippy::mutable_key_type)]
fn root_for_path(
&mut self,
ProjectPath { worktree_id, path }: ProjectPath,
adapters: Vec<Arc<CachedLspAdapter>>,
delegate: Arc<dyn LspAdapterDelegate>,
cx: &mut AppContext,
) -> BTreeMap<AdapterWrapper, 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))),
);
let worktree_roots = match self.root_points.entry(worktree_id) {
Entry::Occupied(occupied_entry) => occupied_entry.get().clone(),
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).clone()
}
};
let key = TriePath::from(&*path);
worktree_roots.update(cx, |this, _| {
this.roots.walk(&key, &mut |path, labels| {
for (label, presence) in labels {
if let Some((marked_path, current_presence)) = roots.get_mut(label) {
if *current_presence > *presence {
debug_assert!(false, "RootPathTrie precondition violation; while walking the tree label presence is only allowed to increase");
}
*marked_path = Some(ProjectPath {worktree_id, path: path.clone()});
*current_presence = *presence;
}
}
ControlFlow::Continue(())
});
});
for (adapter, (root_path, presence)) in &mut roots {
if *presence == LabelPresence::Present {
continue;
}
let depth = root_path
.as_ref()
.map(|root_path| {
path.strip_prefix(&root_path.path)
.unwrap()
.components()
.count()
})
.unwrap_or_else(|| path.components().count() + 1);
if depth > 0 {
let root = adapter.0.find_project_root(&path, depth, &delegate);
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);
*presence = LabelPresence::Present;
*root_path = Some(ProjectPath {
worktree_id,
path: known_root,
});
}),
None => worktree_roots.update(cx, |this, _| {
this.roots
.insert(&key, adapter.0.name(), LabelPresence::KnownAbsent);
}),
}
}
}
roots
.into_iter()
.filter_map(|(k, (path, presence))| {
let path = path?;
presence.eq(&LabelPresence::Present).then(|| (k, path))
})
.collect()
}
fn on_worktree_store_event(
&mut self,
_: Model<WorktreeStore>,
evt: &WorktreeStoreEvent,
cx: &mut ModelContext<Self>,
) {
match evt {
WorktreeStoreEvent::WorktreeRemoved(_, worktree_id) => {
self.root_points.remove(&worktree_id);
cx.emit(ProjectTreeEvent::WorktreeRemoved(*worktree_id));
}
_ => {}
}
}
}