Revert "project: Fine-grained language server management" (#23804)

Reverts zed-industries/zed#23708
This commit is contained in:
Piotr Osiewicz 2025-01-28 22:38:06 +01:00 committed by GitHub
parent bda269059b
commit 22afec32cf
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
28 changed files with 935 additions and 2226 deletions

View file

@ -43,7 +43,6 @@ log.workspace = true
lsp.workspace = true
node_runtime.workspace = true
image.workspace = true
once_cell.workspace = true
parking_lot.workspace = true
pathdiff.workspace = true
paths.workspace = true

View file

@ -942,11 +942,9 @@ fn language_server_for_buffer(
) -> Result<(Arc<CachedLspAdapter>, Arc<LanguageServer>)> {
lsp_store
.update(cx, |lsp_store, cx| {
buffer.update(cx, |buffer, cx| {
lsp_store
.language_server_for_local_buffer(buffer, server_id, cx)
.map(|(adapter, server)| (adapter.clone(), server.clone()))
})
lsp_store
.language_server_for_local_buffer(buffer.read(cx), server_id, cx)
.map(|(adapter, server)| (adapter.clone(), server.clone()))
})?
.ok_or_else(|| anyhow!("no language server found for buffer"))
}

File diff suppressed because it is too large Load diff

View file

@ -40,7 +40,7 @@ pub struct PrettierStore {
prettier_instances: HashMap<PathBuf, PrettierInstance>,
}
pub(crate) enum PrettierStoreEvent {
pub enum PrettierStoreEvent {
LanguageServerRemoved(LanguageServerId),
LanguageServerAdded {
new_server_id: LanguageServerId,

View file

@ -9,7 +9,6 @@ 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;
@ -476,7 +475,6 @@ pub struct DocumentHighlight {
pub struct Symbol {
pub language_server_name: LanguageServerName,
pub source_worktree_id: WorktreeId,
pub source_language_server_id: LanguageServerId,
pub path: ProjectPath,
pub label: CodeLabel,
pub name: String,
@ -1908,7 +1906,7 @@ impl Project {
pub fn open_buffer(
&mut self,
path: impl Into<ProjectPath>,
cx: &mut App,
cx: &mut Context<Self>,
) -> Task<Result<Entity<Buffer>>> {
if self.is_disconnected(cx) {
return Task::ready(Err(anyhow!(ErrorCode::Disconnected)));
@ -1923,11 +1921,11 @@ impl Project {
pub fn open_buffer_with_lsp(
&mut self,
path: impl Into<ProjectPath>,
cx: &mut App,
cx: &mut Context<Self>,
) -> Task<Result<(Entity<Buffer>, lsp_store::OpenLspBufferHandle)>> {
let buffer = self.open_buffer(path, cx);
let lsp_store = self.lsp_store().clone();
cx.spawn(|mut cx| async move {
cx.spawn(|_, mut cx| async move {
let buffer = buffer.await?;
let handle = lsp_store.update(&mut cx, |lsp_store, cx| {
lsp_store.register_buffer_with_language_servers(&buffer, cx)
@ -4133,25 +4131,14 @@ impl Project {
self.lsp_store.read(cx).supplementary_language_servers()
}
pub fn language_server_for_id(
&self,
id: LanguageServerId,
cx: &App,
) -> Option<Arc<LanguageServer>> {
self.lsp_store.read(cx).language_server_for_id(id)
}
pub fn for_language_servers_for_local_buffer<R: 'static>(
&self,
buffer: &Buffer,
callback: impl FnOnce(
Box<dyn Iterator<Item = (&Arc<CachedLspAdapter>, &Arc<LanguageServer>)> + '_>,
) -> R,
cx: &mut App,
) -> R {
self.lsp_store.update(cx, |this, cx| {
callback(Box::new(this.language_servers_for_local_buffer(buffer, cx)))
})
pub fn language_servers_for_local_buffer<'a>(
&'a self,
buffer: &'a Buffer,
cx: &'a App,
) -> impl Iterator<Item = (&'a Arc<CachedLspAdapter>, &'a Arc<LanguageServer>)> {
self.lsp_store
.read(cx)
.language_servers_for_local_buffer(buffer, cx)
}
pub fn buffer_store(&self) -> &Entity<BufferStore> {

View file

@ -1749,12 +1749,6 @@ async fn test_toggling_enable_language_server(cx: &mut gpui::TestAppContext) {
});
})
});
let _rs_buffer = project
.update(cx, |project, cx| {
project.open_local_buffer_with_lsp("/dir/a.rs", cx)
})
.await
.unwrap();
let mut fake_rust_server_2 = fake_rust_servers.next().await.unwrap();
assert_eq!(
fake_rust_server_2
@ -2579,28 +2573,25 @@ async fn test_definition(cx: &mut gpui::TestAppContext) {
fs.insert_tree(
"/dir",
json!({
"a.rs": "const fn a() { A }",
"b.rs": "const y: i32 = crate::a()",
}),
)
.await;
fs.insert_tree(
"/another_dir",
json!({
"a.rs": "const fn a() { A }"}),
)
.await;
let project = Project::test(fs, ["/dir".as_ref()], cx).await;
let project = Project::test(fs, ["/dir/b.rs".as_ref()], cx).await;
let language_registry = project.read_with(cx, |project, _| project.languages().clone());
language_registry.add(rust_lang());
let mut fake_servers = language_registry.register_fake_lsp("Rust", FakeLspAdapter::default());
let (buffer, _handle) = project
.update(cx, |project, cx| {
project.open_local_buffer_with_lsp("/dir/b.rs", cx)
})
.await
.unwrap();
let fake_server = fake_servers.next().await.unwrap();
fake_server.handle_request::<lsp::request::GotoDefinition, _, _>(|params, _| async move {
let params = params.text_document_position_params;
@ -2612,11 +2603,12 @@ async fn test_definition(cx: &mut gpui::TestAppContext) {
Ok(Some(lsp::GotoDefinitionResponse::Scalar(
lsp::Location::new(
lsp::Url::from_file_path("/another_dir/a.rs").unwrap(),
lsp::Url::from_file_path("/dir/a.rs").unwrap(),
lsp::Range::new(lsp::Position::new(0, 9), lsp::Position::new(0, 10)),
),
)))
});
let mut definitions = project
.update(cx, |project, cx| project.definition(&buffer, 22, cx))
.await
@ -2637,21 +2629,18 @@ async fn test_definition(cx: &mut gpui::TestAppContext) {
.as_local()
.unwrap()
.abs_path(cx),
Path::new("/another_dir/a.rs"),
Path::new("/dir/a.rs"),
);
assert_eq!(definition.target.range.to_offset(target_buffer), 9..10);
assert_eq!(
list_worktrees(&project, cx),
[
("/another_dir/a.rs".as_ref(), false),
("/dir".as_ref(), true)
],
[("/dir/a.rs".as_ref(), false), ("/dir/b.rs".as_ref(), true)],
);
drop(definition);
});
cx.update(|cx| {
assert_eq!(list_worktrees(&project, cx), [("/dir".as_ref(), true)]);
assert_eq!(list_worktrees(&project, cx), [("/dir/b.rs".as_ref(), true)]);
});
fn list_worktrees<'a>(project: &'a Entity<Project>, cx: &'a App) -> Vec<(&'a Path, bool)> {

View file

@ -1,243 +0,0 @@
//! 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::{App, AppContext, Context, Entity, EventEmitter, 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::{AdapterQuery, LanguageServerTree, LaunchDisposition};
struct WorktreeRoots {
roots: RootPathTrie<LanguageServerName>,
worktree_store: Entity<WorktreeStore>,
_worktree_subscription: Subscription,
}
impl WorktreeRoots {
fn new(
worktree_store: Entity<WorktreeStore>,
worktree: Entity<Worktree>,
cx: &mut App,
) -> Entity<Self> {
cx.new(|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, 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 {
WorktreeRemoved(WorktreeId),
Cleared,
}
impl EventEmitter<ProjectTreeEvent> for ProjectTree {}
impl ProjectTree {
pub(crate) fn new(worktree_store: Entity<WorktreeStore>, cx: &mut App) -> Entity<Self> {
cx.new(|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 App,
) -> 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,
_: Entity<WorktreeStore>,
evt: &WorktreeStoreEvent,
cx: &mut Context<Self>,
) {
match evt {
WorktreeStoreEvent::WorktreeRemoved(_, worktree_id) => {
self.root_points.remove(&worktree_id);
cx.emit(ProjectTreeEvent::WorktreeRemoved(*worktree_id));
}
_ => {}
}
}
}

View file

@ -1,241 +0,0 @@
use std::{
collections::{btree_map::Entry, BTreeMap},
ffi::OsStr,
ops::ControlFlow,
path::{Path, PathBuf},
sync::Arc,
};
/// [RootPathTrie] is a workhorse of [super::ProjectTree]. 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.
///
/// 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<Label> {
worktree_relative_path: Arc<Path>,
labels: BTreeMap<Label, LabelPresence>,
children: BTreeMap<Arc<OsStr>, RootPathTrie<Label>>,
}
/// Label presence is a marker that allows to optimize searches within [RootPathTrie]; node label can be:
/// - Present; we know there's definitely a project root at this node and it is the only label of that kind on the path to the root of a worktree
/// (none of it's ancestors or descendants can contain the same present label)
/// - Known Absent - we know there's definitely no project root at this node and none of it's ancestors are Present (descendants can be present though!).
/// - Forbidden - we know there's definitely no project root at this node and none of it's ancestors or descendants can be Present.
/// The distinction is there to optimize searching; when we encounter a node with unknown status, we don't need to look at it's full path
/// to the root of the worktree; it's sufficient to explore only the path between last node with a KnownAbsent state and the directory of a path, since we run searches
/// from the leaf up to the root of the worktree. When any of the ancestors is forbidden, we don't need to look at the node or its ancestors.
/// When there's a present labeled node on the path to the root, we don't need to ask the adapter to run the search at all.
///
/// In practical terms, it means that by storing label presence we don't need to do a project discovery on a given folder more than once
/// (unless the node is invalidated, which can happen when FS entries are renamed/removed).
///
/// Storing project absence allows us to recognize which paths have already been scanned for a project root unsuccessfully. This way we don't need to run
/// such scan more than once.
#[derive(Clone, Copy, Debug, PartialOrd, PartialEq, Ord, Eq)]
pub(super) enum LabelPresence {
KnownAbsent,
Present,
}
impl<Label: Ord + Clone> RootPathTrie<Label> {
pub(super) fn new() -> Self {
Self::new_with_key(Arc::from(Path::new("")))
}
fn new_with_key(worktree_relative_path: Arc<Path>) -> Self {
RootPathTrie {
worktree_relative_path,
labels: Default::default(),
children: Default::default(),
}
}
// Internal implementation of inner that allows one to visit descendants of insertion point for a node.
fn insert_inner(
&mut self,
path: &TriePath,
value: Label,
presence: LabelPresence,
) -> &mut Self {
let mut current = self;
let mut path_so_far = PathBuf::new();
for key in path.0.iter() {
path_so_far.push(Path::new(key));
current = match current.children.entry(key.clone()) {
Entry::Vacant(vacant_entry) => vacant_entry
.insert(RootPathTrie::new_with_key(Arc::from(path_so_far.as_path()))),
Entry::Occupied(occupied_entry) => occupied_entry.into_mut(),
};
}
let _previous_value = current.labels.insert(value, presence);
debug_assert_eq!(_previous_value, None);
current
}
pub(super) fn insert(&mut self, path: &TriePath, value: Label, presence: LabelPresence) {
self.insert_inner(path, value, presence);
}
pub(super) fn walk<'a>(
&'a self,
path: &TriePath,
callback: &mut dyn for<'b> FnMut(
&'b Arc<Path>,
&'a BTreeMap<Label, LabelPresence>,
) -> ControlFlow<()>,
) {
let mut current = self;
for key in path.0.iter() {
if !current.labels.is_empty() {
if (callback)(&current.worktree_relative_path, &current.labels).is_break() {
return;
};
}
current = match current.children.get(key) {
Some(child) => child,
None => return,
};
}
if !current.labels.is_empty() {
(callback)(&current.worktree_relative_path, &current.labels);
}
}
pub(super) fn remove(&mut self, path: &TriePath) {
debug_assert_ne!(path.0.len(), 0);
let mut current = self;
for path in path.0.iter().take(path.0.len().saturating_sub(1)) {
current = match current.children.get_mut(path) {
Some(child) => child,
None => return,
};
}
if let Some(final_entry_name) = path.0.last() {
current.children.remove(final_entry_name);
}
}
}
/// [TriePath] is a [Path] preprocessed for amortizing the cost of doing multiple lookups in distinct [RootPathTrie]s.
#[derive(Clone)]
pub(super) struct TriePath(Arc<[Arc<OsStr>]>);
impl From<&Path> for TriePath {
fn from(value: &Path) -> Self {
TriePath(value.components().map(|c| c.as_os_str().into()).collect())
}
}
#[cfg(test)]
mod tests {
use std::collections::BTreeSet;
use super::*;
#[test]
fn test_insert_and_lookup() {
let mut trie = RootPathTrie::<()>::new();
trie.insert(
&TriePath::from(Path::new("a/b/c")),
(),
LabelPresence::Present,
);
trie.walk(&TriePath::from(Path::new("a/b/c")), &mut |path, nodes| {
assert_eq!(nodes.get(&()), Some(&LabelPresence::Present));
assert_eq!(path.as_ref(), Path::new("a/b/c"));
ControlFlow::Continue(())
});
// Now let's annotate a parent with "Known missing" node.
trie.insert(
&TriePath::from(Path::new("a")),
(),
LabelPresence::KnownAbsent,
);
// Ensure that we walk from the root to the leaf.
let mut visited_paths = BTreeSet::new();
trie.walk(&TriePath::from(Path::new("a/b/c")), &mut |path, nodes| {
if path.as_ref() == Path::new("a/b/c") {
assert_eq!(
visited_paths,
BTreeSet::from_iter([Arc::from(Path::new("a/"))])
);
assert_eq!(nodes.get(&()), Some(&LabelPresence::Present));
} else if path.as_ref() == Path::new("a/") {
assert!(visited_paths.is_empty());
assert_eq!(nodes.get(&()), Some(&LabelPresence::KnownAbsent));
} else {
panic!("Unknown path");
}
// Assert that we only ever visit a path once.
assert!(visited_paths.insert(path.clone()));
ControlFlow::Continue(())
});
// One can also pass a path whose prefix is in the tree, but not that path itself.
let mut visited_paths = BTreeSet::new();
trie.walk(
&TriePath::from(Path::new("a/b/c/d/e/f/g")),
&mut |path, nodes| {
if path.as_ref() == Path::new("a/b/c") {
assert_eq!(
visited_paths,
BTreeSet::from_iter([Arc::from(Path::new("a/"))])
);
assert_eq!(nodes.get(&()), Some(&LabelPresence::Present));
} else if path.as_ref() == Path::new("a/") {
assert!(visited_paths.is_empty());
assert_eq!(nodes.get(&()), Some(&LabelPresence::KnownAbsent));
} else {
panic!("Unknown path");
}
// Assert that we only ever visit a path once.
assert!(visited_paths.insert(path.clone()));
ControlFlow::Continue(())
},
);
// Test breaking from the tree-walk.
let mut visited_paths = BTreeSet::new();
trie.walk(&TriePath::from(Path::new("a/b/c")), &mut |path, nodes| {
if path.as_ref() == Path::new("a/") {
assert!(visited_paths.is_empty());
assert_eq!(nodes.get(&()), Some(&LabelPresence::KnownAbsent));
} else {
panic!("Unknown path");
}
// Assert that we only ever visit a path once.
assert!(visited_paths.insert(path.clone()));
ControlFlow::Break(())
});
assert_eq!(visited_paths.len(), 1);
// Entry removal.
trie.insert(
&TriePath::from(Path::new("a/b")),
(),
LabelPresence::KnownAbsent,
);
let mut visited_paths = BTreeSet::new();
trie.walk(&TriePath::from(Path::new("a/b/c")), &mut |path, _nodes| {
// Assert that we only ever visit a path once.
assert!(visited_paths.insert(path.clone()));
ControlFlow::Continue(())
});
assert_eq!(visited_paths.len(), 3);
trie.remove(&TriePath::from(Path::new("a/b/")));
let mut visited_paths = BTreeSet::new();
trie.walk(&TriePath::from(Path::new("a/b/c")), &mut |path, _nodes| {
// Assert that we only ever visit a path once.
assert!(visited_paths.insert(path.clone()));
ControlFlow::Continue(())
});
assert_eq!(visited_paths.len(), 1);
assert_eq!(
visited_paths.into_iter().next().unwrap().as_ref(),
Path::new("a/")
);
}
}

View file

@ -1,461 +0,0 @@
//! 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.
//! This module defines a Project Tree.
use std::{
collections::{BTreeMap, BTreeSet},
path::Path,
sync::{Arc, Weak},
};
use collections::{HashMap, IndexMap};
use gpui::{App, AppContext, Entity, Subscription};
use itertools::Itertools;
use language::{
language_settings::AllLanguageSettings, Attach, LanguageName, LanguageRegistry,
LspAdapterDelegate,
};
use lsp::LanguageServerName;
use once_cell::sync::OnceCell;
use settings::{Settings, SettingsLocation, WorktreeId};
use util::maybe;
use crate::{project_settings::LspSettings, LanguageServerId, ProjectPath};
use super::{AdapterWrapper, ProjectTree, ProjectTreeEvent};
#[derive(Debug, Default)]
struct ServersForWorktree {
roots: BTreeMap<
Arc<Path>,
BTreeMap<LanguageServerName, (Arc<InnerTreeNode>, BTreeSet<LanguageName>)>,
>,
}
pub struct LanguageServerTree {
project_tree: Entity<ProjectTree>,
instances: BTreeMap<WorktreeId, ServersForWorktree>,
attach_kind_cache: HashMap<LanguageServerName, Attach>,
languages: Arc<LanguageRegistry>,
_subscriptions: Subscription,
}
/// 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(Weak<InnerTreeNode>);
/// Describes a request to launch a language server.
#[derive(Debug)]
pub(crate) struct LaunchDisposition<'a> {
pub(crate) server_name: &'a LanguageServerName,
pub(crate) attach: Attach,
pub(crate) path: ProjectPath,
pub(crate) settings: Arc<LspSettings>,
}
impl<'a> From<&'a InnerTreeNode> for LaunchDisposition<'a> {
fn from(value: &'a InnerTreeNode) -> Self {
LaunchDisposition {
server_name: &value.name,
attach: value.attach,
path: value.path.clone(),
settings: value.settings.clone(),
}
}
}
impl LanguageServerTreeNode {
/// Returns a language server ID for this node if there is one.
/// Returns None if this node has not been initialized yet or it is no longer in the tree.
pub(crate) fn server_id(&self) -> Option<LanguageServerId> {
self.0.upgrade()?.id.get().copied()
}
/// Returns a language server ID for this node if it has already been initialized; otherwise runs the provided closure to initialize the language server node in a tree.
/// May return None if the node no longer belongs to the server tree it was created in.
pub(crate) fn server_id_or_init(
&self,
init: impl FnOnce(LaunchDisposition) -> LanguageServerId,
) -> Option<LanguageServerId> {
self.server_id_or_try_init(|disposition| Ok(init(disposition)))
}
fn server_id_or_try_init(
&self,
init: impl FnOnce(LaunchDisposition) -> Result<LanguageServerId, ()>,
) -> Option<LanguageServerId> {
let this = self.0.upgrade()?;
this.id
.get_or_try_init(|| init(LaunchDisposition::from(&*this)))
.ok()
.copied()
}
}
impl From<Weak<InnerTreeNode>> for LanguageServerTreeNode {
fn from(weak: Weak<InnerTreeNode>) -> Self {
LanguageServerTreeNode(weak)
}
}
#[derive(Debug)]
struct InnerTreeNode {
id: OnceCell<LanguageServerId>,
name: LanguageServerName,
attach: Attach,
path: ProjectPath,
settings: Arc<LspSettings>,
}
impl InnerTreeNode {
fn new(
name: LanguageServerName,
attach: Attach,
path: ProjectPath,
settings: impl Into<Arc<LspSettings>>,
) -> Self {
InnerTreeNode {
id: Default::default(),
name,
attach,
path,
settings: settings.into(),
}
}
}
/// Determines how the list of adapters to query should be constructed.
pub(crate) enum AdapterQuery<'a> {
/// Search for roots of all adapters associated with a given language name.
Language(&'a LanguageName),
/// Search for roots of adapter with a given name.
Adapter(&'a LanguageServerName),
}
impl LanguageServerTree {
pub(crate) fn new(
project_tree: Entity<ProjectTree>,
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,
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>(
&'a mut self,
path: ProjectPath,
query: AdapterQuery<'_>,
delegate: Arc<dyn LspAdapterDelegate>,
cx: &mut App,
) -> impl Iterator<Item = LanguageServerTreeNode> + 'a {
let settings_location = SettingsLocation {
worktree_id: path.worktree_id,
path: &path.path,
};
let adapters = match query {
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()))),
),
};
self.get_with_adapters(path, adapters, delegate, cx)
}
fn get_with_adapters<'a>(
&'a mut self,
path: ProjectPath,
adapters: IndexMap<AdapterWrapper, (LspSettings, BTreeSet<LanguageName>)>,
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| {
this.root_for_path(
path,
adapters
.iter()
.map(|(adapter, _)| adapter.0.clone())
.collect(),
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
.into_iter()
.filter_map(move |(adapter, root_path)| {
let attach = self.attach_kind(&adapter);
let (index, _, (settings, new_languages)) = adapters.get_full(&adapter)?;
let inner_node = self
.instances
.entry(root_path.worktree_id)
.or_default()
.roots
.entry(root_path.path.clone())
.or_default()
.entry(adapter.0.name.clone());
let (node, languages) = inner_node.or_insert_with(move || {
(
Arc::new(InnerTreeNode::new(
adapter.0.name(),
attach,
root_path,
settings.clone(),
)),
Default::default(),
)
});
languages.extend(new_languages.iter().cloned());
Some((index, 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 adapters_for_language(
&self,
settings_location: SettingsLocation,
language_name: &LanguageName,
cx: &App,
) -> IndexMap<AdapterWrapper, (LspSettings, BTreeSet<LanguageName>)> {
let settings = AllLanguageSettings::get(Some(settings_location), cx).language(
Some(settings_location),
Some(language_name),
cx,
);
if !settings.enable_language_server {
return Default::default();
}
let available_lsp_adapters = self.languages.lsp_adapters(&language_name);
let available_language_servers = available_lsp_adapters
.iter()
.map(|lsp_adapter| lsp_adapter.name.clone())
.collect::<Vec<_>>();
let desired_language_servers =
settings.customized_language_servers(&available_language_servers);
let adapters_with_settings = desired_language_servers
.into_iter()
.filter_map(|desired_adapter| {
let adapter = if let Some(adapter) = available_lsp_adapters
.iter()
.find(|adapter| adapter.name == desired_adapter)
{
Some(adapter.clone())
} else if let Some(adapter) =
self.languages.load_available_lsp_adapter(&desired_adapter)
{
self.languages
.register_lsp_adapter(language_name.clone(), adapter.adapter.clone());
Some(adapter)
} else {
None
}?;
let adapter_settings = crate::lsp_store::language_server_settings_for(
settings_location,
&adapter.name,
cx,
)
.cloned()
.unwrap_or_default();
Some((
AdapterWrapper(adapter),
(
adapter_settings,
BTreeSet::from_iter([language_name.clone()]),
),
))
})
.collect::<IndexMap<_, _>>();
// After starting all the language servers, reorder them to reflect the desired order
// based on the settings.
//
// This is done, in part, to ensure that language servers loaded at different points
// (e.g., native vs extension) still end up in the right order at the end, rather than
// it being based on which language server happened to be loaded in first.
self.languages.reorder_language_servers(
&language_name,
adapters_with_settings
.keys()
.map(|wrapper| wrapper.0.clone())
.collect(),
);
adapters_with_settings
}
pub(crate) fn on_settings_changed(
&mut self,
get_delegate: &mut dyn FnMut(WorktreeId, &mut App) -> Option<Arc<dyn LspAdapterDelegate>>,
spawn_language_server: &mut dyn FnMut(LaunchDisposition, &mut App) -> LanguageServerId,
on_language_server_removed: &mut dyn FnMut(LanguageServerId),
cx: &mut App,
) {
// Settings are checked at query time. Thus, to avoid messing with inference of applicable settings, we're just going to clear ourselves and let the next query repopulate.
// We're going to optimistically re-run the queries and re-assign the same language server id when a language server still exists at a given tree node.
let old_instances = std::mem::take(&mut self.instances);
let old_attach_kinds = std::mem::take(&mut self.attach_kind_cache);
let mut referenced_instances = BTreeSet::new();
// Re-map the old tree onto a new one. In the process we'll get a list of servers we have to shut down.
let mut all_instances = BTreeSet::new();
for (worktree_id, servers) in &old_instances {
// Record all initialized node ids.
all_instances.extend(servers.roots.values().flat_map(|servers_at_node| {
servers_at_node
.values()
.filter_map(|(server_node, _)| server_node.id.get().copied())
}));
let Some(delegate) = get_delegate(*worktree_id, cx) else {
// If worktree is no longer around, we're just going to shut down all of the language servers (since they've been added to all_instances).
continue;
};
for (path, servers_for_path) in &servers.roots {
for (server_name, (_, languages)) in servers_for_path {
let settings_location = SettingsLocation {
worktree_id: *worktree_id,
path: &path,
};
// Verify which of the previous languages still have this server enabled.
let mut adapter_with_settings = IndexMap::default();
for language_name in languages {
self.adapters_for_language(settings_location, language_name, cx)
.into_iter()
.for_each(|(lsp_adapter, lsp_settings)| {
if &lsp_adapter.0.name() != server_name {
return;
}
adapter_with_settings
.entry(lsp_adapter)
.and_modify(|x: &mut (_, BTreeSet<LanguageName>)| {
x.1.extend(lsp_settings.1.clone())
})
.or_insert(lsp_settings);
});
}
if adapter_with_settings.is_empty() {
// Since all languages that have had this server enabled are now disabled, we can remove the server entirely.
continue;
};
for new_node in self.get_with_adapters(
ProjectPath {
path: path.clone(),
worktree_id: *worktree_id,
},
adapter_with_settings,
delegate.clone(),
cx,
) {
new_node.server_id_or_try_init(|disposition| {
let Some((existing_node, _)) = servers
.roots
.get(&disposition.path.path)
.and_then(|roots| roots.get(disposition.server_name))
.filter(|(old_node, _)| {
old_attach_kinds.get(disposition.server_name).map_or(
false,
|old_attach| {
disposition.attach == *old_attach
&& disposition.settings == old_node.settings
},
)
})
else {
return Ok(spawn_language_server(disposition, cx));
};
if let Some(id) = existing_node.id.get().copied() {
// If we have a node with ID assigned (and it's parameters match `disposition`), reuse the id.
referenced_instances.insert(id);
Ok(id)
} else {
// Otherwise, if we do have a node but it does not have an ID assigned, keep it that way.
Err(())
}
});
}
}
}
}
for server_to_remove in all_instances.difference(&referenced_instances) {
on_language_server_removed(*server_to_remove);
}
}
/// Updates nodes in language server tree in place, changing the ID of initialized nodes.
pub(crate) fn restart_language_servers(
&mut self,
worktree_id: WorktreeId,
ids: BTreeSet<LanguageServerId>,
restart_callback: &mut dyn FnMut(LanguageServerId, LaunchDisposition) -> LanguageServerId,
) {
maybe! {{
for (_, nodes) in &mut self.instances.get_mut(&worktree_id)?.roots {
for (_, (node, _)) in nodes {
let Some(old_server_id) = node.id.get().copied() else {
continue;
};
if !ids.contains(&old_server_id) {
continue;
}
let new_id = restart_callback(old_server_id, LaunchDisposition::from(&**node));
*node = Arc::new(InnerTreeNode::new(node.name.clone(), node.attach, node.path.clone(), node.settings.clone()));
node.id.set(new_id).expect("The id to be unset after clearing the node.");
}
}
Some(())
}
};
}
}