Cargo.lock 🔗
@@ -7396,6 +7396,7 @@ dependencies = [
"serde",
"serde_json",
"smol",
+ "text",
"util",
]
@@ -9832,6 +9833,7 @@ dependencies = [
"log",
"lsp",
"node_runtime",
+ "once_cell",
"parking_lot",
"pathdiff",
"paths",
Piotr Osiewicz and João created
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>
Cargo.lock | 2
Cargo.toml | 7
crates/collab/Cargo.toml | 4
crates/copilot/src/copilot.rs | 6
crates/editor/src/editor.rs | 80 +-
crates/editor/src/editor_tests.rs | 7
crates/editor/src/lsp_ext.rs | 37
crates/editor/src/proposed_changes_editor.rs | 2
crates/gpui_macros/Cargo.toml | 2
crates/language/src/language.rs | 49 +
crates/language/src/language_registry.rs | 41
crates/language_tools/src/lsp_log.rs | 13
crates/languages/src/rust.rs | 16
crates/lsp/Cargo.toml | 1
crates/lsp/src/lsp.rs | 190 ++++-
crates/prettier/src/prettier.rs | 10
crates/project/Cargo.toml | 1
crates/project/src/lsp_command.rs | 8
crates/project/src/lsp_store.rs | 647 ++++++++++---------
crates/project/src/prettier_store.rs | 2
crates/project/src/project.rs | 35
crates/project/src/project_tests.rs | 29
crates/project/src/project_tree.rs | 243 +++++++
crates/project/src/project_tree/path_trie.rs | 241 +++++++
crates/project/src/project_tree/server_tree.rs | 428 +++++++++++++
crates/proto/proto/zed.proto | 1
crates/refineable/derive_refineable/Cargo.toml | 2
crates/ui_macros/Cargo.toml | 2
crates/zed/src/zed/quick_action_bar.rs | 46
29 files changed, 1,656 insertions(+), 496 deletions(-)
@@ -7396,6 +7396,7 @@ dependencies = [
"serde",
"serde_json",
"smol",
+ "text",
"util",
]
@@ -9832,6 +9833,7 @@ dependencies = [
"log",
"lsp",
"node_runtime",
+ "once_cell",
"parking_lot",
"pathdiff",
"paths",
@@ -372,7 +372,7 @@ async-tungstenite = "0.28"
async-watch = "0.3.1"
async_zip = { version = "0.0.17", features = ["deflate", "deflate64"] }
base64 = "0.22"
-bitflags = "2.6.0"
+bitflags = "2.8.0"
blade-graphics = { git = "https://github.com/kvark/blade", rev = "091a8401033847bb9b6ace3fcf70448d069621c5" }
blade-macros = { git = "https://github.com/kvark/blade", rev = "091a8401033847bb9b6ace3fcf70448d069621c5" }
blade-util = { git = "https://github.com/kvark/blade", rev = "091a8401033847bb9b6ace3fcf70448d069621c5" }
@@ -420,12 +420,13 @@ libc = "0.2"
libsqlite3-sys = { version = "0.30.1", features = ["bundled"] }
linkify = "0.10.0"
livekit = { git = "https://github.com/zed-industries/livekit-rust-sdks", rev="060964da10574cd9bf06463a53bf6e0769c5c45e", features = ["dispatcher", "services-dispatcher", "rustls-tls-native-roots"], default-features = false }
-log = { version = "0.4.16", features = ["kv_unstable_serde", "serde"] }
+log = { version = "0.4.25", features = ["kv_unstable_serde", "serde"] }
markup5ever_rcdom = "0.3.0"
nanoid = "0.4"
nbformat = { version = "0.10.0" }
nix = "0.29"
num-format = "0.4.4"
+once_cell = "1.20"
ordered-float = "2.1.1"
palette = { version = "0.7.5", default-features = false, features = ["std"] }
parking_lot = "0.12.1"
@@ -508,7 +509,7 @@ tree-sitter = { version = "0.23", features = ["wasm"] }
tree-sitter-bash = "0.23"
tree-sitter-c = "0.23"
tree-sitter-cpp = "0.23"
-tree-sitter-css = "0.23"
+tree-sitter-css = "0.23.2"
tree-sitter-elixir = "0.3"
tree-sitter-embedded-template = "0.23.0"
tree-sitter-go = "0.23"
@@ -53,7 +53,7 @@ reqwest_client.workspace = true
rpc.workspace = true
rustc-demangle.workspace = true
scrypt = "0.11"
-sea-orm = { version = "1.1.0-rc.1", features = ["sqlx-postgres", "postgres-array", "runtime-tokio-rustls", "with-uuid"] }
+sea-orm = { version = "1.1.4", features = ["sqlx-postgres", "postgres-array", "runtime-tokio-rustls", "with-uuid"] }
semantic_version.workspace = true
semver.workspace = true
serde.workspace = true
@@ -116,7 +116,7 @@ release_channel.workspace = true
remote = { workspace = true, features = ["test-support"] }
remote_server.workspace = true
rpc = { workspace = true, features = ["test-support"] }
-sea-orm = { version = "1.1.0-rc.1", features = ["sqlx-sqlite"] }
+sea-orm = { version = "1.1.4", features = ["sqlx-sqlite"] }
serde_json.workspace = true
session = { workspace = true, features = ["test-support"] }
settings = { workspace = true, features = ["test-support"] }
@@ -461,12 +461,14 @@ impl Copilot {
.on_notification::<StatusNotification, _>(|_, _| { /* Silence the notification */ })
.detach();
- let initialize_params = None;
let configuration = lsp::DidChangeConfigurationParams {
settings: Default::default(),
};
let server = cx
- .update(|cx| server.initialize(initialize_params, configuration.into(), cx))?
+ .update(|cx| {
+ let params = server.default_initialize_params(cx);
+ server.initialize(params, configuration.into(), cx)
+ })?
.await?;
let status = server
@@ -12476,28 +12476,27 @@ impl Editor {
cx.emit(SearchEvent::MatchesInvalidated);
if *singleton_buffer_edited {
if let Some(project) = &self.project {
- let project = project.read(cx);
#[allow(clippy::mutable_key_type)]
- let languages_affected = multibuffer
- .read(cx)
- .all_buffers()
- .into_iter()
- .filter_map(|buffer| {
- let buffer = buffer.read(cx);
- let language = buffer.language()?;
- if project.is_local()
- && project
- .language_servers_for_local_buffer(buffer, cx)
- .count()
- == 0
- {
- None
- } else {
- Some(language)
- }
- })
- .cloned()
- .collect::<HashSet<_>>();
+ let languages_affected = multibuffer.update(cx, |multibuffer, cx| {
+ multibuffer
+ .all_buffers()
+ .into_iter()
+ .filter_map(|buffer| {
+ buffer.update(cx, |buffer, cx| {
+ let language = buffer.language()?;
+ let should_discard = project.update(cx, |project, cx| {
+ project.is_local()
+ && project.for_language_servers_for_local_buffer(
+ buffer,
+ |it| it.count() == 0,
+ cx,
+ )
+ });
+ should_discard.not().then_some(language.clone())
+ })
+ })
+ .collect::<HashSet<_>>()
+ });
if !languages_affected.is_empty() {
self.refresh_inlay_hints(
InlayHintRefreshReason::BufferEdited(languages_affected),
@@ -13051,15 +13050,18 @@ impl Editor {
self.handle_input(text, cx);
}
- pub fn supports_inlay_hints(&self, cx: &AppContext) -> bool {
+ pub fn supports_inlay_hints(&self, cx: &mut AppContext) -> bool {
let Some(provider) = self.semantics_provider.as_ref() else {
return false;
};
let mut supports = false;
- self.buffer().read(cx).for_each_buffer(|buffer| {
- supports |= provider.supports_inlay_hints(buffer, cx);
+ self.buffer().update(cx, |this, cx| {
+ this.for_each_buffer(|buffer| {
+ supports |= provider.supports_inlay_hints(buffer, cx);
+ })
});
+
supports
}
@@ -13671,7 +13673,7 @@ pub trait SemanticsProvider {
cx: &mut AppContext,
) -> Option<Task<anyhow::Result<InlayHint>>>;
- fn supports_inlay_hints(&self, buffer: &Model<Buffer>, cx: &AppContext) -> bool;
+ fn supports_inlay_hints(&self, buffer: &Model<Buffer>, cx: &mut AppContext) -> bool;
fn document_highlights(
&self,
@@ -14056,17 +14058,25 @@ impl SemanticsProvider for Model<Project> {
}))
}
- fn supports_inlay_hints(&self, buffer: &Model<Buffer>, cx: &AppContext) -> bool {
+ fn supports_inlay_hints(&self, buffer: &Model<Buffer>, cx: &mut AppContext) -> bool {
// TODO: make this work for remote projects
- self.read(cx)
- .language_servers_for_local_buffer(buffer.read(cx), cx)
- .any(
- |(_, server)| match server.capabilities().inlay_hint_provider {
- Some(lsp::OneOf::Left(enabled)) => enabled,
- Some(lsp::OneOf::Right(_)) => true,
- None => false,
- },
- )
+ buffer.update(cx, |buffer, cx| {
+ self.update(cx, |this, cx| {
+ this.for_language_servers_for_local_buffer(
+ buffer,
+ |mut it| {
+ it.any(
+ |(_, server)| match server.capabilities().inlay_hint_provider {
+ Some(lsp::OneOf::Left(enabled)) => enabled,
+ Some(lsp::OneOf::Right(_)) => true,
+ None => false,
+ },
+ )
+ },
+ cx,
+ )
+ })
+ })
}
fn inlay_hints(
@@ -6839,7 +6839,7 @@ async fn test_document_format_during_save(cx: &mut gpui::TestAppContext) {
let fs = FakeFs::new(cx.executor());
fs.insert_file("/file.rs", Default::default()).await;
- let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
+ let project = Project::test(fs, ["/".as_ref()], cx).await;
let language_registry = project.read_with(cx, |project, _| project.languages().clone());
language_registry.add(rust_lang());
@@ -7193,7 +7193,7 @@ async fn test_range_format_during_save(cx: &mut gpui::TestAppContext) {
let fs = FakeFs::new(cx.executor());
fs.insert_file("/file.rs", Default::default()).await;
- let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
+ let project = Project::test(fs, ["/".as_ref()], cx).await;
let language_registry = project.read_with(cx, |project, _| project.languages().clone());
language_registry.add(rust_lang());
@@ -7327,7 +7327,7 @@ async fn test_document_format_manual_trigger(cx: &mut gpui::TestAppContext) {
let fs = FakeFs::new(cx.executor());
fs.insert_file("/file.rs", Default::default()).await;
- let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
+ let project = Project::test(fs, ["/".as_ref()], cx).await;
let language_registry = project.read_with(cx, |project, _| project.languages().clone());
language_registry.add(Arc::new(Language::new(
@@ -10742,7 +10742,6 @@ async fn test_language_server_restart_due_to_settings_change(cx: &mut gpui::Test
0,
"Should not restart LSP server on an unrelated LSP settings change"
);
-
update_test_project_settings(cx, |project_settings| {
project_settings.lsp.insert(
language_server_name.into(),
@@ -11,7 +11,7 @@ use multi_buffer::Anchor;
pub(crate) fn find_specific_language_server_in_selection<F>(
editor: &Editor,
- cx: &WindowContext,
+ cx: &mut WindowContext,
filter_language: F,
language_server_name: &str,
) -> Option<(Anchor, Arc<Language>, LanguageServerId, Model<Buffer>)>
@@ -21,7 +21,6 @@ where
let Some(project) = &editor.project else {
return None;
};
- let multibuffer = editor.buffer().read(cx);
let mut language_servers_for = HashMap::default();
editor
.selections
@@ -29,29 +28,33 @@ where
.iter()
.filter(|selection| selection.start == selection.end)
.filter_map(|selection| Some((selection.start.buffer_id?, selection.start)))
- .filter_map(|(buffer_id, trigger_anchor)| {
- let buffer = multibuffer.buffer(buffer_id)?;
+ .find_map(|(buffer_id, trigger_anchor)| {
+ let buffer = editor.buffer().read(cx).buffer(buffer_id)?;
let server_id = *match language_servers_for.entry(buffer_id) {
Entry::Occupied(occupied_entry) => occupied_entry.into_mut(),
Entry::Vacant(vacant_entry) => {
- let language_server_id = project
- .read(cx)
- .language_servers_for_local_buffer(buffer.read(cx), cx)
- .find_map(|(adapter, server)| {
- if adapter.name.0.as_ref() == language_server_name {
- Some(server.server_id())
- } else {
- None
- }
- });
+ let language_server_id = buffer.update(cx, |buffer, cx| {
+ project.update(cx, |project, cx| {
+ project.for_language_servers_for_local_buffer(
+ buffer,
+ |mut it| {
+ it.find_map(|(adapter, server)| {
+ if adapter.name.0.as_ref() == language_server_name {
+ Some(server.server_id())
+ } else {
+ None
+ }
+ })
+ },
+ cx,
+ )
+ })
+ });
vacant_entry.insert(language_server_id)
}
}
.as_ref()?;
- Some((buffer, trigger_anchor, server_id))
- })
- .find_map(|(buffer, trigger_anchor, server_id)| {
let language = buffer.read(cx).language_at(trigger_anchor.text_anchor)?;
if !filter_language(&language) {
return None;
@@ -455,7 +455,7 @@ impl SemanticsProvider for BranchBufferSemanticsProvider {
self.0.resolve_inlay_hint(hint, buffer, server_id, cx)
}
- fn supports_inlay_hints(&self, buffer: &Model<Buffer>, cx: &AppContext) -> bool {
+ fn supports_inlay_hints(&self, buffer: &Model<Buffer>, cx: &mut AppContext) -> bool {
if let Some(buffer) = self.to_base(&buffer, &[], cx) {
self.0.supports_inlay_hints(&buffer, cx)
} else {
@@ -14,6 +14,6 @@ proc-macro = true
doctest = false
[dependencies]
-proc-macro2 = "1.0.66"
+proc-macro2 = "1.0.93"
quote = "1.0.9"
syn = { version = "1.0.72", features = ["full", "extra-traits"] }
@@ -45,7 +45,6 @@ use serde::{de, Deserialize, Deserializer, Serialize, Serializer};
use serde_json::Value;
use settings::WorktreeId;
use smol::future::FutureExt as _;
-use std::num::NonZeroU32;
use std::{
any::Any,
ffi::OsStr,
@@ -61,6 +60,7 @@ use std::{
Arc, LazyLock,
},
};
+use std::{num::NonZeroU32, sync::OnceLock};
use syntax_map::{QueryCursorHandle, SyntaxSnapshot};
use task::RunnableTag;
pub use task_context::{ContextProvider, RunnableRange};
@@ -163,6 +163,7 @@ pub struct CachedLspAdapter {
pub adapter: Arc<dyn LspAdapter>,
pub reinstall_attempt_count: AtomicU64,
cached_binary: futures::lock::Mutex<Option<LanguageServerBinary>>,
+ attach_kind: OnceLock<Attach>,
}
impl Debug for CachedLspAdapter {
@@ -198,6 +199,7 @@ impl CachedLspAdapter {
adapter,
cached_binary: Default::default(),
reinstall_attempt_count: AtomicU64::new(0),
+ attach_kind: Default::default(),
})
}
@@ -259,6 +261,38 @@ impl CachedLspAdapter {
.cloned()
.unwrap_or_else(|| language_name.lsp_id())
}
+ pub fn find_project_root(
+ &self,
+ path: &Path,
+ ancestor_depth: usize,
+ delegate: &Arc<dyn LspAdapterDelegate>,
+ ) -> Option<Arc<Path>> {
+ self.adapter
+ .find_project_root(path, ancestor_depth, delegate)
+ }
+ pub fn attach_kind(&self) -> Attach {
+ *self.attach_kind.get_or_init(|| self.adapter.attach_kind())
+ }
+}
+
+#[derive(Clone, Copy, Debug, 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<Path>),
+ ) -> (WorktreeId, Arc<Path>) {
+ 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
@@ -505,6 +539,19 @@ pub trait LspAdapter: 'static + Send + Sync {
fn prepare_initialize_params(&self, original: InitializeParams) -> Result<InitializeParams> {
Ok(original)
}
+ fn attach_kind(&self) -> Attach {
+ Attach::Shared
+ }
+ fn find_project_root(
+ &self,
+
+ _path: &Path,
+ _ancestor_depth: usize,
+ _: &Arc<dyn LspAdapterDelegate>,
+ ) -> Option<Arc<Path>> {
+ // By default all language servers are rooted at the root of the worktree.
+ Some(Arc::from("".as_ref()))
+ }
}
async fn try_fetch_server_binary<L: LspAdapter + 'static + Send + Sync + ?Sized>(
@@ -96,6 +96,7 @@ struct LanguageRegistryState {
available_languages: Vec<AvailableLanguage>,
grammars: HashMap<Arc<str>, AvailableGrammar>,
lsp_adapters: HashMap<LanguageName, Vec<Arc<CachedLspAdapter>>>,
+ all_lsp_adapters: HashMap<LanguageServerName, Arc<CachedLspAdapter>>,
available_lsp_adapters:
HashMap<LanguageServerName, Arc<dyn Fn() -> Arc<CachedLspAdapter> + 'static + Send + Sync>>,
loading_languages: HashMap<LanguageId, Vec<oneshot::Sender<Result<Arc<Language>>>>>,
@@ -222,6 +223,7 @@ impl LanguageRegistry {
language_settings: Default::default(),
loading_languages: Default::default(),
lsp_adapters: Default::default(),
+ all_lsp_adapters: Default::default(),
available_lsp_adapters: HashMap::default(),
subscription: watch::channel(),
theme: Default::default(),
@@ -344,12 +346,16 @@ impl LanguageRegistry {
adapter: Arc<dyn LspAdapter>,
) -> Arc<CachedLspAdapter> {
let cached = CachedLspAdapter::new(adapter);
- self.state
- .write()
+ let mut state = self.state.write();
+ state
.lsp_adapters
.entry(language_name)
.or_default()
.push(cached.clone());
+ state
+ .all_lsp_adapters
+ .insert(cached.name.clone(), cached.clone());
+
cached
}
@@ -389,12 +395,17 @@ impl LanguageRegistry {
let adapter_name = LanguageServerName(adapter.name.into());
let capabilities = adapter.capabilities.clone();
let initializer = adapter.initializer.take();
- self.state
- .write()
- .lsp_adapters
- .entry(language_name.clone())
- .or_default()
- .push(CachedLspAdapter::new(Arc::new(adapter)));
+ let adapter = CachedLspAdapter::new(Arc::new(adapter));
+ {
+ let mut state = self.state.write();
+ state
+ .lsp_adapters
+ .entry(language_name.clone())
+ .or_default()
+ .push(adapter.clone());
+ state.all_lsp_adapters.insert(adapter.name(), adapter);
+ }
+
self.register_fake_language_server(adapter_name, capabilities, initializer)
}
@@ -407,12 +418,16 @@ impl LanguageRegistry {
adapter: crate::FakeLspAdapter,
) {
let language_name = language_name.into();
- self.state
- .write()
+ let mut state = self.state.write();
+ let cached_adapter = CachedLspAdapter::new(Arc::new(adapter));
+ state
.lsp_adapters
.entry(language_name.clone())
.or_default()
- .push(CachedLspAdapter::new(Arc::new(adapter)));
+ .push(cached_adapter.clone());
+ state
+ .all_lsp_adapters
+ .insert(cached_adapter.name(), cached_adapter);
}
/// Register a fake language server (without the adapter)
@@ -880,6 +895,10 @@ impl LanguageRegistry {
.unwrap_or_default()
}
+ pub fn adapter_for_name(&self, name: &LanguageServerName) -> Option<Arc<CachedLspAdapter>> {
+ self.state.read().all_lsp_adapters.get(name).cloned()
+ }
+
pub fn update_lsp_status(
&self,
server_name: LanguageServerName,
@@ -730,7 +730,8 @@ impl LspLogView {
* Binary: {BINARY:#?}
-* Running in project: {PATH:?}
+* Registered workspace folders:
+{WORKSPACE_FOLDERS}
* Capabilities: {CAPABILITIES}
@@ -738,7 +739,15 @@ impl LspLogView {
NAME = server.name(),
ID = server.server_id(),
BINARY = server.binary(),
- PATH = server.root_path(),
+ WORKSPACE_FOLDERS = server
+ .workspace_folders()
+ .iter()
+ .filter_map(|path| path
+ .to_file_path()
+ .ok()
+ .map(|path| path.to_string_lossy().into_owned()))
+ .collect::<Vec<_>>()
+ .join(", "),
CAPABILITIES = serde_json::to_string_pretty(&server.capabilities())
.unwrap_or_else(|e| format!("Failed to serialize capabilities: {e}")),
CONFIGURATION = serde_json::to_string_pretty(server.configuration())
@@ -74,6 +74,22 @@ impl LspAdapter for RustLspAdapter {
Self::SERVER_NAME.clone()
}
+ fn find_project_root(
+ &self,
+ path: &Path,
+ ancestor_depth: usize,
+ delegate: &Arc<dyn LspAdapterDelegate>,
+ ) -> Option<Arc<Path>> {
+ let mut outermost_cargo_toml = None;
+ for path in path.ancestors().take(ancestor_depth) {
+ let p = path.join("Cargo.toml").to_path_buf();
+ if smol::block_on(delegate.read_text_file(p)).is_ok() {
+ outermost_cargo_toml = Some(Arc::from(path));
+ }
+ }
+
+ outermost_cargo_toml
+ }
async fn check_if_user_installed(
&self,
delegate: &dyn LspAdapterDelegate,
@@ -29,6 +29,7 @@ serde.workspace = true
serde_json.workspace = true
schemars.workspace = true
smol.workspace = true
+text.workspace = true
util.workspace = true
release_channel.workspace = true
@@ -7,6 +7,7 @@ use anyhow::{anyhow, Context, Result};
use collections::HashMap;
use futures::{channel::oneshot, io::BufWriter, select, AsyncRead, AsyncWrite, Future, FutureExt};
use gpui::{AppContext, AsyncAppContext, BackgroundExecutor, SharedString, Task};
+use notification::DidChangeWorkspaceFolders;
use parking_lot::{Mutex, RwLock};
use postage::{barrier, prelude::Stream};
use schemars::{
@@ -21,12 +22,14 @@ use smol::{
io::{AsyncBufReadExt, AsyncWriteExt, BufReader},
process::Child,
};
+use text::BufferId;
use std::{
+ collections::BTreeSet,
ffi::{OsStr, OsString},
fmt,
io::Write,
- ops::DerefMut,
+ ops::{Deref, DerefMut},
path::PathBuf,
pin::Pin,
sync::{
@@ -96,9 +99,9 @@ pub struct LanguageServer {
#[allow(clippy::type_complexity)]
io_tasks: Mutex<Option<(Task<Option<()>>, Task<Option<()>>)>>,
output_done_rx: Mutex<Option<barrier::Receiver>>,
- root_path: PathBuf,
- working_dir: PathBuf,
server: Arc<Mutex<Option<Child>>>,
+ workspace_folders: Arc<Mutex<BTreeSet<Url>>>,
+ registered_buffers: Arc<Mutex<HashMap<BufferId, Url>>>,
}
/// Identifies a running language server.
@@ -376,8 +379,6 @@ impl LanguageServer {
Some(stderr),
stderr_capture,
Some(server),
- root_path,
- working_dir,
code_action_kinds,
binary,
cx,
@@ -403,8 +404,6 @@ impl LanguageServer {
stderr: Option<Stderr>,
stderr_capture: Arc<Mutex<Option<String>>>,
server: Option<Child>,
- root_path: &Path,
- working_dir: &Path,
code_action_kinds: Option<Vec<CodeActionKind>>,
binary: LanguageServerBinary,
cx: AsyncAppContext,
@@ -488,9 +487,9 @@ impl LanguageServer {
executor: cx.background_executor().clone(),
io_tasks: Mutex::new(Some((input_task, output_task))),
output_done_rx: Mutex::new(Some(output_done_rx)),
- root_path: root_path.to_path_buf(),
- working_dir: working_dir.to_path_buf(),
server: Arc::new(Mutex::new(server)),
+ workspace_folders: Default::default(),
+ registered_buffers: Default::default(),
}
}
@@ -615,12 +614,11 @@ impl LanguageServer {
}
pub fn default_initialize_params(&self, cx: &AppContext) -> InitializeParams {
- let root_uri = Url::from_file_path(&self.working_dir).unwrap();
#[allow(deprecated)]
InitializeParams {
process_id: None,
root_path: None,
- root_uri: Some(root_uri.clone()),
+ root_uri: None,
initialization_options: None,
capabilities: ClientCapabilities {
general: Some(GeneralClientCapabilities {
@@ -787,10 +785,7 @@ impl LanguageServer {
}),
},
trace: None,
- workspace_folders: Some(vec![WorkspaceFolder {
- uri: root_uri,
- name: Default::default(),
- }]),
+ workspace_folders: None,
client_info: release_channel::ReleaseChannel::try_global(cx).map(|release_channel| {
ClientInfo {
name: release_channel.display_name().to_string(),
@@ -809,16 +804,10 @@ impl LanguageServer {
/// [LSP Specification](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#initialize)
pub fn initialize(
mut self,
- initialize_params: Option<InitializeParams>,
+ params: InitializeParams,
configuration: Arc<DidChangeConfigurationParams>,
cx: &AppContext,
) -> Task<Result<Arc<Self>>> {
- let params = if let Some(params) = initialize_params {
- params
- } else {
- self.default_initialize_params(cx)
- };
-
cx.spawn(|_| async move {
let response = self.request::<request::Initialize>(params).await?;
if let Some(info) = response.server_info {
@@ -1070,16 +1059,10 @@ impl LanguageServer {
self.server_id
}
- /// Get the root path of the project the language server is running against.
- pub fn root_path(&self) -> &PathBuf {
- &self.root_path
- }
-
/// Language server's binary information.
pub fn binary(&self) -> &LanguageServerBinary {
&self.binary
}
-
/// Sends a RPC request to the language server.
///
/// [LSP Specification](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#requestMessage)
@@ -1207,6 +1190,129 @@ impl LanguageServer {
outbound_tx.try_send(message)?;
Ok(())
}
+
+ /// Add new workspace folder to the list.
+ pub fn add_workspace_folder(&self, uri: Url) {
+ if self
+ .capabilities()
+ .workspace
+ .and_then(|ws| {
+ ws.workspace_folders.and_then(|folders| {
+ folders
+ .change_notifications
+ .map(|caps| matches!(caps, OneOf::Left(false)))
+ })
+ })
+ .unwrap_or(true)
+ {
+ return;
+ }
+
+ let is_new_folder = self.workspace_folders.lock().insert(uri.clone());
+ if is_new_folder {
+ let params = DidChangeWorkspaceFoldersParams {
+ event: WorkspaceFoldersChangeEvent {
+ added: vec![WorkspaceFolder {
+ uri,
+ name: String::default(),
+ }],
+ removed: vec![],
+ },
+ };
+ self.notify::<DidChangeWorkspaceFolders>(¶ms).log_err();
+ }
+ }
+ /// Add new workspace folder to the list.
+ pub fn remove_workspace_folder(&self, uri: Url) {
+ if self
+ .capabilities()
+ .workspace
+ .and_then(|ws| {
+ ws.workspace_folders.and_then(|folders| {
+ folders
+ .change_notifications
+ .map(|caps| !matches!(caps, OneOf::Left(false)))
+ })
+ })
+ .unwrap_or(true)
+ {
+ return;
+ }
+ let was_removed = self.workspace_folders.lock().remove(&uri);
+ if was_removed {
+ let params = DidChangeWorkspaceFoldersParams {
+ event: WorkspaceFoldersChangeEvent {
+ added: vec![],
+ removed: vec![WorkspaceFolder {
+ uri,
+ name: String::default(),
+ }],
+ },
+ };
+ self.notify::<DidChangeWorkspaceFolders>(¶ms).log_err();
+ }
+ }
+ pub fn set_workspace_folders(&self, folders: BTreeSet<Url>) {
+ let mut workspace_folders = self.workspace_folders.lock();
+ let added: Vec<_> = folders
+ .iter()
+ .map(|uri| WorkspaceFolder {
+ uri: uri.clone(),
+ name: String::default(),
+ })
+ .collect();
+
+ let removed: Vec<_> = std::mem::replace(&mut *workspace_folders, folders)
+ .into_iter()
+ .map(|uri| WorkspaceFolder {
+ uri: uri.clone(),
+ name: String::default(),
+ })
+ .collect();
+ let should_notify = !added.is_empty() || !removed.is_empty();
+
+ if should_notify {
+ let params = DidChangeWorkspaceFoldersParams {
+ event: WorkspaceFoldersChangeEvent { added, removed },
+ };
+ self.notify::<DidChangeWorkspaceFolders>(¶ms).log_err();
+ }
+ }
+
+ pub fn workspace_folders(&self) -> impl Deref<Target = BTreeSet<Url>> + '_ {
+ self.workspace_folders.lock()
+ }
+
+ pub fn register_buffer(
+ &self,
+ buffer_id: BufferId,
+ uri: Url,
+ language_id: String,
+ version: i32,
+ initial_text: String,
+ ) {
+ let previous_value = self
+ .registered_buffers
+ .lock()
+ .insert(buffer_id, uri.clone());
+ if previous_value.is_none() {
+ self.notify::<notification::DidOpenTextDocument>(&DidOpenTextDocumentParams {
+ text_document: TextDocumentItem::new(uri, language_id, version, initial_text),
+ })
+ .log_err();
+ } else {
+ debug_assert_eq!(previous_value, Some(uri));
+ }
+ }
+
+ pub fn unregister_buffer(&self, buffer_id: BufferId) {
+ if let Some(path) = self.registered_buffers.lock().remove(&buffer_id) {
+ self.notify::<notification::DidCloseTextDocument>(&DidCloseTextDocumentParams {
+ text_document: TextDocumentIdentifier::new(path),
+ })
+ .log_err();
+ }
+ }
}
impl Drop for LanguageServer {
@@ -1288,8 +1394,6 @@ impl FakeLanguageServer {
let (stdout_writer, stdout_reader) = async_pipe::pipe();
let (notifications_tx, notifications_rx) = channel::unbounded();
- let root = Self::root_path();
-
let server_name = LanguageServerName(name.clone().into());
let process_name = Arc::from(name.as_str());
let mut server = LanguageServer::new_internal(
@@ -1300,8 +1404,6 @@ impl FakeLanguageServer {
None::<async_pipe::PipeReader>,
Arc::new(Mutex::new(None)),
None,
- root,
- root,
None,
binary.clone(),
cx.clone(),
@@ -1319,8 +1421,6 @@ impl FakeLanguageServer {
None::<async_pipe::PipeReader>,
Arc::new(Mutex::new(None)),
None,
- root,
- root,
None,
binary,
cx.clone(),
@@ -1357,16 +1457,6 @@ impl FakeLanguageServer {
(server, fake)
}
-
- #[cfg(target_os = "windows")]
- fn root_path() -> &'static Path {
- Path::new("C:\\")
- }
-
- #[cfg(not(target_os = "windows"))]
- fn root_path() -> &'static Path {
- Path::new("/")
- }
}
#[cfg(any(test, feature = "test-support"))]
@@ -1554,12 +1644,14 @@ mod tests {
})
.detach();
- let initialize_params = None;
- let configuration = DidChangeConfigurationParams {
- settings: Default::default(),
- };
let server = cx
- .update(|cx| server.initialize(initialize_params, configuration.into(), cx))
+ .update(|cx| {
+ let params = server.default_initialize_params(cx);
+ let configuration = DidChangeConfigurationParams {
+ settings: Default::default(),
+ };
+ server.initialize(params, configuration.into(), cx)
+ })
.await
.unwrap();
server
@@ -283,13 +283,13 @@ impl Prettier {
)
.context("prettier server creation")?;
- let initialize_params = None;
- let configuration = lsp::DidChangeConfigurationParams {
- settings: Default::default(),
- };
let server = cx
.update(|cx| {
- executor.spawn(server.initialize(initialize_params, configuration.into(), cx))
+ let params = server.default_initialize_params(cx);
+ let configuration = lsp::DidChangeConfigurationParams {
+ settings: Default::default(),
+ };
+ executor.spawn(server.initialize(params, configuration.into(), cx))
})?
.await
.context("prettier server initialization")?;
@@ -43,6 +43,7 @@ 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
@@ -942,9 +942,11 @@ fn language_server_for_buffer(
) -> Result<(Arc<CachedLspAdapter>, Arc<LanguageServer>)> {
lsp_store
.update(cx, |lsp_store, cx| {
- lsp_store
- .language_server_for_local_buffer(buffer.read(cx), server_id, cx)
- .map(|(adapter, server)| (adapter.clone(), server.clone()))
+ buffer.update(cx, |buffer, cx| {
+ lsp_store
+ .language_server_for_local_buffer(buffer, server_id, cx)
+ .map(|(adapter, server)| (adapter.clone(), server.clone()))
+ })
})?
.ok_or_else(|| anyhow!("no language server found for buffer"))
}
@@ -6,6 +6,7 @@ use crate::{
lsp_ext_command,
prettier_store::{self, PrettierStore, PrettierStoreEvent},
project_settings::{LspSettings, ProjectSettings},
+ project_tree::{LanguageServerTree, LaunchDisposition, ProjectTree},
relativize_path, resolve_path,
toolchain_store::{EmptyToolchainStore, ToolchainStoreEvent},
worktree_store::{WorktreeStore, WorktreeStoreEvent},
@@ -38,9 +39,9 @@ use language::{
proto::{deserialize_anchor, deserialize_version, serialize_anchor, serialize_version},
range_from_lsp, range_to_lsp, Bias, Buffer, BufferSnapshot, CachedLspAdapter, CodeLabel,
Diagnostic, DiagnosticEntry, DiagnosticSet, Diff, Documentation, File as _, Language,
- LanguageName, LanguageRegistry, LanguageServerBinaryStatus, LanguageToolchainStore, LocalFile,
- LspAdapter, LspAdapterDelegate, Patch, PointUtf16, TextBufferSnapshot, ToOffset, ToPointUtf16,
- Transaction, Unclipped,
+ LanguageRegistry, LanguageServerBinaryStatus, LanguageToolchainStore, LocalFile, LspAdapter,
+ LspAdapterDelegate, Patch, PointUtf16, TextBufferSnapshot, ToOffset, ToPointUtf16, Transaction,
+ Unclipped,
};
use lsp::{
notification::DidRenameFiles, CodeActionKind, CompletionContext, DiagnosticSeverity,
@@ -48,8 +49,8 @@ use lsp::{
FileOperationPatternKind, FileOperationRegistrationOptions, FileRename, FileSystemWatcher,
InsertTextFormat, LanguageServer, LanguageServerBinary, LanguageServerBinaryOptions,
LanguageServerId, LanguageServerName, LspRequestFuture, MessageActionItem, MessageType, OneOf,
- RenameFilesParams, ServerHealthStatus, ServerStatus, SymbolKind, TextEdit, Url,
- WillRenameFiles, WorkDoneProgressCancelParams, WorkspaceFolder,
+ RenameFilesParams, ServerHealthStatus, ServerStatus, SymbolKind, TextEdit, WillRenameFiles,
+ WorkDoneProgressCancelParams, WorkspaceFolder,
};
use node_runtime::read_package_installed_version;
use parking_lot::Mutex;
@@ -78,6 +79,7 @@ use std::{
time::{Duration, Instant},
};
use text::{Anchor, BufferId, LineEnding, OffsetRangeExt};
+use url::Url;
use util::{
debug_panic, defer, maybe, merge_json_value_into, paths::SanitizedPath, post_inc, ResultExt,
TryFutureExt as _,
@@ -130,13 +132,14 @@ impl FormatTrigger {
}
pub struct LocalLspStore {
+ weak: WeakModel<LspStore>,
worktree_store: Model<WorktreeStore>,
toolchain_store: Model<ToolchainStore>,
http_client: Arc<dyn HttpClient>,
environment: Model<ProjectEnvironment>,
fs: Arc<dyn Fs>,
languages: Arc<LanguageRegistry>,
- language_server_ids: HashMap<(WorktreeId, LanguageServerName), LanguageServerId>,
+ language_server_ids: HashMap<(WorktreeId, LanguageServerName), BTreeSet<LanguageServerId>>,
yarn: Model<YarnPathStore>,
pub language_servers: HashMap<LanguageServerId, LanguageServerState>,
buffers_being_formatted: HashSet<BufferId>,
@@ -149,7 +152,6 @@ pub struct LocalLspStore {
supplementary_language_servers:
HashMap<LanguageServerId, (LanguageServerName, Arc<LanguageServer>)>,
prettier_store: Model<PrettierStore>,
- current_lsp_settings: HashMap<LanguageServerName, LspSettings>,
next_diagnostic_group_id: usize,
diagnostics: HashMap<
WorktreeId,
@@ -163,7 +165,7 @@ pub struct LocalLspStore {
>,
buffer_snapshots: HashMap<BufferId, HashMap<LanguageServerId, Vec<LspBufferSnapshot>>>, // buffer_id -> server_id -> vec of snapshots
_subscription: gpui::Subscription,
- registered_buffers: HashMap<BufferId, usize>,
+ lsp_tree: Model<LanguageServerTree>,
}
impl LocalLspStore {
@@ -172,26 +174,15 @@ impl LocalLspStore {
worktree_handle: &Model<Worktree>,
delegate: Arc<LocalLspAdapterDelegate>,
adapter: Arc<CachedLspAdapter>,
- cx: &mut ModelContext<LspStore>,
- ) {
+ settings: Arc<LspSettings>,
+ cx: &mut AppContext,
+ ) -> LanguageServerId {
let worktree = worktree_handle.read(cx);
let worktree_id = worktree.id();
let root_path = worktree.abs_path();
let key = (worktree_id, adapter.name.clone());
- if self.language_server_ids.contains_key(&key) {
- return;
- }
-
- let project_settings = ProjectSettings::get(
- Some(SettingsLocation {
- worktree_id,
- path: Path::new(""),
- }),
- cx,
- );
- let lsp = project_settings.lsp.get(&adapter.name);
- let override_options = lsp.and_then(|s| s.initialization_options.clone());
+ let override_options = settings.initialization_options.clone();
let stderr_capture = Arc::new(Mutex::new(Some(String::new())));
@@ -207,12 +198,13 @@ impl LocalLspStore {
let adapter = adapter.clone();
let server_name = adapter.name.clone();
let stderr_capture = stderr_capture.clone();
+ #[cfg(any(test, feature = "test-support"))]
+ let lsp_store = self.weak.clone();
- move |_lsp_store, cx| async move {
+ move |cx| async move {
let binary = binary.await?;
-
#[cfg(any(test, feature = "test-support"))]
- if let Some(server) = _lsp_store
+ if let Some(server) = lsp_store
.update(&mut cx.clone(), |this, cx| {
this.languages.create_fake_language_server(
server_id,
@@ -239,13 +231,15 @@ impl LocalLspStore {
}
});
- let state = LanguageServerState::Starting({
+ let pending_workspace_folders: Arc<Mutex<BTreeSet<Url>>> = Default::default();
+ let startup = {
let server_name = adapter.name.0.clone();
let delegate = delegate as Arc<dyn LspAdapterDelegate>;
let key = key.clone();
let adapter = adapter.clone();
-
- cx.spawn(move |this, mut cx| async move {
+ let this = self.weak.clone();
+ let pending_workspace_folders = pending_workspace_folders.clone();
+ cx.spawn(move |mut cx| async move {
let result = {
let delegate = delegate.clone();
let adapter = adapter.clone();
@@ -292,7 +286,7 @@ impl LocalLspStore {
let language_server = cx
.update(|cx| {
language_server.initialize(
- Some(initialization_params),
+ initialization_params,
did_change_configuration_params.clone(),
cx,
)
@@ -326,6 +320,7 @@ impl LocalLspStore {
server.clone(),
server_id,
key,
+ pending_workspace_folders,
&mut cx,
);
})
@@ -348,82 +343,18 @@ impl LocalLspStore {
}
}
})
- });
+ };
+ let state = LanguageServerState::Starting {
+ startup,
+ pending_workspace_folders,
+ };
self.language_servers.insert(server_id, state);
- self.language_server_ids.insert(key, server_id);
- }
-
- pub fn start_language_servers(
- &mut self,
- worktree: &Model<Worktree>,
- language: LanguageName,
- cx: &mut ModelContext<LspStore>,
- ) {
- let root_file = worktree
- .update(cx, |tree, cx| tree.root_file(cx))
- .map(|f| f as _);
- let settings = language_settings(Some(language.clone()), root_file.as_ref(), cx);
- if !settings.enable_language_server {
- return;
- }
-
- let available_lsp_adapters = self.languages.clone().lsp_adapters(&language);
- 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 mut enabled_lsp_adapters: Vec<Arc<CachedLspAdapter>> = Vec::new();
- for desired_language_server in desired_language_servers {
- if let Some(adapter) = available_lsp_adapters
- .iter()
- .find(|adapter| adapter.name == desired_language_server)
- {
- enabled_lsp_adapters.push(adapter.clone());
- continue;
- }
-
- if let Some(adapter) = self
- .languages
- .load_available_lsp_adapter(&desired_language_server)
- {
- self.languages
- .register_lsp_adapter(language.clone(), adapter.adapter.clone());
- enabled_lsp_adapters.push(adapter);
- continue;
- }
-
- log::warn!(
- "no language server found matching '{}'",
- desired_language_server.0
- );
- }
-
- for adapter in &enabled_lsp_adapters {
- let delegate = LocalLspAdapterDelegate::new(
- self.languages.clone(),
- &self.environment,
- cx.weak_model(),
- &worktree,
- self.http_client.clone(),
- self.fs.clone(),
- cx,
- );
- self.start_language_server(worktree, delegate, adapter.clone(), cx);
- }
-
- // 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, enabled_lsp_adapters);
+ self.language_server_ids
+ .entry(key)
+ .or_default()
+ .insert(server_id);
+ server_id
}
fn get_language_server_binary(
@@ -431,7 +362,7 @@ impl LocalLspStore {
adapter: Arc<CachedLspAdapter>,
delegate: Arc<dyn LspAdapterDelegate>,
allow_binary_download: bool,
- cx: &mut ModelContext<LspStore>,
+ cx: &mut AppContext,
) -> Task<Result<LanguageServerBinary>> {
let settings = ProjectSettings::get(
Some(SettingsLocation {
@@ -446,7 +377,7 @@ impl LocalLspStore {
if settings.as_ref().is_some_and(|b| b.path.is_some()) {
let settings = settings.unwrap();
- return cx.spawn(|_, _| async move {
+ return cx.spawn(|_| async move {
Ok(LanguageServerBinary {
path: PathBuf::from(&settings.path.unwrap()),
env: Some(delegate.shell_env().await),
@@ -467,7 +398,7 @@ impl LocalLspStore {
allow_binary_download,
};
let toolchains = self.toolchain_store.read(cx).as_language_toolchain_store();
- cx.spawn(|_, mut cx| async move {
+ cx.spawn(|mut cx| async move {
let binary_result = adapter
.clone()
.get_language_server_command(
@@ -567,14 +498,16 @@ impl LocalLspStore {
else {
return Ok(None);
};
- let root = server.root_path();
- let Ok(uri) = Url::from_file_path(&root) else {
- return Ok(None);
- };
- Ok(Some(vec![WorkspaceFolder {
- uri,
- name: Default::default(),
- }]))
+ let root = server.workspace_folders();
+ Ok(Some(
+ root.iter()
+ .cloned()
+ .map(|uri| WorkspaceFolder {
+ uri,
+ name: Default::default(),
+ })
+ .collect(),
+ ))
}
}
})
@@ -996,7 +929,7 @@ impl LocalLspStore {
use LanguageServerState::*;
match server_state {
Running { server, .. } => server.shutdown()?.await,
- Starting(task) => task.await?.shutdown()?.await,
+ Starting { startup, .. } => startup.await?.shutdown()?.await,
}
})
.collect::<Vec<_>>();
@@ -1012,42 +945,58 @@ impl LocalLspStore {
) -> impl Iterator<Item = &Arc<LanguageServer>> {
self.language_server_ids
.iter()
- .filter_map(move |((language_server_worktree_id, _), id)| {
- if *language_server_worktree_id == worktree_id {
+ .flat_map(move |((language_server_path, _), ids)| {
+ ids.iter().filter_map(move |id| {
+ if *language_server_path != worktree_id {
+ return None;
+ }
if let Some(LanguageServerState::Running { server, .. }) =
self.language_servers.get(id)
{
return Some(server);
+ } else {
+ None
}
- }
- None
+ })
})
}
- pub(crate) fn language_server_ids_for_buffer(
+ fn language_server_ids_for_buffer(
&self,
buffer: &Buffer,
- cx: &AppContext,
+ cx: &mut AppContext,
) -> Vec<LanguageServerId> {
if let Some((file, language)) = File::from_dyn(buffer.file()).zip(buffer.language()) {
let worktree_id = file.worktree_id(cx);
- self.languages
- .lsp_adapters(&language.name())
- .iter()
- .flat_map(|adapter| {
- let key = (worktree_id, adapter.name.clone());
- self.language_server_ids.get(&key).copied()
- })
- .collect()
+
+ let Some(path): Option<Arc<Path>> = file.path().parent().map(Arc::from) else {
+ return vec![];
+ };
+ let worktree_path = ProjectPath { worktree_id, path };
+ let Some(worktree) = self
+ .worktree_store
+ .read(cx)
+ .worktree_for_id(worktree_id, cx)
+ else {
+ return vec![];
+ };
+ let delegate = LocalLspAdapterDelegate::from_local_lsp(self, &worktree, cx);
+ let root = self.lsp_tree.update(cx, |this, cx| {
+ this.get(worktree_path, &language.name(), delegate, cx)
+ .filter_map(|node| node.server_id())
+ .collect::<Vec<_>>()
+ });
+
+ root
} else {
Vec::new()
}
}
- pub(crate) fn language_servers_for_buffer<'a>(
+ fn language_servers_for_buffer<'a>(
&'a self,
buffer: &'a Buffer,
- cx: &'a AppContext,
+ cx: &'a mut AppContext,
) -> impl Iterator<Item = (&'a Arc<CachedLspAdapter>, &'a Arc<LanguageServer>)> {
self.language_server_ids_for_buffer(buffer, cx)
.into_iter()
@@ -1062,7 +1011,7 @@ impl LocalLspStore {
fn primary_language_server_for_buffer<'a>(
&'a self,
buffer: &'a Buffer,
- cx: &'a AppContext,
+ cx: &'a mut AppContext,
) -> Option<(&'a Arc<CachedLspAdapter>, &'a Arc<LanguageServer>)> {
// The list of language servers is ordered based on the `language_servers` setting
// for each language, thus we can consider the first one in the list to be the
@@ -1108,20 +1057,22 @@ impl LocalLspStore {
for buffer in &buffers {
let (primary_adapter_and_server, adapters_and_servers) =
lsp_store.update(&mut cx, |lsp_store, cx| {
- let buffer = buffer.handle.read(cx);
-
- let adapters_and_servers = lsp_store
- .as_local()
- .unwrap()
- .language_servers_for_buffer(buffer, cx)
- .map(|(adapter, lsp)| (adapter.clone(), lsp.clone()))
- .collect::<Vec<_>>();
+ let adapters_and_servers = buffer.handle.update(cx, |buffer, cx| {
+ lsp_store
+ .as_local()
+ .unwrap()
+ .language_servers_for_buffer(buffer, cx)
+ .map(|(adapter, lsp)| (adapter.clone(), lsp.clone()))
+ .collect::<Vec<_>>()
+ });
- let primary_adapter = lsp_store
- .as_local()
- .unwrap()
- .primary_language_server_for_buffer(buffer, cx)
- .map(|(adapter, lsp)| (adapter.clone(), lsp.clone()));
+ let primary_adapter = buffer.handle.update(cx, |buffer, cx| {
+ lsp_store
+ .as_local()
+ .unwrap()
+ .primary_language_server_for_buffer(buffer, cx)
+ .map(|(adapter, lsp)| (adapter.clone(), lsp.clone()))
+ });
(primary_adapter, adapters_and_servers)
})?;
@@ -1730,7 +1681,8 @@ impl LocalLspStore {
) {
let buffer = buffer_handle.read(cx);
- let Some(file) = File::from_dyn(buffer.file()) else {
+ let file = buffer.file().cloned();
+ let Some(file) = File::from_dyn(file.as_ref()) else {
return;
};
if !file.is_local() {
@@ -1738,7 +1690,6 @@ impl LocalLspStore {
}
let worktree_id = file.worktree_id(cx);
- let language = buffer.language().cloned();
if let Some(diagnostics) = self.diagnostics.get(&worktree_id) {
for (server_id, diagnostics) in
@@ -1748,45 +1699,6 @@ impl LocalLspStore {
.log_err();
}
}
-
- let Some(language) = language else {
- return;
- };
- for adapter in self.languages.lsp_adapters(&language.name()) {
- let server = self
- .language_server_ids
- .get(&(worktree_id, adapter.name.clone()))
- .and_then(|id| self.language_servers.get(id))
- .and_then(|server_state| {
- if let LanguageServerState::Running { server, .. } = server_state {
- Some(server.clone())
- } else {
- None
- }
- });
- let server = match server {
- Some(server) => server,
- None => continue,
- };
-
- buffer_handle.update(cx, |buffer, cx| {
- buffer.set_completion_triggers(
- server.server_id(),
- server
- .capabilities()
- .completion_provider
- .as_ref()
- .and_then(|provider| {
- provider
- .trigger_characters
- .as_ref()
- .map(|characters| characters.iter().cloned().collect())
- })
- .unwrap_or_default(),
- cx,
- );
- });
- }
}
pub(crate) fn reset_buffer(
@@ -1798,14 +1710,35 @@ impl LocalLspStore {
buffer.update(cx, |buffer, cx| {
let worktree_id = old_file.worktree_id(cx);
- let ids = &self.language_server_ids;
-
if let Some(language) = buffer.language().cloned() {
- for adapter in self.languages.lsp_adapters(&language.name()) {
- if let Some(server_id) = ids.get(&(worktree_id, adapter.name.clone())) {
- buffer.update_diagnostics(*server_id, DiagnosticSet::new([], buffer), cx);
- buffer.set_completion_triggers(*server_id, Default::default(), cx);
- }
+ let Some(worktree) = self
+ .worktree_store
+ .read(cx)
+ .worktree_for_id(worktree_id, cx)
+ else {
+ return;
+ };
+ let Some(path): Option<Arc<Path>> = old_file.path().parent().map(Arc::from) else {
+ return;
+ };
+
+ let delegate = LocalLspAdapterDelegate::from_local_lsp(self, &worktree, cx);
+ let nodes = self.lsp_tree.update(cx, |this, cx| {
+ this.get(
+ ProjectPath { worktree_id, path },
+ &language.name(),
+ delegate,
+ cx,
+ )
+ .collect::<Vec<_>>()
+ });
+ for node in nodes {
+ let Some(server_id) = node.server_id() else {
+ continue;
+ };
+
+ buffer.update_diagnostics(server_id, DiagnosticSet::new([], buffer), cx);
+ buffer.set_completion_triggers(server_id, Default::default(), cx);
}
}
});
@@ -1909,81 +1842,186 @@ impl LocalLspStore {
};
let initial_snapshot = buffer.text_snapshot();
let worktree_id = file.worktree_id(cx);
- let worktree = file.worktree.clone();
let Some(language) = buffer.language().cloned() else {
return;
};
- self.start_language_servers(&worktree, language.name(), cx);
+ let Some(path): Option<Arc<Path>> = file.path().parent().map(Arc::from) else {
+ return;
+ };
+ let Some(worktree) = self
+ .worktree_store
+ .read(cx)
+ .worktree_for_id(worktree_id, cx)
+ else {
+ return;
+ };
+ let delegate = LocalLspAdapterDelegate::from_local_lsp(self, &worktree, cx);
+ let servers = self.lsp_tree.clone().update(cx, |this, cx| {
+ this.get(
+ ProjectPath { worktree_id, path },
+ &language.name(),
+ delegate.clone(),
+ cx,
+ )
+ .collect::<Vec<_>>()
+ });
+ let servers = servers
+ .into_iter()
+ .filter_map(|server_node| {
+ let server_id = server_node.server_id_or_init(
+ |LaunchDisposition {
+ server_name,
+ attach,
+ path,
+ settings,
+ }| match attach {
+ language::Attach::InstancePerRoot => {
+ // todo: handle instance per root proper.
+ if let Some(server_ids) = self
+ .language_server_ids
+ .get(&(worktree_id, server_name.clone()))
+ {
+ server_ids.iter().cloned().next().unwrap()
+ } else {
+ let language_name = language.name();
+
+ self.start_language_server(
+ &worktree,
+ delegate.clone(),
+ self.languages
+ .lsp_adapters(&language_name)
+ .into_iter()
+ .find(|adapter| &adapter.name() == server_name)
+ .expect("To find LSP adapter"),
+ settings,
+ cx,
+ )
+ }
+ }
+ language::Attach::Shared => {
+ let uri = Url::from_directory_path(
+ worktree.read(cx).abs_path().join(&path.path),
+ );
+ let key = (worktree_id, server_name.clone());
+ if !self.language_server_ids.contains_key(&key) {
+ let language_name = language.name();
+ self.start_language_server(
+ &worktree,
+ delegate.clone(),
+ self.languages
+ .lsp_adapters(&language_name)
+ .into_iter()
+ .find(|adapter| &adapter.name() == server_name)
+ .expect("To find LSP adapter"),
+ settings,
+ cx,
+ );
+ }
+ if let Some(server_ids) = self
+ .language_server_ids
+ .get(&key)
+ {
+ debug_assert_eq!(server_ids.len(), 1);
+ let server_id = server_ids.iter().cloned().next().unwrap();
+
+ if let Some(state) = self.language_servers.get(&server_id) {
+ if let Ok(uri) = uri {
+ state.add_workspace_folder(uri);
+ };
+ }
+ server_id
+ } else {
+ unreachable!("Language server ID should be available, as it's registered on demand")
+ }
+ }
+ },
+ )?;
+ let server_state = self.language_servers.get(&server_id)?;
+ if let LanguageServerState::Running { server, .. } = server_state {
+ Some(server.clone())
+ } else {
+ None
+ }
+ })
+ .collect::<Vec<_>>();
+ for server in servers {
+ buffer_handle.update(cx, |buffer, cx| {
+ buffer.set_completion_triggers(
+ server.server_id(),
+ server
+ .capabilities()
+ .completion_provider
+ .as_ref()
+ .and_then(|provider| {
+ provider
+ .trigger_characters
+ .as_ref()
+ .map(|characters| characters.iter().cloned().collect())
+ })
+ .unwrap_or_default(),
+ cx,
+ );
+ });
+ }
for adapter in self.languages.lsp_adapters(&language.name()) {
- let server = self
+ let servers = self
.language_server_ids
.get(&(worktree_id, adapter.name.clone()))
- .and_then(|id| self.language_servers.get(id))
- .and_then(|server_state| {
- if let LanguageServerState::Running { server, .. } = server_state {
- Some(server.clone())
- } else {
- None
- }
+ .map(|ids| {
+ ids.iter().flat_map(|id| {
+ self.language_servers.get(id).and_then(|server_state| {
+ if let LanguageServerState::Running { server, .. } = server_state {
+ Some(server.clone())
+ } else {
+ None
+ }
+ })
+ })
});
- let server = match server {
+ let servers = match servers {
Some(server) => server,
None => continue,
};
- server
- .notify::<lsp::notification::DidOpenTextDocument>(&lsp::DidOpenTextDocumentParams {
- text_document: lsp::TextDocumentItem::new(
- uri.clone(),
- adapter.language_id(&language.name()),
- 0,
- initial_snapshot.text(),
- ),
- })
- .log_err();
+ for server in servers {
+ server.register_buffer(
+ buffer_id,
+ uri.clone(),
+ adapter.language_id(&language.name()),
+ 0,
+ initial_snapshot.text(),
+ );
- let snapshot = LspBufferSnapshot {
- version: 0,
- snapshot: initial_snapshot.clone(),
- };
- self.buffer_snapshots
- .entry(buffer_id)
- .or_default()
- .insert(server.server_id(), vec![snapshot]);
+ let snapshot = LspBufferSnapshot {
+ version: 0,
+ snapshot: initial_snapshot.clone(),
+ };
+ self.buffer_snapshots
+ .entry(buffer_id)
+ .or_default()
+ .insert(server.server_id(), vec![snapshot]);
+ }
}
}
+
pub(crate) fn unregister_old_buffer_from_language_servers(
&mut self,
buffer: &Model<Buffer>,
- old_file: &File,
-
cx: &mut AppContext,
) {
- let old_path = match old_file.as_local() {
- Some(local) => local.abs_path(cx),
- None => return,
- };
- let file_url = lsp::Url::from_file_path(old_path).unwrap();
- self.unregister_buffer_from_language_servers(buffer, file_url, cx);
+ self.unregister_buffer_from_language_servers(buffer, cx);
}
pub(crate) fn unregister_buffer_from_language_servers(
&mut self,
buffer: &Model<Buffer>,
- file_url: lsp::Url,
cx: &mut AppContext,
) {
buffer.update(cx, |buffer, cx| {
self.buffer_snapshots.remove(&buffer.remote_id());
for (_, language_server) in self.language_servers_for_buffer(buffer, cx) {
- language_server
- .notify::<lsp::notification::DidCloseTextDocument>(
- &lsp::DidCloseTextDocumentParams {
- text_document: lsp::TextDocumentIdentifier::new(file_url.clone()),
- },
- )
- .log_err();
+ language_server.unregister_buffer(buffer.remote_id());
}
});
}
@@ -2509,6 +2547,46 @@ impl LocalLspStore {
failure_reason: None,
})
}
+
+ fn remove_worktree(
+ &mut self,
+ id_to_remove: WorktreeId,
+ cx: &mut ModelContext<'_, LspStore>,
+ ) -> Vec<LanguageServerId> {
+ 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 ((path, server_name), ref server_ids) in &self.language_server_ids {
+ if *path == id_to_remove {
+ servers_to_remove.extend(server_ids.iter().map(|id| (*id, server_name.clone())));
+ } else {
+ servers_to_preserve.extend(server_ids.iter().cloned());
+ }
+ }
+ servers_to_remove.retain(|server_id, _| !servers_to_preserve.contains(server_id));
+
+ for (server_id_to_remove, _) in &servers_to_remove {
+ self.language_server_ids
+ .values_mut()
+ .for_each(|server_ids| {
+ server_ids.remove(server_id_to_remove);
+ });
+ 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,
@@ -2845,6 +2923,7 @@ pub struct LanguageServerStatus {
struct CoreSymbol {
pub language_server_name: LanguageServerName,
pub source_worktree_id: WorktreeId,
+ pub source_language_server_id: LanguageServerId,
pub path: ProjectPath,
pub name: String,
pub kind: lsp::SymbolKind,
@@ -2924,23 +3003,6 @@ impl LspStore {
}
}
- pub fn swap_current_lsp_settings(
- &mut self,
- new_settings: HashMap<LanguageServerName, LspSettings>,
- ) -> Option<HashMap<LanguageServerName, LspSettings>> {
- match &mut self.mode {
- LspStoreMode::Local(LocalLspStore {
- current_lsp_settings,
- ..
- }) => {
- let ret = mem::take(current_lsp_settings);
- *current_lsp_settings = new_settings;
- Some(ret)
- }
- LspStoreMode::Remote(_) => None,
- }
- }
-
#[allow(clippy::too_many_arguments)]
pub fn new_local(
buffer_store: Model<BufferStore>,
@@ -2969,8 +3031,10 @@ impl LspStore {
let (sender, receiver) = watch::channel();
(Self::maintain_workspace_config(receiver, cx), sender)
};
+ let project_tree = ProjectTree::new(worktree_store.clone(), cx);
Self {
mode: LspStoreMode::Local(LocalLspStore {
+ weak: cx.weak_model(),
worktree_store: worktree_store.clone(),
toolchain_store: toolchain_store.clone(),
supplementary_language_servers: Default::default(),
@@ -2981,7 +3045,6 @@ impl LspStore {
language_server_watched_paths: Default::default(),
language_server_paths_watched_for_rename: Default::default(),
language_server_watcher_registrations: Default::default(),
- current_lsp_settings: ProjectSettings::get_global(cx).lsp.clone(),
buffers_being_formatted: Default::default(),
buffer_snapshots: Default::default(),
prettier_store,
@@ -2994,7 +3057,7 @@ impl LspStore {
_subscription: cx.on_app_quit(|this, cx| {
this.as_local_mut().unwrap().shutdown_language_servers(cx)
}),
- registered_buffers: HashMap::default(),
+ lsp_tree: LanguageServerTree::new(project_tree, languages.clone(), cx),
}),
last_formatting_failure: None,
downstream_client: None,
@@ -3008,7 +3071,7 @@ impl LspStore {
active_entry: None,
_maintain_workspace_config,
- _maintain_buffer_languages: Self::maintain_buffer_languages(languages.clone(), cx),
+ _maintain_buffer_languages: Self::maintain_buffer_languages(languages, cx),
}
}
@@ -3067,17 +3130,6 @@ impl LspStore {
}
}
- fn worktree_for_id(
- &self,
- worktree_id: WorktreeId,
- cx: &ModelContext<Self>,
- ) -> Result<Model<Worktree>> {
- self.worktree_store
- .read(cx)
- .worktree_for_id(worktree_id, cx)
- .ok_or_else(|| anyhow!("worktree not found"))
- }
-
fn on_buffer_store_event(
&mut self,
_: Model<BufferStore>,
@@ -3089,22 +3141,19 @@ impl LspStore {
self.on_buffer_added(buffer, cx).log_err();
}
BufferStoreEvent::BufferChangedFilePath { buffer, old_file } => {
- let buffer_id = buffer.read(cx).remote_id();
- if let Some(old_file) = File::from_dyn(old_file.as_ref()) {
- if let Some(local) = self.as_local_mut() {
+ if let Some(local) = self.as_local_mut() {
+ if let Some(old_file) = File::from_dyn(old_file.as_ref()) {
local.reset_buffer(buffer, old_file, cx);
- if local.registered_buffers.contains_key(&buffer_id) {
- local.unregister_old_buffer_from_language_servers(buffer, old_file, cx);
- }
+
+ local.unregister_old_buffer_from_language_servers(buffer, cx);
}
}
self.detect_language_for_buffer(buffer, cx);
if let Some(local) = self.as_local_mut() {
local.initialize_buffer(buffer, cx);
- if local.registered_buffers.contains_key(&buffer_id) {
- local.register_buffer_with_language_servers(buffer, cx);
- }
+
+ local.register_buffer_with_language_servers(buffer, cx);
}
}
BufferStoreEvent::BufferDropped(_) => {}
@@ -3235,8 +3284,6 @@ impl LspStore {
buffer: &Model<Buffer>,
cx: &mut ModelContext<Self>,
) -> OpenLspBufferHandle {
- let buffer_id = buffer.read(cx).remote_id();
-
let handle = cx.new_model(|_| buffer.clone());
if let Some(local) = self.as_local_mut() {
@@ -3246,25 +3293,12 @@ impl LspStore {
if !file.is_local() {
return handle;
}
- let refcount = local.registered_buffers.entry(buffer_id).or_insert(0);
- *refcount += 1;
- if *refcount == 1 {
- local.register_buffer_with_language_servers(buffer, cx);
- }
+
+ local.register_buffer_with_language_servers(buffer, cx);
cx.observe_release(&handle, move |this, buffer, cx| {
let local = this.as_local_mut().unwrap();
- let Some(refcount) = local.registered_buffers.get_mut(&buffer_id) else {
- debug_panic!("bad refcounting");
- return;
- };
- *refcount -= 1;
- if *refcount == 0 {
- local.registered_buffers.remove(&buffer_id);
- if let Some(file) = File::from_dyn(buffer.read(cx).file()).cloned() {
- local.unregister_old_buffer_from_language_servers(&buffer, &file, cx);
- }
- }
+ local.unregister_old_buffer_from_language_servers(&buffer, cx);
})
.detach();
} else if let Some((upstream_client, upstream_project_id)) = self.upstream_client() {
@@ -3308,14 +3342,10 @@ impl LspStore {
.update(cx, |buffer, cx| buffer.set_language(None, cx));
if let Some(local) = this.as_local_mut() {
local.reset_buffer(&buffer, &f, cx);
- if local
- .registered_buffers
- .contains_key(&buffer.read(cx).remote_id())
- {
- local.unregister_old_buffer_from_language_servers(
- &buffer, &f, cx,
- );
- }
+
+ local.unregister_old_buffer_from_language_servers(
+ &buffer, cx,
+ );
}
}
}
@@ -3341,12 +3371,7 @@ impl LspStore {
this.detect_language_for_buffer(&buffer, cx);
if let Some(local) = this.as_local_mut() {
local.initialize_buffer(&buffer, cx);
- if local
- .registered_buffers
- .contains_key(&buffer.read(cx).remote_id())
- {
- local.register_buffer_with_language_servers(&buffer, cx);
- }
+ local.register_buffer_with_language_servers(&buffer, cx);
}
}
@@ -40,7 +40,7 @@ pub struct PrettierStore {
prettier_instances: HashMap<PathBuf, PrettierInstance>,
}
-pub enum PrettierStoreEvent {
+pub(crate) enum PrettierStoreEvent {
LanguageServerRemoved(LanguageServerId),
LanguageServerAdded {
new_server_id: LanguageServerId,
@@ -9,6 +9,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;
@@ -474,6 +475,7 @@ 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,
@@ -1890,7 +1892,7 @@ impl Project {
pub fn open_buffer(
&mut self,
path: impl Into<ProjectPath>,
- cx: &mut ModelContext<Self>,
+ cx: &mut AppContext,
) -> Task<Result<Model<Buffer>>> {
if self.is_disconnected(cx) {
return Task::ready(Err(anyhow!(ErrorCode::Disconnected)));
@@ -1905,11 +1907,11 @@ impl Project {
pub fn open_buffer_with_lsp(
&mut self,
path: impl Into<ProjectPath>,
- cx: &mut ModelContext<Self>,
+ cx: &mut AppContext,
) -> Task<Result<(Model<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)
@@ -4145,14 +4147,25 @@ impl Project {
self.lsp_store.read(cx).supplementary_language_servers()
}
- pub fn language_servers_for_local_buffer<'a>(
- &'a self,
- buffer: &'a Buffer,
- cx: &'a AppContext,
- ) -> impl Iterator<Item = (&'a Arc<CachedLspAdapter>, &'a Arc<LanguageServer>)> {
- self.lsp_store
- .read(cx)
- .language_servers_for_local_buffer(buffer, cx)
+ pub fn language_server_for_id(
+ &self,
+ id: LanguageServerId,
+ cx: &AppContext,
+ ) -> 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 AppContext,
+ ) -> R {
+ self.lsp_store.update(cx, |this, cx| {
+ callback(Box::new(this.language_servers_for_local_buffer(buffer, cx)))
+ })
}
pub fn buffer_store(&self) -> &Model<BufferStore> {
@@ -1749,6 +1749,12 @@ 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
@@ -2573,25 +2579,28 @@ 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/b.rs".as_ref()], cx).await;
+ let project = Project::test(fs, ["/dir".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;
@@ -2603,12 +2612,11 @@ async fn test_definition(cx: &mut gpui::TestAppContext) {
Ok(Some(lsp::GotoDefinitionResponse::Scalar(
lsp::Location::new(
- lsp::Url::from_file_path("/dir/a.rs").unwrap(),
+ lsp::Url::from_file_path("/another_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
@@ -2629,18 +2637,21 @@ async fn test_definition(cx: &mut gpui::TestAppContext) {
.as_local()
.unwrap()
.abs_path(cx),
- Path::new("/dir/a.rs"),
+ Path::new("/another_dir/a.rs"),
);
assert_eq!(definition.target.range.to_offset(target_buffer), 9..10);
assert_eq!(
list_worktrees(&project, cx),
- [("/dir/a.rs".as_ref(), false), ("/dir/b.rs".as_ref(), true)],
+ [
+ ("/another_dir/a.rs".as_ref(), false),
+ ("/dir".as_ref(), true)
+ ],
);
drop(definition);
});
cx.update(|cx| {
- assert_eq!(list_worktrees(&project, cx), [("/dir/b.rs".as_ref(), true)]);
+ assert_eq!(list_worktrees(&project, cx), [("/dir".as_ref(), true)]);
});
fn list_worktrees<'a>(
@@ -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));
+ }
+ _ => {}
+ }
+ }
+}
@@ -0,0 +1,241 @@
+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)(¤t.worktree_relative_path, ¤t.labels).is_break() {
+ return;
+ };
+ }
+ current = match current.children.get(key) {
+ Some(child) => child,
+ None => return,
+ };
+ }
+ if !current.labels.is_empty() {
+ (callback)(¤t.worktree_relative_path, ¤t.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/")
+ );
+ }
+}
@@ -0,0 +1,428 @@
+//! 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::{AppContext, Context as _, Model, Subscription};
+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: Model<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(),
+ }
+ }
+}
+
+impl LanguageServerTree {
+ pub(crate) fn new(
+ project_tree: Model<ProjectTree>,
+ languages: Arc<LanguageRegistry>,
+ cx: &mut AppContext,
+ ) -> Model<Self> {
+ cx.new_model(|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,
+ language_name: &LanguageName,
+ delegate: Arc<dyn LspAdapterDelegate>,
+ cx: &mut AppContext,
+ ) -> impl Iterator<Item = LanguageServerTreeNode> + 'a {
+ let settings_location = SettingsLocation {
+ worktree_id: path.worktree_id,
+ path: &path.path,
+ };
+ let adapters = self.adapters_for_language(settings_location, language_name, cx);
+ 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 AppContext,
+ ) -> 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 (settings, new_languages) = adapters.get(&adapter).cloned()?;
+ 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,
+ )),
+ Default::default(),
+ )
+ });
+ languages.extend(new_languages);
+ Some(Arc::downgrade(&node).into())
+ })
+ }
+
+ fn adapters_for_language(
+ &self,
+ settings_location: SettingsLocation,
+ language_name: &LanguageName,
+ cx: &AppContext,
+ ) -> 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<_, _>>();
+ adapters_with_settings
+ }
+
+ pub(crate) fn on_settings_changed(
+ &mut self,
+ get_delegate: &mut dyn FnMut(
+ WorktreeId,
+ &mut AppContext,
+ ) -> Option<Arc<dyn LspAdapterDelegate>>,
+ spawn_language_server: &mut dyn FnMut(
+ LaunchDisposition,
+ &mut AppContext,
+ ) -> LanguageServerId,
+ on_language_server_removed: &mut dyn FnMut(LanguageServerId),
+ cx: &mut AppContext,
+ ) {
+ // 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(())
+ }
+ };
+ }
+}
@@ -765,6 +765,7 @@ message Symbol {
PointUtf16 start = 7;
PointUtf16 end = 8;
bytes signature = 9;
+ uint64 language_server_id = 10;
}
message OpenBufferForSymbol {
@@ -16,4 +16,4 @@ doctest = false
[dependencies]
syn = "1.0.72"
quote = "1.0.9"
-proc-macro2 = "1.0.66"
+proc-macro2 = "1.0.93"
@@ -13,7 +13,7 @@ path = "src/ui_macros.rs"
proc-macro = true
[dependencies]
-proc-macro2 = "1.0.66"
+proc-macro2 = "1.0.93"
quote = "1.0.9"
syn = { version = "1.0.72", features = ["full", "extra-traits"] }
convert_case.workspace = true
@@ -95,26 +95,17 @@ impl Render for QuickActionBar {
show_git_blame_gutter,
auto_signature_help_enabled,
inline_completions_enabled,
- ) = {
- let editor = editor.read(cx);
- let selection_menu_enabled = editor.selection_menu_enabled(cx);
- let inlay_hints_enabled = editor.inlay_hints_enabled();
- let supports_inlay_hints = editor.supports_inlay_hints(cx);
- let git_blame_inline_enabled = editor.git_blame_inline_enabled();
- let show_git_blame_gutter = editor.show_git_blame_gutter();
- let auto_signature_help_enabled = editor.auto_signature_help_enabled(cx);
- let inline_completions_enabled = editor.inline_completions_enabled(cx);
-
+ ) = editor.update(cx, |editor, cx| {
(
- selection_menu_enabled,
- inlay_hints_enabled,
- supports_inlay_hints,
- git_blame_inline_enabled,
- show_git_blame_gutter,
- auto_signature_help_enabled,
- inline_completions_enabled,
+ editor.selection_menu_enabled(cx),
+ editor.inlay_hints_enabled(),
+ editor.supports_inlay_hints(cx),
+ editor.git_blame_inline_enabled(),
+ editor.show_git_blame_gutter(),
+ editor.auto_signature_help_enabled(cx),
+ editor.inline_completions_enabled(cx),
)
- };
+ });
let focus_handle = editor.read(cx).focus_handle(cx);
@@ -450,16 +441,19 @@ impl ToolbarItemView for QuickActionBar {
if let Some(editor) = active_item.downcast::<Editor>() {
let mut inlay_hints_enabled = editor.read(cx).inlay_hints_enabled();
- let mut supports_inlay_hints = editor.read(cx).supports_inlay_hints(cx);
+ let mut supports_inlay_hints =
+ editor.update(cx, |this, cx| this.supports_inlay_hints(cx));
self._inlay_hints_enabled_subscription =
Some(cx.observe(&editor, move |_, editor, cx| {
- let editor = editor.read(cx);
- let new_inlay_hints_enabled = editor.inlay_hints_enabled();
- let new_supports_inlay_hints = editor.supports_inlay_hints(cx);
- let should_notify = inlay_hints_enabled != new_inlay_hints_enabled
- || supports_inlay_hints != new_supports_inlay_hints;
- inlay_hints_enabled = new_inlay_hints_enabled;
- supports_inlay_hints = new_supports_inlay_hints;
+ let mut should_notify = false;
+ editor.update(cx, |editor, cx| {
+ let new_inlay_hints_enabled = editor.inlay_hints_enabled();
+ let new_supports_inlay_hints = editor.supports_inlay_hints(cx);
+ should_notify = inlay_hints_enabled != new_inlay_hints_enabled
+ || supports_inlay_hints != new_supports_inlay_hints;
+ inlay_hints_enabled = new_inlay_hints_enabled;
+ supports_inlay_hints = new_supports_inlay_hints;
+ });
if should_notify {
cx.notify()
}